Metadata-Version: 2.4
Name: lekit-robot
Version: 0.1.0
Summary: Small Python SDK for observing and controlling Lekit Robot Node
Project-URL: Repository, https://github.com/sorelferris/lekit
Project-URL: Issues, https://github.com/sorelferris/lekit/issues
Author: Sorel Ferris
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: eclipse-zenoh<2,>=1.9
Requires-Dist: numpy<3,>=1.26
Requires-Dist: pillow<13,>=10
Requires-Dist: pydantic<3,>=2
Description-Content-Type: text/markdown

# lekit-robot

Small Python SDK for a running **Lekit Robot Node**. Discover a robot by name,
read joint/pose feedback and RGB/depth images, and send absolute motion targets.
The client does not install LeRobot, Torch, OpenCV, robot drivers, or camera drivers.
Python 3.12+ is required.

## Install

After publication:

```sh
pip install lekit-robot
```

From this repository before publication:

```sh
pip install ./packages/lekit_robot
```

Robot Node must already be running on the robot computer. Clients need network
access to its Zenoh peer; the browser's HTTP port 8080 is not the SDK endpoint.
LAN multicast discovery is the default. If discovery is blocked, configure both
Node (`lekit-robot --zenoh-router tcp/ROUTER_IP:7447`) and client
(`connect(name="NERO Dual", router="tcp/ROUTER_IP:7447")`) to use an existing Zenoh router.
This package does not install the Robot Node command-line server.

## Observe (does not acquire control)

```python
from lekit_robot import connect

with connect(name="NERO Dual", timeout=5) as robot:
    obs = robot.get_observation(images=True, timeout=5)
    print(obs.robot.status, obs.robot.state)
    camera = obs.camera(name="head_view")
    if camera.color is not None:
        print(camera.color.shape)  # H x W x 3, RGB uint8
    print(robot.descriptor.joint_fields)
```

`images=False` omits images. `robot.observations(images=False, timeout=5)` yields
latest-only snapshots. Camera samples have independent capture times; an observation
is not a hardware-synchronized frame set. Check sample status and capture time.
Depth is a uint16 array in the server's camera units, not automatically metres.

## Control

Control requires enabled motors, `allow_control=True`, fresh feedback, normal mode,
and no emergency stop. Close Teleop before acquiring external control. The context
manager holds and renews an exclusive control lease and releases it on exit.
Multiple clients may observe; only one controller may own the lease.

The following function sends a real motion command **when called**:

```python
from lekit_robot import connect

def move_left_joint3(delta_rad):
    with connect(name="NERO Dual") as robot:
        with robot.control():
            obs = robot.get_observation(images=False)
            state = obs.robot.state
            if obs.robot.status != "connected" or not state or not state.get("left.feedback_fresh"):
                raise RuntimeError("Fresh left arm feedback is required")
            joints = [state[f"left.joint_{i}"] for i in range(1, 8)]
            joints[2] += delta_rad
            return robot.set_joints(joints, side="left")
```

| Method | Target |
| --- | --- |
| `set_joints(values, side=None, timeout=None)` | Absolute joint values in advertised order, or a mapping with full field names |
| `set_target(values, side=None, mode="point", timeout=None)` | Absolute flange/EEF pose in advertised order, or a full-key mapping |
| `set_target(x=..., y=..., z=..., roll=..., pitch=..., yaw=..., side="left")` | One arm's complete pose |
| `set_gripper(width, side="left", timeout=None)` | One gripper's absolute opening |
| `set_gripper(values, timeout=None)` | All grippers, in advertised order or full-key mapping |

For NERO Dual, `left` is can1 and `right` is can0. Joint/rotation units are radians;
position and gripper width units are metres. Pose fields represent the flange in
that arm's base frame. For other robots use the plugin-declared field semantics.
`side` selects only that arm; the other arm receives no target. Without `side`,
provide the complete advertised target (14 joint values or 12 pose values for NERO).
A scalar gripper width requires `side` on a dual-arm robot. There is no implicit
broadcast of one pose or gripper value to both arms.

NERO uses firmware-planned point targets. `stream` replaces targets through the
same backend; it does not guarantee a hard realtime servo or a straight path.
Unsupported modes, partial targets and out-of-range values are rejected.

A receipt acknowledges command submission, **not arrival or successful IK**.
Continue observing measured state and alarms. `ActionOutcomeUnknownError` means a
command may already have executed: do not retry it automatically. A control lease
is ownership arbitration, not a hardware emergency stop or a stop-on-disconnect guarantee.
Use the Robot Node emergency stop or physical stop as appropriate.

## Compatibility

The wire protocol remains `lekit/v1`. New clients opt into the gripper manifest
extension; old clients continue to receive the original manifest shape. An old
Node can still supply observations and existing targets, but gripper commands need
an updated Node; unsupported grippers raise `UnsupportedCommandError` locally.
Per-arm commands also require the updated Node validation. Update client and Node
together for complete NERO Dual support. Reconnect after replacing the Node or its
robot configuration; the SDK never silently rebinds an existing control session.

Existing applications may keep `from lekit.robot import connect` when the main
project is installed: those imports re-export this package's implementation.

## Build and release

From `packages/lekit_robot`:

```sh
python -m pip install build twine
python -m build
python -m twine check dist/*
python -m twine upload --repository testpypi dist/*
# After testing the installed wheel in a clean environment:
python -m twine upload dist/*
```

Publishing needs a PyPI account and an API token or configured Trusted Publisher.
Never store credentials in source files. Version 0.1.0 is the initial release;
subsequent releases must use a new version number.

License: Apache-2.0, matching the repository's existing standalone package convention.

A build/test and manual publishing workflow is provided at
`.github/workflows/lekit-robot-release.yml`. It installs the built wheel in an
isolated environment before testing. Pushes and pull requests only build/test.
After the workflow is committed to the default branch, configure a PyPI pending
Trusted Publisher for a new project:

- Project: `lekit-robot`
- Owner: `sorelferris`
- Repository: `lekit`
- Workflow: `lekit-robot-release.yml`
- Environment: `pypi` (or `testpypi` on TestPyPI)

Then run the workflow manually, selecting `testpypi` first, and `pypi` for the
release. Configure the two registries separately. This avoids storing a long-lived
PyPI token. See https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/.
