add dora_ulite6 standalone node with command lifecycle status

Migrates the ulite6 Python dora node from dora_littlehand into a
self-contained repository following the dora-zed structure. Adds
command lifecycle state ("started"/"done"/"failed") and type
("command"/"error") to the status output so coordinator nodes can
reliably sequence robot actions without polling.
This commit is contained in:
cristhian aguilera
2026-04-01 08:51:19 -03:00
parent 19ef9c074f
commit 8e3d976a17
9 changed files with 1269 additions and 1 deletions
+13
View File
@@ -0,0 +1,13 @@
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
*.egg
.env
.venv
.pytest_cache/
.coverage
.ruff_cache/
out/
+50
View File
@@ -0,0 +1,50 @@
# AGENTS.md
This file provides guidance to AI coding assistants when working with code in this repository.
## What this is
A dora-rs Python node that interfaces with a UFactory Lite6 robot arm. On each tick it reads joint angles and TCP pose from the robot and publishes them as dora outputs. It also accepts JSON commands (home, move_to, move_to_pose, vacuum_on/off) via a `command` input. A FastAPI REST server with a web UI runs in a background thread for manual control. The robot control wrapper lives in `dora_ulite6/main.py` as the `ULite6Helper` class, and the dora event loop is the `main()` function in the same file.
## Build
```bash
uv pip install -e .
```
Requirements: Python >=3.8, uv, and network access to the UFactory Lite6 robot.
## Run
```bash
dora up
dora start dataflow.yml
```
Robot behavior is configured through environment variables (see README.md for the full list: ROBOT_IP, DEFAULT_SPEED, DEFAULT_UNITS, API_HOST, API_PORT, VACUUM_ENABLED, INIT_MODE, etc.).
## Architecture
- **`dora_ulite6/main.py`** -- Single-file implementation containing:
- `ULite6Helper`: Wraps the xArm Python SDK for motion control (go_home, move_to_pose, get_current_position, get_joint_angles_deg, get_pose_matrix, vacuum gripper, status, reset).
- `MoveToRequest` / `MoveToPoseRequest`: Pydantic models for API validation.
- `create_api()`: FastAPI app factory with REST endpoints and embedded HTML/JS web UI for manual control.
- `main()`: Dora event loop. Starts FastAPI in a daemon thread, then processes `tick` (publishes joint_pose and tcp_pose) and `command` (executes robot actions and publishes status) inputs.
- **`dora_ulite6/__init__.py`** -- Package marker.
- **`pyproject.toml`** -- Package metadata, dependencies, and entry point.
- **`dataflow.yml`** -- Example dora dataflow using the git: source format.
## Key APIs
- **dora Python API** (`dora`): `Node()`, `node.send_output()`, event iteration via `for event in node`.
- **xArm SDK** (`xarm.wrapper.XArmAPI`): `connect()`, `motion_enable()`, `set_mode()`, `set_state()`, `set_position()`, `move_gohome()`, `get_position()`, `get_servo_angle()`, `set_vacuum_gripper()`.
- Output data is sent as PyArrow arrays with metadata dicts.
## Tools
- **Issue tracker:** Gitea. Use the Gitea MCP tools (`mcp__gitea__*`) to read and manage issues, pull requests, and other repository operations.
## Rules
- Do not mention AI tools in commit messages or documentation.
- This controls a real robot. Test changes carefully and preserve safety checks.
+3
View File
@@ -0,0 +1,3 @@
# CLAUDE.md
See [AGENTS.md](AGENTS.md) for repository guidance.
+114 -1
View File
@@ -1,2 +1,115 @@
# dora_ulite6 # Dora ULite6 Node
Dora node for controlling a UFactory Lite6 robot via REST API, web UI, and publishing joint/TCP state.
## Installation
```bash
uv pip install -e .
```
## Dataflow
```yaml
nodes:
- id: ulite6
git: ssh://git@gitea.liria.cl:5001/liria/dora_ulite6.git
git_checkout: v0.1.0
build: uv pip install -e .
path: dora_ulite6/main.py
inputs:
tick: dora/timer/millis/10
outputs: [status, joint_pose, tcp_pose]
inputs: [command]
env:
ROBOT_IP: "192.168.1.192"
DEFAULT_SPEED: "30"
DEFAULT_UNITS: "mm"
API_HOST: "0.0.0.0"
API_PORT: "8080"
VACUUM_ENABLED: "false"
```
## Configuration
| Env Variable | Default | Description |
|--------------|---------|-------------|
| `ROBOT_IP` | `192.168.1.192` | Robot IP address |
| `DEFAULT_SPEED` | `30` | Movement speed (mm/s) |
| `DEFAULT_UNITS` | `mm` | Position units (mm or m) |
| `API_HOST` | `0.0.0.0` | API server host |
| `API_PORT` | `8080` | API server port |
| `VACUUM_ENABLED` | `false` | Enable vacuum gripper controls |
| `INIT_MODE` | `none` | Startup positioning: `home`, `pose`, or `none` |
| `INIT_X/Y/Z` | `300/0/250` | Initial pose coordinates (mm) |
| `INIT_ROLL/PITCH/YAW` | `180/0/0` | Initial pose orientation (deg) |
| `INIT_SPEED` | `50` | Startup movement speed (mm/s) |
## Web UI
Access at `http://localhost:8080/`
- Live status, TCP pose, and joint angles
- Home and Reset buttons
- Move to position form
- Vacuum gripper controls (when enabled)
## REST API
Interactive docs at `http://localhost:8080/docs`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/` | Web control interface |
| GET | `/api/status` | Robot status (connected, errors) |
| GET | `/api/pose` | Current TCP pose |
| GET | `/api/joints` | Current joint angles |
| GET | `/api/config` | API configuration |
| POST | `/api/home` | Go to home position |
| POST | `/api/reset` | Clear errors and reset state |
| POST | `/api/move_to` | Move to position |
| POST | `/api/move_to_pose` | Move to full pose |
| POST | `/api/disconnect` | Disconnect from robot |
### Vacuum Gripper (when `VACUUM_ENABLED=true`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/vacuum` | Get vacuum status |
| POST | `/api/vacuum/on` | Turn vacuum on |
| POST | `/api/vacuum/off` | Turn vacuum off |
## CLI Examples
```bash
# Get pose
curl http://localhost:8080/api/pose
# Go home
curl -X POST http://localhost:8080/api/home
# Move to position
curl -X POST http://localhost:8080/api/move_to \
-H "Content-Type: application/json" \
-d '{"x": 200, "y": 0, "z": 300}'
# Move with orientation
curl -X POST http://localhost:8080/api/move_to_pose \
-H "Content-Type: application/json" \
-d '{"x": 200, "y": 0, "z": 300, "roll": 180, "pitch": 0, "yaw": 0}'
```
## Dora Outputs
- `status` - JSON: `{ok, action, message, timestamp_ns}`
- `joint_pose` - 6 joint angles in degrees
- `tcp_pose` - `[x, y, z, roll, pitch, yaw]` in mm/deg
## Dora Inputs
- `command` - JSON string: `{id, action, payload}`
- `id`: unique command id (required)
- `action`: `home`, `move_to`, or `move_to_pose`
- `payload`: fields for move (`x`, `y`, `z`, `roll`, `pitch`, `yaw`, `speed`)
`status` will include `command_id` for command responses.
+19
View File
@@ -0,0 +1,19 @@
nodes:
- id: ulite6
git: ssh://git@gitea.liria.cl:5001/liria/dora_ulite6.git
git_checkout: v0.1.0
build: uv pip install -e .
path: dora_ulite6/main.py
inputs:
tick: dora/timer/millis/10
outputs:
- status
- joint_pose
- tcp_pose
env:
ROBOT_IP: "192.168.1.192"
DEFAULT_SPEED: "30"
DEFAULT_UNITS: "mm"
API_HOST: "0.0.0.0"
API_PORT: "9000"
VACUUM_ENABLED: "true"
+1
View File
@@ -0,0 +1 @@
"""Dora ULite6 node package."""
+1020
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
[project]
name = "dora-ulite6"
version = "0.1.0"
license = "MIT"
authors = [
{ name = "Dora" }
]
description = "Dora node for controlling the UFactory Lite6 robot"
requires-python = ">=3.8"
dependencies = [
"dora-rs >= 0.3.9",
"numpy < 2.0.0",
"scipy >= 1.14.0",
"xarm-python-sdk >= 1.17.3",
"fastapi >= 0.109.0",
"uvicorn >= 0.27.0",
]
[dependency-groups]
dev = ["pytest >=8.1.1", "ruff >=0.9.1"]
[project.scripts]
dora-ulite6 = "dora_ulite6.main:main"
[tool.setuptools]
packages = ["dora_ulite6"]
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Install dependencies for dora-ulite6.
# Requires: Python >=3.8
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="${SCRIPT_DIR}/.."
# Install uv if not present
if ! command -v uv &>/dev/null; then
echo ">>> Installing uv..."
curl -LsSf https://astral.sh/uv/install.sh | sh
fi
# Create venv and install
echo ">>> Installing dora-ulite6 in editable mode..."
cd "$REPO_DIR"
uv venv --seed --allow-existing
uv pip install -e ".[dev]"
echo ">>> Done. Activate with: source .venv/bin/activate"