Compare commits
4
Commits
v0.1.1
..
69ba84e406
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69ba84e406 | ||
|
|
a5f05ddc6f | ||
|
|
1c7654e429 | ||
|
|
b7aa1dd3aa |
@@ -0,0 +1,22 @@
|
||||
name: markdownlint
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "**.md"
|
||||
- ".markdownlint.json"
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.md"
|
||||
- ".markdownlint.json"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
- run: npm install -g markdownlint-cli2
|
||||
- run: markdownlint-cli2 "**/*.md"
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"MD013": {
|
||||
"line_length": 120,
|
||||
"tables": false,
|
||||
"code_blocks": false
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dora-ulite6"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "Dora" }
|
||||
|
||||
Reference in New Issue
Block a user