Commit Graph
11 Commits
Author SHA1 Message Date
cristhianandClaude Opus 5 8cd4db93f1 docs: update the test count
validate / markdownlint (pull_request) Successful in 58m43s
validate / validate (pull_request) Successful in 58m48s
validate / test (pull_request) Successful in 58m55s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:32:43 -05:00
cristhianandClaude Opus 5 4427da9c34 fix requests queued while the arm is down never being answered
_run_control never drained the job queue while the arm had not been reached, so
every request that arrived during an outage waited for a connection that might
never come. That breaks the one-response-per-request rule the node states as
absolute, and it was circular: requests are queued before the arm is reached
because recover is what a caller sends then, yet recover is served by the
control thread and so was the one service that could never run while down.

Serve the queue during the wait between connection attempts. Non-motion services
are tried on the arm, which is what lets recover and clear reach a controller
that answers while refusing to serve. A motion is refused rather than held: the
arm is somewhere unknown and nobody is watching, so a held motion would fire
unattended the moment the link came back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:32:31 -05:00
cristhianandClaude Opus 5 608e22078e fix zero-velocity jog reporting a running motion as idle
start_jog restored READY for any zero velocity, without checking whether a
motion owned the arm. A teleop source streaming zero-velocity ticks during a
move published moving -> ready mid-motion, and the closing guard in _run_motion
then saw READY rather than MOVING and never republished, so a caller watching
robot_state concluded the motion had finished early.

Route every "the arm has stopped" transition through _idle_unless_busy, which
reports READY only when no motion is running, no jog is driving the arm, and the
state has not already left MOVING for unavailable or recovering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:32:17 -05:00
cristhian 412aeac955 docs: rewrite README and AGENTS for the service node
validate / validate (pull_request) Successful in 58m45s
validate / test (pull_request) Successful in 58m51s
validate / markdownlint (pull_request) Successful in 58m44s
Documents the eleven services, the wait flag, motion_state and
preemption, and the lifecycle guarantees around restart and shutdown.

AGENTS.md records the two rules that are easy to break by accident: the
stopping event must stay set-to-stop, because an event that is set while
running makes every wait() return at once and turns each cadence into a
busy loop; and every request gets exactly one response, since a caller
has no other way to know its request finished.
2026-09-12 11:47:06 -03:00
cristhian 4ff584e9b5 add Gitea CI running dora validate, ruff, pytest and markdownlint
Replaces the GitHub markdownlint workflow with a Gitea one covering all
four gates, matching dora-zed and dora-plot. None of them need a robot:
dora validate runs no node, and the tests need neither a dataflow nor a
daemon.
2026-09-12 11:47:06 -03:00
cristhian 6311f6daae update packaging for dora 1.0 and uv
dora-rs 1.0.1 requires Python 3.11+. Drops the numpy<2 pin, which the
littlehand lockfile shows is unnecessary with the current xArm SDK, and
moves to hatchling with a uv lockfile that CI installs with --locked so a
dependency change that was never locked cannot reach main.

Removes scripts/install.sh: uv sync is the whole install now.
2026-09-12 11:47:06 -03:00
cristhian a2bf59065d add dora-node.yml manifest and a service example dataflow
The manifest is the only contract a consumer has, since no source package
is shared between node repositories, and dora validate checks a dataflow
against it without running the node — so CI needs no robot.

request and response are deliberately untyped. They carry request_id, and
dora defines that as the marker for a pattern-correlated port where
several Arrow schemas legitimately flow, one per service; the runtime
skips type checks on those messages, so annotating them would claim a
contract neither side keeps.

examples/ carries its own manifest because dora resolves a node's
manifest by walking up from its path, and a manifest applies only when
<manifest dir>/<entrypoint> names the same file as the dataflow's path.
Without it the example caller would be matched against the arm node's
manifest and strict validation would fail on the entrypoint mismatch.
2026-09-12 11:47:06 -03:00
cristhian fdabf7e241 refactor node into services over the dora 1.0 contract
Replaces the command/status JSON pair with dora's own service pattern:
one request, one response, correlated by the request_id dora puts in the
message metadata. Motions take a wait flag, and their lifecycle is
published as the arm's state on motion_state rather than as a per-goal
stream — the arm is one resource, so a new motion preempts the one in
play and a state describes that exactly.

motion_state is keyed by request_id and publishes accepted before the
control thread reaches the motion. Both exist to close one race: a caller
polling a bare moving/idle flag right after its request would read "not
moving" and conclude the motion had already finished.

Three threads, because the SDK blocks. The control thread carries out one
job at a time and may block; the reporter thread only ever reads the
report cache, so telemetry keeps flowing through a ten-second move; the
stopper thread exists because a stop fits nowhere else — it must not
queue behind the motion it aborts, and emergency_stop re-commands the
controller for up to three seconds, which is far too long to stop routing
a dataflow.

The event loop never calls anything that can block, get_state included:
that one is allowed there only because it reads the cache. The node is
built before the arm threads start, so INIT_MODE cannot move a robot
before the process has joined the dataflow, and a restart leaves the arm
where it is rather than driving it home unasked. Shutdown halts the arm
first — a motion lives in the controller, and dropping the connection
does not abort it.
2026-09-12 11:46:59 -03:00
cristhian eccd9e3c24 add xArm driver layer ported from littlehand
Brings across the robot layer from monolitic-littlehand-python, which was
written against this same controller. The point of it is that a command
either moved the arm or raised: the SDK returns a code for a motion the
controller quietly refused, wait_move calls that refusal finished, and
arm.error_code is the report stream's stale copy.

So errors are read live, arrival is confirmed rather than assumed, and a
move whose straight-line path leaves the joint envelope is refused before
it is commanded. Failures are typed, because the distinctions decide the
response: an unreachable pose is not an unavailable arm, and a joint past
its limit needs a motion nobody should start without warning the room.

read_report is the deliberate exception to all of it: a report-cache read
that costs no round trip and cannot block, which is the only read that
works while a motion is in flight.
2026-09-12 11:46:54 -03:00
cristhian 8333e53018 Update README.md 2026-04-01 23:42:22 +00:00
cristhian 19ef9c074f Initial commit 2026-04-01 08:03:25 +00:00