Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Test gate for a repository that currently ships documentation only.
# Test gate for the LiteGrip C++ SDK.
#
# Tests live in their own workflow: they run on pull requests, while the release
# workflow runs on pushes to the default branch. Mixing the two means a broken
# release blocks a pull request, or the reverse.
#
# Replace the test step with the project's real toolchain and test command as soon
# as source code lands; see the repository standards in AGENTS.md.
# The suite runs without hardware: test_transport is deliberately
# non-transmitting and every other test covers behaviour that must hold while
# disconnected. No CAN interface and no root are required.
#
# AGENTS.md is deliberately not linted: it is canonical content synced from
# repo-template and must not be edited here.
Expand All @@ -31,6 +32,15 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4

- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

- name: Build
run: cmake --build build -j

- name: Run Tests
run: ctest --test-dir build --output-on-failure

- name: Setup Node
uses: actions/setup-node@v4
with:
Expand Down
127 changes: 127 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
cmake_minimum_required(VERSION 3.16)
project(litegrip_cpp VERSION 0.1.0 LANGUAGES CXX)

# litegrip_cpp — ROS-agnostic C++ SDK for the LiteGrip adaptive two-finger gripper.
#
# Design constraints (see ../PLAN-litegrip-cpp.md):
# * C++17, zero third-party dependencies (stdlib + Linux SocketCAN only).
# * MUST NOT depend on ROS / ament / ros2_control — this library is the bottom
# layer that ros2_control links, and is usable from any plain C++ program.
# * Installable both into a ROS workspace (colcon, plain-cmake package.xml)
# and into a normal prefix (find_package + pkg-config).

if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(LITEGRIP_CPP_IS_TOP_LEVEL ON)
else()
set(LITEGRIP_CPP_IS_TOP_LEVEL OFF)
endif()

option(LITEGRIP_CPP_BUILD_TESTS "Build litegrip_cpp unit tests" ${LITEGRIP_CPP_IS_TOP_LEVEL})
option(LITEGRIP_CPP_BUILD_EXAMPLES "Build litegrip_cpp examples" ${LITEGRIP_CPP_IS_TOP_LEVEL})

# Must come before any $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> use:
# the variable is only defined once GNUInstallDirs is included, and an empty
# value silently drops the include directory from the exported target.
include(GNUInstallDirs)

find_package(Threads REQUIRED)

# ── library ───────────────────────────────────────────────────────────────
# T1 froze the API surface (headers). T2 adds the can/* layer; bus/LiteGrip
# land in T3 and safety/ControlLoop in T4.
add_library(litegrip_cpp
src/version.cpp
src/exceptions.cpp
src/constants.cpp
src/can/protocol.cpp
src/can/motor.cpp
src/can/transport.cpp
src/can/controller.cpp
src/json.cpp
src/calibration.cpp
src/hold_policy.cpp
src/bus.cpp
src/safety.cpp
src/gripper.cpp
src/control_loop.cpp
)
add_library(litegrip_cpp::litegrip_cpp ALIAS litegrip_cpp)

target_compile_features(litegrip_cpp PUBLIC cxx_std_17)
target_link_libraries(litegrip_cpp PUBLIC Threads::Threads)

target_include_directories(litegrip_cpp
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

set_target_properties(litegrip_cpp PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
POSITION_INDEPENDENT_CODE ON
)

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(litegrip_cpp PRIVATE -Wall -Wextra -Wpedantic)
endif()

# ── install ───────────────────────────────────────────────────────────────
include(CMakePackageConfigHelpers)

install(TARGETS litegrip_cpp
EXPORT litegrip_cppTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# Calibration / safety data (factory_calibration.json, safety_limits_*.json).
# These are *data*: a deployment may override them, but the factory defaults
# must ship so load_calibration()'s read-only fallback works out of the box.
# The path is mirrored by litegrip_cpp_DATA_DIR in the CMake package config.
install(DIRECTORY calibration/
DESTINATION ${CMAKE_INSTALL_DATADIR}/litegrip_cpp/calibration
)

install(EXPORT litegrip_cppTargets
FILE litegrip_cppTargets.cmake
NAMESPACE litegrip_cpp::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/litegrip_cpp
)

configure_package_config_file(
cmake/litegrip_cpp-config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/litegrip_cpp
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)

install(FILES
${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/litegrip_cpp
)

# ── pkg-config ────────────────────────────────────────────────────────────
configure_file(cmake/litegrip_cpp.pc.in
${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp.pc @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/litegrip_cpp.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)

# ── tests / examples (no third-party test framework: plain assert-based) ──
if(LITEGRIP_CPP_BUILD_TESTS)
enable_testing()
add_subdirectory(test)
endif()

if(LITEGRIP_CPP_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
157 changes: 133 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,143 @@
# litegrip-cpp
# litegrip_cpp

C++ SDK for the **LiteGrip lightweight robotic gripper series**.
ROS-agnostic **C++ SDK** for the LiteGrip adaptive two-finger gripper.

> **Status:** repository initialized. Source code, packaging and documentation
> have not landed yet.
**English** · [简体中文](README_zh.md)

## Scope
This is layer 1 of the litegrip stack — `litegrip_cpp` (SDK) → `litegrip_ros2_control`
(hardware interface) → `litegrip_moveit_config` (MoveIt). It speaks SocketCAN and
the Damiao DM4310 MIT protocol directly and depends on **nothing** but the C++17
standard library, `pthread`, and the Linux SocketCAN headers: no ROS, no ament,
no third-party libraries. Non-ROS implementations can link it as-is.

| | |
| --- | --- |
| Product | LiteGrip lightweight robotic gripper series |
| Repository role | C++ SDK |
| Status | Initializing — no source code yet |
> Status: **layer 1 is complete** — `can/*`, `GripperBus`, `LiteGrip`,
> `json`/calibration, `SafetyGuard` and `ControlLoop` are all implemented and
> tested.

## Related repositories
## What is **not** in this version

| Repository | Role |
| --- | --- |
| [litegrip-python](https://github.com/nexform-tech/litegrip-python) | Python SDK |
| [litegrip-docs](https://github.com/nexform-tech/litegrip-docs) | Product documentation |
| [litegrip-ros2](https://github.com/nexform-tech/litegrip-ros2) | ROS 2 driver |
| [litegrip-ros1](https://github.com/nexform-tech/litegrip-ros1) | ROS 1 driver |
Per the agreed v1 scope: `grasp()`, `set_force()`, `move_at_speed*()` and the
public zero-gravity mode. `close(force_n=...)` accepts the argument, ignores it
and says so, because applying a grip force needs torque feed-forward and
verified force calibration.

## Repository standards
## Safety wiring

This repository follows the shared NEXFORM ROBOTICS repository standards: the
agent operating rules in [AGENTS.md](AGENTS.md), Conventional Commits, and
automated semantic-release versioning on every merge to `main`.
The motion path (`goto_rad` / `move_to` / `open` / `close` / `home`) passes
through `SafetyGuard::guard_motion_frame`, and rejections are **raised, not
clamped**. Two paths deliberately bypass it, each documented at its definition:
`stop()` (an emergency stop must work from outside the red lines — it asserts
the zero-torque invariant instead) and the calibration routines (they drive to
the mechanical stops, which lie outside the red lines).

## License
## Testing

Copyright © 2026 NEXFORM ROBOTICS. Licensed under the
[Apache License 2.0](LICENSE).
```bash
ctest --test-dir build --output-on-failure
```

Two kinds of test:

- **Hand-lifted golden vectors** (`test_protocol.cpp`, `test_motor.cpp`) — the
cases from the Python suite, ported one for one, so a divergence in the port
shows up here rather than on hardware.
- **Generated parity harness** (`test_golden.cpp` +
`test/golden_generated.hpp`) — `test/generate_golden.py` drives the **real
Python SDK** and emits the exact bytes it produces (quantization, MIT packing,
status decoding, parameter frames); the C++ side must reproduce them. 1284
checks. Regenerate after changing the Python original:
`python3 test/generate_golden.py`.

`test_transport.cpp` is deliberately **non-transmitting** (a live gripper may be
on `can0`): it only reads an interface MTU, checks the missing-interface error
path, and opens/closes a socket. The send/receive path is not yet covered by a
test — it needs a vcan interface (root) or the real device.

`test_bus.cpp`, `test_gripper.cpp`, `test_safety.cpp` and
`test_control_loop.cpp` cover the behaviour that must hold **without** hardware:
lifecycle refusals while disconnected, the missing-interface error path, config
plumbing, the injectable hold policy, the calibration file round-trip, every
safety criterion — including the adversarial "must reject" cases, since each of
those is a case where accepting it would move hardware — and the control loop
itself via `dry_run`.

`dry_run` is not "do nothing": it runs the whole control path (rate limiting,
torque-budget allocation, the gate, the watchdogs) against a simulated plant and
only skips opening CAN and sending. That is what makes the loop, and every
deploy-config fail-closed rule, testable without a gripper.

What is **not** covered here, and needs the real device: connect, `init`/enable,
motion, calibration, and the transport send/receive path.

## Layers

| Layer | Type | Ported from |
|---|---|---|
| `can::CanTransport` | SocketCAN raw transport (CAN / CAN-FD, RX id filter) | `can/transport.py` |
| `can` protocol | pure codec: MIT frames, status frames, param frames | `can/protocol.py` |
| `can::MotorState` | per-motor decoded state | `can/motor.py` |
| `can::MotorController` | multi-motor dispatch on one bus | `can/controller.py` |
| `GripperBus` | single-gripper bus API (init = hold) | `protocols/can_bus.py` |
| `LiteGrip` | high-level API | `gripper.py` |
| `SafetyGuard` + `SafetyLimits` | red lines, torque budget, watchdog, modes | `safety_limits.py` (core) |
| `ControlLoop` | background 200 Hz streaming + rate limit + gate | old ROS-side daemon |

## Build

Zero-dependency, plain CMake:

```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
```

It is also buildable by `colcon` (declared as a plain-cmake package via
`package.xml`) so a ROS workspace can build it beside the ROS packages.

## Consume

```cmake
find_package(litegrip_cpp REQUIRED)
target_link_libraries(my_node PRIVATE litegrip_cpp::litegrip_cpp)
```

```bash
pkg-config --cflags --libs litegrip_cpp
```

## Non-ROS usage

```cpp
#include <litegrip/litegrip.hpp>

int main() {
litegrip::GripperConfig cfg; // can0, can_id 0x08, DM4310
auto gripper = litegrip::LiteGrip::connect_raii(cfg);
gripper.init(); // enable and hold current position
gripper.open();
gripper.goto_mm(40.0);
const auto state = gripper.get_state();
return state.is_stale() ? 1 : 0;
}
```

## Safety invariants

These are the safety argument and must not be relaxed:

1. **Tighten only** — limits may only be a sub-interval of the shipped baseline.
2. **Reject, do not clamp** — an out-of-range command is refused with a reason,
never silently rewritten and sent.
3. **Fail-closed** — limits unavailable ⇒ reject everything; a value that cannot
be bounded ⇒ do not move that way.
4. **Strict numeric boundary** — NaN / ±inf / non-numbers are rejected before any
comparison.

## Red lines are not yet unit-specific

The packaged safety baseline carries the reference unit's hand-push measurement.
They must be re-derived per gripper (caliper + closed-end re-zero) before
real-hardware motion, and `ControlLoopConfig::max_feedback_velocity_rad_s` must
be calibrated first — until it is, the loop refuses to send any motion frame
(deliberate fail-closed).
Loading
Loading