Skip to content

About

Python code to drive and simlulate the movement of xArm5 to manipulate objects

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

Robot Motion

Installs as robot-motion with the API and the shared web UI (src/web) included by default. Optional extras are [xarm], [ur], and [mg400] (reserved; MG400 is not implemented yet). Run robot-motion serve for the UR service, or robot-motion drivers for an offline capability inventory. UR model profiles are ur3e, ur5e and ur5_cb3. The UR service serves the shared panel and maps RTDE telemetry into the status keys that panel reads. Physical UR control (arm motion, the Robotiq gripper, manual mode and force zeroing) is config-gated, identity-checked and hard-claimed; without the local control config the service has no /control routes (see the packaged ARM_MOTION.md and GRIPPER_CONTROL.md). Machine configuration, control limits and calibrated graphs belong in *.local.json.

The existing xArm5 application remains available through pyxarm and the explicit robot-motion legacy-xarm command after installing [xarm]. Its controller, API, hard claims, UI, graph interlocks and settings are unchanged.

Branches and deployments

main carries the unified UI and API. Each deployed robot runs its own branch, and main reaches a robot only when it is merged into that branch with someone at the robot.

Branch Robot Checkout and service
debug-xarm5 xArm5 on sdl2-pc-03-cytation C:\Users\sdl2\Projects\xarm-translocation, NSSM xarm
debug-ur5e ligand_ur5e on sdl2-pc-05 C:\Users\sdl2\Projects\robot-motion, NSSM robot-motion-prototype
debug-ur3e ur3e_gibbie on sdl2-pc-04 (observer since 2026-10-12, src/docs/UR3E_GIBBIE_PLAN.md) C:\Users\sdl2\Projects\robot-motion, NSSM robot-motion-ur3e
debug-ur5cb3 lle_ur5_arm (UR5 CB3, Process Chemistry) on sdl2-pc-00-lle (since 2026-10-13, src/docs/UR5_CB3_LLE_PLAN.md) C:\Users\sdl2\Projects\robot-motion, NSSM robot-motion-ur5cb3

xarm-python-sdk is in the [xarm] extra. A service started with uv run re-syncs the environment on every start and removes extras it was not asked for, so when debug-xarm5 first takes this layout, the xarm service's AppParameters must change from run pyxarm web to run --extra xarm pyxarm web. The UR service runs the checkout's Python directly and does not re-sync.

See the packaged operator guide. This prototype implements the STATUS_SPEC v1.2 read-only profile; the legacy xArm application's conformance and behavior are described below.

Existing xArm application (compatibility)

UFACTORY xArm control for the AC Organic Self-Driving Lab — arm, gripper, linear track, and force-torque sensor, driven through one Python controller, a web UI, and a STATUS_SPEC v1.2 REST API. The Docker simulator is the supported path for off-hardware testing. Installs as the pyxarm package.

xArm Web Control Interface

Lab Status Spec Conformance

This repo conforms to lab status spec v1.2. Wire-contract types come from the shared sdl-lab-contract package (pinned to v1.2.0) rather than a vendored models.py. The FastAPI service exposes the standard read surface so the AC Organic Self-Driving Lab dashboard can poll it like any other device:

  • GET / -- ProbeResponse (equipment_id, equipment_name, protocol_version)
  • GET /health -- HealthResponse(status="healthy")
  • GET /status -- EquipmentStatus envelope (snake_case, side-effect-free)
  • GET /positions -- read-only snapshot of joint angles, Cartesian pose, track position, and gripper position (no movement)
  • GET /kinematics/config, GET /kinematics/limits -- read-only controller configuration, DH parameters, and reduced limits with explicit availability/provenance
  • POST /kinematics/fk, POST /kinematics/ik -- controller calculations without motion; IK supports reference angles on firmware >=2.7.103 (SDK >=1.18.4)
  • GET /openapi.json -- generated by FastAPI

The v1.1 claim surface (POST /control/claim, /control/heartbeat, /control/release) is implemented with hard enforcement: every mutating endpoint requires the active claim's X-Claim-Token or returns HTTP 423 — including when no claim is held at all. /move/stop, /clear/errors, /connect, and /disconnect stay ungated as the safety floor. Set XARM_ENFORCE_CLAIMS falsy to drop back to advisory mode for demos.

The browser UI lives at /web/. The xArm-specific control endpoints (/move/*, /gripper/*, /track/*, /force-torque/*, /connect, /disconnect) are unchanged; programming the arm continues to happen via these or the XArmController Python SDK. The SDK in ac-organic-lab keeps do_not_call_connect: true for this device — a robot arm is never auto-connected.

Primary operation (v1.2 activity)

v1.2 splits "is this arm healthy" (equipment_status) from "is it working right now" (activity). For this device the primary operation is a commanded arm or linear-track motion: activity is running from the moment a motion primitive dispatches its SDK call until that call returns, and idle otherwise. It is observed from the controller's motion bookkeeping, never derived from equipment_status.

Consequences worth knowing as a reader:

  • Gripper actuation is not primary operation. Grip / release happens while parked at a graph node with the arm stationary, and takes well under a second. It shows in components.gripper and details.gripper, not on the activity axis.
  • activity_since is the transition instant, latched when the motion flag flips — not the time of the poll that observed it. Two polls of the same in-flight move report the same value, so elapsed move time is recoverable.
  • degraded + running is expressible and does occur. A connected-but-not-fully-alive controller mid-move reports both; neither fact suppresses the other. Consequently busy and degraded never co-occur (spec §2.3).
  • Polling cannot see every move. The dashboard polls at 60 s while a graph hop can take a few seconds, so a sampled activity series is not usage accounting (spec §2.3.1). This device does not yet publish metrics["cycles_total"]; the state_transition rows the events exporter pushes to POST /api/ingest/events are the timing-accurate record.

One motion at a time

Only one motion may be in flight. A move arriving while another is running is refused with HTTP 409 and a body distinguishable by shape:

{"detail": {"error": "motion_in_progress",
            "message": "A motion is already in flight. Wait for it to finish, or POST /move/stop to abort it."}}

This is 409 rather than 412 because it is a device-state conflict, not a precondition the caller could have satisfied in advance (spec §6.1). allowed_actions mirrors it: while activity is running, every move.<node_id> target and every gripper.<state> target is withheld and only stop remains, so a client that reads /status and immediately POSTs a listed action is never refused for being busy (§6.2). (The gripper is withheld for a different reason than the motion slot below — the stroke is invariant during arm motion, so set_gripper_state requires a parked arm.)

Covered: /move/{position,joints,relative,location,home,plate_linear}, /track/move, /track/move/location, /control/graph/{move_to,travel_to}, /assistant/execute, and both /force-torque/move-* endpoints. A composite move holds the slot for its whole duration — a cross-rail graph edge is two sub-moves, a travel is N hops, and an assistant run is a whole step list, none of which release between parts.

Not covered, deliberately: /move/stop and /clear/errors (the safety floor must always be reachable), /control/graph/recover_to (a bookkeeping re-pin, not a motion), and the gripper endpoints (set_gripper_state already refuses while the arm is moving, and gripper actuation is not primary operation).

Operator workspace

/xarm5/web/ (or /web/ directly on the device) is the shared control interface. The Camera tile switches between Tapo Camera and Stereo Camera. The workspace below switches between Graph Drive, Direct Drive, and Edit Graph; gripper/rail controls sit above the drive controls, with the console alongside. Graph Drive defaults to Reachable Node; Travel to Node has its own tab. The legacy graph.html link redirects to #edit-graph in this workspace.

All tabs use one control claim, heartbeat, and status connection. Switching views never acquires or releases control. The graph editor uses the main panel's current claim token, and graph viewing remains available during motion or while another operator holds control. The manual-mode switch retains its existing server-side motion guards.

Manual motion defaults are 20 mm/s for rail and linear arm motion, and 5 degrees/s for joints. Empty speed fields use the same defaults. These are defaults, not hard caps; graph moves retain their edge limits. Backend profile defaults are loaded when the controller connects.

UI composition and responsive styles live in workspace.js / workspace.css; main.js owns robot controls and the shared claim, graph.js owns topology editing, and the existing camera modules own their players.

Motion-graph enforcement, and the bounded way to leave it

Explicit graph moves (/control/graph/move_to, /control/graph/travel_to, and assistant move steps) always enforce the edge's movement mode and speed cap, including during an OFF or ADVISORY override. Linear edge speeds are TCP translation speeds in mm/s; joint edge speeds are degrees/s. They are not percentages or interchangeable units. Cross-rail graph moves use the edge speed for the arm and the track configuration's mm/s speed for the rail. Linear acceleration is mm/s², joint acceleration degrees/s².

Saving an edge in the graph editor updates the server's in-memory graph; the next move uses the saved settings. It does not retime a move already sent to the robot. Direct YAML edits require a server restart. GET /graph on the same server as the UI reports the loaded values; a different checkout's YAML may differ. Reload an already-open graph tab to fetch other clients' edits.

Administrator one-click OFF: POST /control/admin/graph/off with an authenticated administrator session/API key; no body or claim is required. Optional reason is recorded for audit. OFF remains active for all users until POST /control/admin/graph/restore, including across claim changes, disconnects and service restarts. Freehand still requires the caller's motion claim and passes the existing device safety checks. Ordinary mode switches cannot replace this admin setting. Authentication is mandatory even when the general login gate is disabled.

This state is stored separately from graph topology in ignored src/settings/graph_admin_override_*.json files, scoped to device/profile. Set XARM_GRAPH_ADMIN_STATE_DIR to place them in a persistent service-writable directory. Preserve these files during deployment. The status override reports scope: admin, the owner and reason, and null expiry; the device panel shows "OFF until an administrator restores it". The timed workflow below remains available when the administrator switch is inactive.

Without a saved administrator OFF setting, the motion graph boots STRICT whenever motion_graph.yaml loads: moves must follow whitelisted edges, and the graph-bypassing /control/freehand/* family (raw Cartesian, raw joints, jog, velocity streaming, raw rail, freehand gripper) is refused outright with 409 graph_mode_strict.

Real Cartesian work — camera surveys, teaching a pose, calibration — needs that family, which means lowering the mode. Lowering is administrator-only (since 2026-09-23): an administrator turns enforcement OFF with POST /control/admin/graph/off and back ON with POST /control/admin/graph/restore, or with the Administrator button on the /web/ Motion Graph card. That OFF has no expiry and needs no claim; it persists across claims, disconnects and restarts until restored. Claim holders, including agents, cannot lower it: POST /control/graph/mode below strict and POST /control/graph/off return 403 admin_required. Setting strict stays open to any claim holder.

While an administrator has enforcement OFF:

  1. Acquire a claim with POST /control/claim and use its claim_token as X-Claim-Token on control requests.
  2. Read /status. When idle and available, allowed_actions includes freehand.position, freehand.relative, and freehand.joints. These map to POST /control/freehand/position, /control/freehand/relative, and /control/freehand/joints. Use Content-Type: application/json.
  3. Release the claim when finished. Enforcement comes back only when an administrator restores it.

(The node-anchored POST /control/freehand/nudge, once the one freehand route allowed in STRICT, was removed on 2026-09-23.)

Absolute Cartesian requests take {x,y,z,roll?,pitch?,yaw?,speed?}; relative requests take {dx?,dy?,dz?,droll?,dpitch?,dyaw?,speed?}. The agent's tool catalog must register the matching freehand.* names. Claims, motion concurrency, and configured controller/interlock checks still apply. A raw move clears the named pose pin; graph motion may require recovery to a known node afterward. A move response means the command was accepted; poll status to follow completion.

Fume hood sash interlock (cross-device precondition) — CURRENTLY DISABLED

Disabled 2026-09-21 (enabled: false in src/settings/interlocks.yaml), and the hood tag has been removed from the seven hood graph nodes. The arm will now drive into the hood against a closed sash and nothing in this repo will stop it, on entry or mid-move — the sash is an operator responsibility until the replacement lands. The reason is the unsettled question at the end of this section: it was never confirmed whether the fume hood device's metrics.sash_position is a true readback or the last commanded preset, so what the guard enforced was not known to be a collision guard, while it did reliably refuse bench work. The replacement is an explicit xyz safe/danger volume owned by this device, which needs no second device to be reachable and no assumption about another firmware's fields.

Re-enabling means restoring both the master switch and tags: [hood, …] on the hood_* nodes (see the note in motion_graph.yaml). The gated_rails: [Hood] clause still covers all seven on its own, so a half-restore over-blocks rather than silently under-blocking. The rest of this section describes the behaviour as designed, and is accurate again the moment the switch goes back.

The arm reaches into the fume hood (7 hood-tagged graph nodes) and over the Opentrons deck (8 opentrons-tagged nodes). The sash that closes over that space belongs to a different device — fume_hood_actuator — so this service reads that device's /status and refuses motion the sash could collide with. Configuration and the full rationale live in src/settings/interlocks.yaml; the implementation is src/core/sash_interlock.py.

Two behaviours, and the second is why this is more than a precondition check:

  1. Entry is refused unless the sash reports its required preset. A move whose target is a gated node returns HTTP 412 (spec §6.1 — the request is well-formed and this arm is healthy; an external precondition is unmet):
{"detail": {"error": "interlock_not_satisfied", "interlock": "fume_hood_sash",
            "action": "graph.move_to", "required": "sash_position=5", "observed": 3,
            "state": "blocked", "retry_after_s": null,
            "hint": "Move the sash to preset 5 (POST /control/sash/move on fume_hood_actuator), then retry. Or POST /control/interlocks/sash/override.",
            "message": "'graph.move_to' to 'hood_shaker_low' is refused: fume hood sash is at position 3, required 5"}}
  1. The arm is stopped if the sash leaves that preset while the arm is already inside the region — a watchdog thread calls the same stop_motion() that backs /control/stop. After that, all motion is refused, egress included: there is deliberately no automatic retreat, because a blind retreat could drag the arm through a descending sash, and the interlock cannot see where the sash is along that path.

retry_after_s is null because recovery is operator-driven. allowed_actions mirrors the refusal (§6.2): gated move.<node_id> targets are withheld while the sash is not parked, and every target is withheld while the arm is stuck inside. A claim conflict still surfaces first — 423 ahead of the 412.

The override is the only way out, so it is a first-class control rather than a debug flag: POST /control/interlocks/sash/override {reason, ttl_seconds} (claim- and login-gated, capped, re-issuable, audited), surfaced as a button in the /web/ banner. GET /interlocks/sash (+?refresh=true) reads the current state without commanding a move, and answers before /connect.

It fails open. If the fume hood device cannot be reached, moves are allowed with a loud warning — a deliberate choice not to halt arm work on another device's uptime. Because that means the guard protects nothing during an outage, every bypass is logged at WARNING, counted in details.interlocks.fume_hood_sash.moves_allowed_while_blind, emitted as an interlock_bypass event, and shown as an amber SASH INTERLOCK BLIND banner with a [SASH-BLIND] prefix on message. Two carve-outs keep faith with that choice while closing what it did not cover: a device never reached since startup is misconfiguration, not an outage, and fails closed; so does one that answers with an unparseable body.

equipment_status deliberately does not go degraded when the interlock is blind: §2.2 scopes degraded to an unhealthy subsystem of this device, and a neighbouring Pi being unreachable is not this arm's ill health — under fail-open its capability is not reduced, only its supervision. details.interlocks plus the message prefix carry that instead.

Two limits worth knowing:

  • Freehand moves cannot be gated on entry. /move/{position,joints,relative}, /velocity/cartesian and raw /track/move carry no node id, so there is no way to know where they end. They are refused while the arm is inside the region, and in STRICT mode strict_graph_guard already refuses them outright. Velocity streaming continues after its request returns, so a pre-flight check there is only a snapshot — the watchdog is the backstop.
  • Gripper actions are not gated, on purpose. The hazard is the arm envelope against the sash glass; a jaw stroke does not extend that envelope. Gating them would add a way to trap a plate for no safety gain, and setting a plate down is the one useful thing left to an arm that cannot move.

Unverified assumption, and the reason the check is composite. It is not yet confirmed whether the fume hood device's metrics.sash_position is a true readback or the last commanded preset. If it is the latter, position == 5 alone would mean "someone last asked for 5" — satisfied even with the sash pulled down by hand. So "satisfied" additionally requires components.sash.state, components.sash.connected, components.actuator.state == "idle" (a last-commanded metric reports the target for the whole trip, so an idle actuator is what rules out a sash in motion) and the device's own equipment_status == "ready". Settle it at the bench — command the sash to 5, move it by hand, re-read /status — before treating this as a hard collision guard.

Simulation self-identification

When the service is connected via the docker profile (the UFACTORY Docker simulator), it says so on every surface: healthy states report equipment_status: "dry_run" instead of ready/busy (with activity still observed — dry_run permits any activity per spec §2.3), every message carries a [SIMULATION] prefix, details.simulated: true is set, and the web panel shows a persistent amber banner. Fault states keep their honest values so recovery paths can be exercised against the sim. The events exporter is suppressed while simulated, so sim sessions never write into the lab's history DB as the real device. Deliberate consequence: anything gating on equipment_status == "ready" will not run against a simulator by accident.

The contract is defined in ac-organic-lab/docs/STATUS_SPEC.md.

✨ Key Features

  • Multi-Model Support: xArm5, xArm6, xArm7, and xArm850 with auto-detection
  • Unified Control: Single controller for arm, gripper, linear track, and force torque sensor
  • Docker Simulator Support: Drive the official UFACTORY Docker simulator through the same controller path used for hardware
  • REST API: FastAPI server for web-based control and monitoring
  • Flexible Configuration: Model-specific configs with component auto-enable
  • 6-Axis Force Torque Sensor: Safety monitoring, force-controlled movement, and torque-controlled joint operations
  • Intel RealSense depth cameras (optional): colour + metric depth from USB cameras on the device PC, each addressed by a device-local id — snapshots, live MJPEG preview, pixel → metres, and health on /status. See Depth camera.

🚀 Quick Start

Installation

# Prepare and activate your python enviroment, and install PyxArm in editable mode
pip install -e .

# Or, install PyxArm in development mode with pytests
pip install -e ".[dev]"

Basic Usage

from src.core.xarm_controller import XArmController

# Auto-detect and connect
controller = XArmController(
    gripper_type='bio',
    enable_track=True,
    auto_enable=True
)

if controller.initialize():
    # Move robot
    controller.move_to_position(x=300, y=0, z=300)
    controller.open_gripper()
    controller.disconnect()

Running against the Docker simulator

# Start the simulator (e.g. "5" for xArm5)
src/docker/docker_setup.sh start 5

# Drive it with the demo using the `docker` profile
python src/examples/demo_docker_sim.py

The in-process software simulation has been removed; use the Docker simulator for all simulator-mode work. See PYXARM_TESTING.md for setup details.

Web Interface & API Server

# Start the web UI (also auto-starts the API server on port 8000)
pyxarm web

# Or specify a custom host/port for the web UI
pyxarm web --host 0.0.0.0 --port 6001

# Run only the API server (this is how it is deployed as a device service)
pyxarm api --host 0.0.0.0 --port 8000

# Alternative method (without installing package)
python -m src.cli.main web

Access Points:

📷 Depth camera (Intel RealSense)

The service can own an Intel RealSense (D435i etc.) plugged into this PC over USB 3 — a local depth camera, distinct from the network PTZ "Lab Camera" the panel pans across the motion graph. It is the primitive later vision work (arrival verification, plate location) builds on; this release ships capture, health, images and pixel → metres.

C:\SDL_Tools\uv.exe sync --extra realsense      # pyrealsense2 + numpy + Pillow, once per venv

Then set enabled: true in src/settings/realsense.yaml and list each camera under cameras: with a device-local id (the first is rs435i), and restart. The panel grows a Depth Camera card per camera (Color / Depth preview, Start / Stop, click-to-measure). GET /realsense/cameras lists the cameras and hands back a URL per route; everything else lives under /realsense/<id>/ — status, start, stop, snapshot.jpg, depth.png, stream.mjpg, depth?x=&y=, intrinsics — and /status carries one components.realsense_<id> per camera plus details.realsense while configured. A camera never changes equipment_status: arm motion does not depend on it. Full reference, gating policy and troubleshooting: src/docs/REALSENSE_CAMERA.md.

💻 Command Line Interface

PyxArm includes a convenient command-line interface:

# Show help
pyxarm --help

# Start web interface
pyxarm web

# Start on custom host/port
pyxarm web --host 127.0.0.1 --port 8080

# Show version
pyxarm --version

🎯 Testing Strategy

  1. Docker Simulator → run the official UFACTORY simulator via the docker profile for off-hardware validation
  2. Real Hardware → production validation against the configured robot profile

📚 Documentation

For detailed guides on specific topics, please see the project root:

  • Features Overview: A high-level overview of the controller's features.
  • API Reference: Detailed documentation of the XArmController methods and parameters.
  • Simulation & Testing Guide: A comprehensive guide to simulation modes and the project's testing strategy.
  • Depth Camera: Intel RealSense integration — setup, /realsense/<id>/* API, /status blocks, troubleshooting.
  • Network Access: Tailscale port-forwarding setup for remote access to both the PyxArm API and the official UFactory Studio UI.

🔧 Advanced Features

  • Multi-gripper support: BioGripper Gen1, BioGripper Gen2 (stroke + force control), Standard, RobotIQ, or none
  • Named locations: Predefined positions with easy recall
  • Real-time monitoring: WebSocket updates and status tracking
  • Error handling: Comprehensive error history and recovery
  • Remote deployment: Docker containers on web servers
  • Configuration: All settings are now in src/settings/. Modify the .yaml files to match your hardware.
  • Examples: Run demo scripts in src/examples/ to see different functionalities.
  • API Server: Start the web server with pyxarm web or uvicorn src.core.xarm_api_server:app --reload.

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass: pytest
  5. Submit a pull request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🆘 Support

  • Issues: Create GitHub issues for bugs and feature requests
  • Documentation: Check root directory for comprehensive guides
  • Examples: Run demo scripts in src/examples/
  • CLI: Use pyxarm --help for command-line interface
  • API: Access interactive docs at http://localhost:8000/docs

Get started in minutes against the Docker simulator, then move to real hardware when ready.

About

Python code to drive and simlulate the movement of xArm5 to manipulate objects

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages