3 Commits
Author SHA1 Message Date
cristhian aguileraandClaude Sonnet 4.6 a5f05ddc6f fix AGENTS.md line-length lint errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:52:13 -04:00
cristhian aguilera 1c7654e429 add markdownlint config and fix README lint errors
Add .markdownlint.json with 120-column line limit.
Fix MD060 table separator style to match padded header rows.
2026-04-02 14:19:05 -03:00
cristhian aguilera b7aa1dd3aa add TOOL_TYPE offset to tcp_pose for vacuum and gripper
Apply Z-axis tool offset when publishing tcp_pose so the output
reflects the actual tool tip. Adds TOOL_TYPE (none/vacuum/gripper)
and TOOL_OFFSET_MM env vars with standard Lite6 accessory presets.
Bump version to 0.1.2.
2026-04-02 14:07:28 -03:00
7 changed files with 69 additions and 19 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"MD013": {
"line_length": 120,
"tables": false,
"code_blocks": false
}
}
+15 -6
View File
@@ -4,7 +4,11 @@ This file provides guidance to AI coding assistants when working with code in th
## 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.
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
@@ -21,15 +25,18 @@ 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.).
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).
- `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.
- `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.
@@ -37,12 +44,14 @@ Robot behavior is configured through environment variables (see README.md for th
## 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()`.
- **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.
- **Issue tracker:** Gitea. Use the Gitea MCP tools (`mcp__gitea__*`) to read and manage issues,
pull requests, and other repository operations.
## Rules
+6 -4
View File
@@ -33,13 +33,15 @@ nodes:
## 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 |
| `TOOL_TYPE` | `none` | Mounted tool: `none`, `vacuum`, `gripper` |
| `TOOL_OFFSET_MM` | `0.0` | Additional Z-offset in mm added on top of the tool preset |
| `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) |
@@ -59,7 +61,7 @@ Access at `http://localhost:8080/`
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 |
@@ -74,7 +76,7 @@ Interactive docs at `http://localhost:8080/docs`
### 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 |
@@ -103,7 +105,7 @@ curl -X POST http://localhost:8080/api/move_to_pose \
- `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
- `tcp_pose` - `[x, y, z, roll, pitch, yaw]` in mm/deg — reflects tool tip when `TOOL_TYPE` is set
## Dora Inputs
+2
View File
@@ -15,3 +15,5 @@ nodes:
API_HOST: "0.0.0.0"
API_PORT: "9000"
VACUUM_ENABLED: "true"
TOOL_TYPE: "vacuum"
TOOL_OFFSET_MM: "0.0"
+37 -7
View File
@@ -67,6 +67,20 @@ class ULite6Helper:
"yaw": pos[5],
}
def get_tool_tcp_pose(self, offset_mm: float) -> Dict[str, float]:
pos = self.get_current_position()
if offset_mm == 0.0:
return pos
T = self.get_pose_matrix(tcp_offset_mm=offset_mm)
return {
"x": T[0, 3] * 1000.0,
"y": T[1, 3] * 1000.0,
"z": T[2, 3] * 1000.0,
"roll": pos["roll"],
"pitch": pos["pitch"],
"yaw": pos["yaw"],
}
def get_pose_matrix(self, tcp_offset_mm: float = 0.0) -> np.ndarray:
pos = self.get_current_position()
R = Rotation.from_euler(
@@ -154,7 +168,12 @@ class MoveToPoseRequest(BaseModel):
def create_api(
helper: ULite6Helper, default_speed: float, default_units: str, vacuum_enabled: bool
helper: ULite6Helper,
default_speed: float,
default_units: str,
vacuum_enabled: bool,
tool_type: str,
tool_offset_mm: float,
) -> FastAPI:
"""Create FastAPI application with robot control endpoints."""
app = FastAPI(
@@ -348,7 +367,7 @@ def create_api(
</div>
<div class="card">
<h2>TCP Pose (mm, deg)</h2>
<h2 id="tcp-pose-title">TCP Pose (mm, deg)</h2>
<div class="pose-grid">
<div class="pose-item"><div class="label">X</div><div class="value" id="pose-x">--</div></div>
<div class="pose-item"><div class="label">Y</div><div class="value" id="pose-y">--</div></div>
@@ -528,6 +547,9 @@ def create_api(
updateVacuumStatus();
setInterval(updateVacuumStatus, 1000);
}
if (config.tool_type && config.tool_type !== 'none') {
$('tcp-pose-title').textContent = 'TCP Pose — ' + config.tool_type + ' (mm, deg)';
}
}
async function updateVacuumStatus() {
@@ -586,9 +608,9 @@ def create_api(
@app.get("/api/pose")
def get_pose():
"""Get current TCP pose [x, y, z, roll, pitch, yaw]."""
"""Get current TCP pose [x, y, z, roll, pitch, yaw] with tool offset applied."""
try:
return helper.get_current_position()
return helper.get_tool_tcp_pose(tool_offset_mm)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -698,7 +720,11 @@ def create_api(
@app.get("/api/config")
def get_config():
"""Get API configuration."""
return {"vacuum_enabled": vacuum_enabled}
return {
"vacuum_enabled": vacuum_enabled,
"tool_type": tool_type,
"tool_offset_mm": tool_offset_mm,
}
return app
@@ -801,6 +827,10 @@ def main() -> None:
api_port = int(os.getenv("API_PORT", "8080"))
vacuum_enabled = os.getenv("VACUUM_ENABLED", "false").lower() in ("true", "1", "yes")
_TOOL_OFFSET_PRESETS: Dict[str, float] = {"none": 0.0, "vacuum": 61.0, "gripper": 65.0}
tool_type = os.getenv("TOOL_TYPE", "none").lower()
tool_offset_mm = _TOOL_OFFSET_PRESETS.get(tool_type, 0.0) + float(os.getenv("TOOL_OFFSET_MM", "0.0"))
# Initial position settings
init_mode = os.getenv("INIT_MODE", "none").lower() # "home", "pose", or "none"
init_x = float(os.getenv("INIT_X", "300.0"))
@@ -839,7 +869,7 @@ def main() -> None:
_log("Skipping initial position (INIT_MODE=none)")
# Create and start FastAPI server in background thread
app = create_api(helper, default_speed, default_units, vacuum_enabled)
app = create_api(helper, default_speed, default_units, vacuum_enabled, tool_type, tool_offset_mm)
api_thread = threading.Thread(
target=run_uvicorn, args=(app, api_host, api_port), daemon=True
)
@@ -854,7 +884,7 @@ def main() -> None:
if event["id"] == "tick":
try:
joints = helper.get_joint_angles_deg()
tcp_pose = helper.get_current_position()
tcp_pose = helper.get_tool_tcp_pose(tool_offset_mm)
_send_joint_pose(node, joints)
_send_tcp_pose(node, tcp_pose)
except Exception as exc:
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "dora-ulite6"
version = "0.1.1"
version = "0.1.2"
license = "MIT"
authors = [
{ name = "Dora" }
Generated
+1 -1
View File
@@ -78,7 +78,7 @@ wheels = [
[[package]]
name = "dora-ulite6"
version = "0.1.0"
version = "0.1.1"
source = { virtual = "." }
dependencies = [
{ name = "dora-rs" },