Skip to content

Unify connection joint/interface routing for task commands - #5016

Open
TomCC7 wants to merge 18 commits into
mainfrom
cc/feat/connection-base-winners
Open

TomCC7 wants to merge 18 commits into
mainfrom
cc/feat/connection-base-winners

Conversation

@TomCC7

@TomCC7 TomCC7 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Overview

Unify connection control around named joints and supported interfaces. First layer of #5017 → #5019 → #5084.

Changes

  • Use ordinary per-joint arbitration for arms and body velocity-only action axes; keep native body commands independent of odometry yaw.
  • Keep pose telemetry on independent Odometry and vendor encoding in concrete connections. Preserve model limits and fresh-input requirements after E-stop clear.
  • Operator hold freezes position-capable joints and zeros velocity-only joints; joint_names restricts both groups. Update the hold documentation accordingly.
  • Respect xArm's shared SDK mode: position-only arm declarations by default, or an explicit velocity-only arm declaration for jogging. Reject mixed/dual arm declarations; the gripper remains position-controlled. Generic per-joint arbitration is unchanged.
  • Reset required readings and recapture holds after any required feedback port expires, including joint feedback outages masked by live IMU traffic.
  • Migrate the Piper connection and the newly landed Piper/xArm coordinator blueprints to per-joint declarations; remove unused virtual-coordinate conversion helpers and their tests, retaining navigation's own world-to-body conversion.

Rebase and validation

Head: 6d4ea6e92245816d0c59396d302cd36d0e236a09, rebased onto main 5bc57a07087d427cbbdd84281523c5c1e970ff0c. All 17 original commits were replayed from the previous main boundary; the original head is backed up locally.

Implementer validation at this head:

  • 307 passed: affected control source/coordinator/connection/contract/hold/velocity tests, Piper and xArm connections and blueprint tests, both CI-failing blueprint-kwargs cases, navigation controllers, and blueprint-registry generation with CI=1.
  • xArm connection tests also passed under hash seeds 1 and 2 (23 each; seed 0 is included above).
  • Targeted mypy passed for 7 production files; Ruff lint/format passed for all 10 changed Python files; git diff --check passed.
  • The 13 Piper failures and 2 blueprint-kwargs failures from run 42960540692 are covered by the passing local run. Its Piper declaration type error is also fixed.

Hardware behavior/latency, the full test suite, and post-push CI are not verified. No new CI polling was started. Existing dependency deprecation/resource warnings remain.

Only this root PR was rewritten. The published R1 Pro descendants already use dictionary declarations and connections=, but still need a separate parent synchronization after this root rebase; their branches were not changed.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go2 replay realtime (arm64)

Details
Benchmark suite Current: 6d4ea6e Previous: 5bc57a0 Ratio
peak memory 1736.574 MB 1776.91 MB 0.98
peak threads 375 threads 376 threads 1.00
network (transport) 2725.84 MB 2730.972 MB 1.00
disk write 1.566 MB 1.59 MB 0.98
instructions 137.175 G 138.52 G 0.99

This comment was automatically generated by workflow using github-action-benchmark.

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[High impact] The PR changes shared command routing used by arm and mobile-robot control workflows.

Do not merge yet: xArm commands can still fight each other, and joint-feedback recovery can still restore old holds.

Findings

  1. P1 xArm commands fight each other ▶
  2. P1 Old holds return after silence ▶
  3. P2 Hold instructions contradict behavior ▶

Summary

Unifies connection commands around named joints and their declared interfaces.

  • Connections and tasks share declared joint interfaces for routing and limits.
  • Base Twist commands map selected components directly to velocity joints.
  • Operator hold freezes position joints and zeros velocity-only joints.

Reviews (4) · Last reviewed commit: "fix(control): stop velocity-only joints ..." · Reviewed by Greptile

Comment thread dimos/control/coordinator.py Resolved
@codecov

codecov Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.04598% with 17 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/control/connection_source.py 89.85% 4 Missing and 3 partials ⚠️
dimos/control/coordinator.py 77.77% 1 Missing and 1 partial ⚠️
...control/tasks/velocity_task/twist_velocity_task.py 91.66% 1 Missing and 1 partial ⚠️
dimos/control/tasks/velocity_task/velocity_task.py 84.61% 0 Missing and 2 partials ⚠️
dimos/robot/manipulators/xarm/connection.py 84.61% 1 Missing and 1 partial ⚠️
dimos/control/tick_loop.py 92.30% 1 Missing ⚠️
dimos/robot/manipulators/piper/connection.py 75.00% 0 Missing and 1 partial ⚠️
@@            Coverage Diff             @@
##             main    #5016      +/-   ##
==========================================
+ Coverage   83.82%   83.93%   +0.11%     
==========================================
  Files        1866     1868       +2     
  Lines      182008   182436     +428     
  Branches    14987    15002      +15     
==========================================
+ Hits       152575   153136     +561     
+ Misses      25467    25428      -39     
+ Partials     3966     3872      -94     
Components Coverage Δ
Tests 96.69% <100.00%> (+0.03%) ⬆️
Flag Coverage Δ
OS-macos-latest 78.09% <97.93%> (+0.07%) ⬆️
OS-ubuntu-24.04-arm 77.84% <94.02%> (+0.04%) ⬆️
OS-ubuntu-latest 78.22% <97.93%> (+0.07%) ⬆️
Py-3.10 78.22% <97.93%> (+0.07%) ⬆️
Py-3.11 78.20% <97.93%> (+0.07%) ⬆️
Py-3.12 78.20% <97.93%> (+0.06%) ⬆️
SelfHosted-Linux 40.69% <30.80%> (-0.05%) ⬇️
SelfHosted-macOS 40.42% <30.80%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/control/_control_test_helpers.py 89.47% <100.00%> (+0.87%) ⬆️
dimos/control/blueprints/mobile.py 100.00% <ø> (ø)
dimos/control/connection/connection_module.py 99.33% <100.00%> (-0.02%) ⬇️
dimos/control/connection/mock_joint_connection.py 90.78% <100.00%> (+1.90%) ⬆️
dimos/control/connection/test_connection_module.py 97.58% <ø> (ø)
...s/control/connection/test_mock_joint_connection.py 100.00% <100.00%> (ø)
dimos/control/contract/convert.py 100.00% <100.00%> (ø)
dimos/control/contract/keys.py 100.00% <ø> (ø)
dimos/control/contract/limits.py 100.00% <100.00%> (ø)
dimos/control/contract/test_convert.py 100.00% <100.00%> (ø)
... and 55 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TomCC7
TomCC7 added this pull request to stack #5018 November 4, 2026 08:05
@TomCC7
TomCC7 marked this pull request as draft November 4, 2026 08:05
TomCC7 added a commit that referenced this pull request Nov 4, 2026
… hold

Greptile review on #5016:

- P1: OperatorHoldTask discovered axes only in the joint-state snapshot,
  which never contains a connection robot's base axes (they are commanded
  speeds, not read positions), so a running velocity task kept driving the
  base through a hold, including after an e-stop clear. The coordinator
  now names every connection base axis to the hold (on start and on
  add_task), and the hold zeroes them like an adapter base's velocity
  joints.
- P2: the base-timeout test slept 0.1s against a 0.2s reading timeout, so
  a paused runner stopped the command entirely and the zeroed twist never
  arrived (seen on macOS CI). The test now advances the task clock
  (perf_counter, the source of state.t_now) past the task timeout by hand
  and re-reports the base's feedback right before each tick, so only the
  task command expires.
- P2: the routing test drove an arm named mock and a base named go2, never
  exercising one robot's <robot>/base split and merge. A new test gives
  one robot joint ports and cmd_vel and asserts both commands leave in the
  same tick; another covers the route-level mode-conflict guard, which was
  the patch's only uncovered code.

Regressions: the hold zeroes a running base and keeps it after an e-stop
clear (connection level), and unit tests cover set_base_axes with and
without an e-stop.
@TomCC7 TomCC7 changed the title feat(control): let tasks drive a connection robot's base through the coordinator Unify connection joint/interface routing for task commands Nov 9, 2026
Comment thread dimos/control/connection_source.py Resolved
Comment thread dimos/control/connection_source.py Resolved
Comment thread dimos/robot/planar.py Resolved
@TomCC7
TomCC7 marked this pull request as ready for review November 10, 2026 04:58
@TomCC7
TomCC7 requested a review from jeff-hykin as a code owner November 10, 2026 04:58
Comment on lines 441 to 446
velocity_key = joint.command_keys.get(Interface.VELOCITY)
if mode is ControlMode.VELOCITY and velocity_key is not None:
values[velocity_key] = 0.0
self._holds.pop(joint.name, None)
continue
hold = self._hold(joint, latest)
if hold is not None:
values[joint.command_keys[Interface.POSITION]] = hold
elif velocity_key is not None:
values[velocity_key] = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 xArm commands fight each other

Jogging one xArm joint now leaves the other joints on position hold. That sends both position_command and velocity_command in the same tick. But XArmConnection.write() switches the whole arm between position and velocity modes, and each write fills every arm joint. The messages overwrite each other and repeatedly switch modes.

Preserve the xArm's shared-mode constraint, or translate the complete command into one supported arm-wide write.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d4ea6e. The concrete xArm config now declares one shared arm interface: position-only by default, or explicitly velocity-only for jogging; mixed/dual arm declarations are rejected. The gripper remains position-only. Generic arbitration has no arm/base special case. The regression sends a single velocity winner through ConnectionSource and the real command port, verifies a single velocity frame with zero-speed peers, no servo overwrite and no repeated SDK mode switches. xArm connection tests passed under hash seeds 0, 1 and 2 (23 cases per seed); the final affected suite passed 307 cases.

Comment on lines +344 to +345
return len(self._reported) == len(self._needed) and all(
now - self._port_receipts[port] <= self._timeout for port in self._needed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Old holds return after silence

The new per-port timeout stops commands when joint_state goes quiet while imu keeps arriving. Recovery still checks only the last message from any port. The live IMU prevents _resumed from being set, so returning joint feedback makes command() reuse old position holds instead of taking the newly measured positions. The robot can move back toward an old target after the outage.

Reset holds and required readings after any required port expires, using the same freshness check that stopped commands.

Knowledge Base Used: Robot control platform

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d4ea6e. Recovery now checks the required ports' own receipt ages, not the last message from any port. Any expired required port clears cached required readings and marks holds for recapture. The regression keeps IMU live across a joint-state outage, returns partial then complete joint feedback, and verifies the old 0.5 position target is replaced by the newly measured 0.8 hold. Included in the final 307 passing affected tests.

joint_commands: tuple[Interface, ...] = ()
pd: bool = False
base_range: Twist | None = None
joint_commands: dict[str, tuple[Interface, ...]] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dependent R1 Pro startup breaks

The new joint_commands dictionary breaks R1ProConnectionConfig in PR #5017, which still declares a tuple. The revised limits() calls .keys() on that tuple and fails before connecting. PRs #5019 and #5084 retain the same declaration. They also still use the removed base converters and old coordinator configuration.

Migrate those dependent callers alongside this contract change, or keep a compatible path until they are updated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the actual published descendants: #5017 at b61686c, #5019 at 3920bf8, and #5084 at f03b8b2 all declare joint_commands as a dictionary (upper-body positions, base velocities) and their coordinator blueprints use connections=. The connection/blueprint files do not reference the removed virtual-coordinate converters. No legacy shim was added. 6d4ea6e additionally migrates the Piper connection and new Piper/xArm blueprints from main; their actual CI-failing cases now pass locally, with targeted mypy clean. Only #5016 was rebased/pushed; the descendants still need a separate parent synchronization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking the published descendants. You’re right—the current R1 Pro declarations use dictionary-based joint_commands, and the changes in #5016 migrate its Piper connection and coordinator blueprints as well. My startup-break warning was based on stale branch state; no legacy shim is needed, and I retract that finding. The descendants’ separate parent synchronization remains follow-up coordination, not a blocker in #5016.

TomCC7 added a commit that referenced this pull request Nov 10, 2026
… hold

Greptile review on #5016:

- P1: OperatorHoldTask discovered axes only in the joint-state snapshot,
  which never contains a connection robot's base axes (they are commanded
  speeds, not read positions), so a running velocity task kept driving the
  base through a hold, including after an e-stop clear. The coordinator
  now names every connection base axis to the hold (on start and on
  add_task), and the hold zeroes them like an adapter base's velocity
  joints.
- P2: the base-timeout test slept 0.1s against a 0.2s reading timeout, so
  a paused runner stopped the command entirely and the zeroed twist never
  arrived (seen on macOS CI). The test now advances the task clock
  (perf_counter, the source of state.t_now) past the task timeout by hand
  and re-reports the base's feedback right before each tick, so only the
  task command expires.
- P2: the routing test drove an arm named mock and a base named go2, never
  exercising one robot's <robot>/base split and merge. A new test gives
  one robot joint ports and cmd_vel and asserts both commands leave in the
  same tick; another covers the route-level mode-conflict guard, which was
  the patch's only uncovered code.

Regressions: the hold zeroes a running base and keeps it after an e-stop
clear (connection level), and unit tests cover set_base_axes with and
without an e-stop.
@TomCC7
TomCC7 force-pushed the cc/feat/connection-base-winners branch from 8f86fbc to a1502e7 Compare November 10, 2026 05:14
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Nov 10, 2026
Comment on lines +183 to +186
eligible = {
name
for name, interfaces in state.joints.joint_command_interfaces.items()
if Interface.POSITION in interfaces

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operator hold should stop every joint, including speed-only ones.

We send the base zero speed how its implemented right now. with this change nav or teleop can keep driving the base while a hold is on

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d7763c0. OperatorHoldTask now freezes measured positions on position-capable joints and sends zero velocity on velocity-only joints, selected from declared interfaces rather than base names. joint_names scopes both groups. Mixed-mode outputs use ordinary per-joint arbitration; no coordinator hold special case. Regression checks cover ongoing velocity input, E-stop/clear remaining held until acknowledgement, and scoped exclusions. 141 targeted tests passed; 57 hold/connection tests independently rerun; targeted production mypy and Ruff passed. No CI or hardware checks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requested d7763c0 hold behavior is preserved in the rebased head 6d4ea6e: position-capable joints freeze, velocity-only joints receive zero speed, explicit scope restricts both, and E-stop clear does not replay stale motion. The README contradiction is now fixed too. Hold/coordinator/connection regressions are included in the final 307 passing affected tests. No human threads were auto-resolved.

@mustafab0

Copy link
Copy Markdown
Contributor

@greptile review

@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Nov 11, 2026
@TomCC7

TomCC7 commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

@greptile

Comment thread dimos/control/README.md Outdated
Comment on lines +143 to +147
`operator_hold` (priority 100) automatically freezes every joint that accepts
position commands at its measured position and waits for a person. Velocity-only
joints remain visible to other tasks but are not claimed by the hold, even when
they report positions. `joint_names` optionally restricts the eligible subset.
It is not an e-stop: the e-stop cuts motion; the hold keeps position.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hold instructions contradict behavior

operator_hold now claims velocity-only joints and sends zero speed, but this section still says those joints are not claimed. That gives operators the wrong expectation about whether the base can move during a hold. Update the description to say that the hold stops velocity-only joints unless joint_names excludes them.

Suggested change
`operator_hold` (priority 100) automatically freezes every joint that accepts
position commands at its measured position and waits for a person. Velocity-only
joints remain visible to other tasks but are not claimed by the hold, even when
they report positions. `joint_names` optionally restricts the eligible subset.
It is not an e-stop: the e-stop cuts motion; the hold keeps position.
`operator_hold` (priority 100) automatically freezes every joint that accepts
position commands at its measured position and waits for a person. Velocity-only
joints are claimed at the same priority and commanded to zero speed, even when
they report positions. `joint_names` optionally restricts both groups.
It is not an e-stop: the e-stop cuts motion; the hold keeps commanding these targets.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d4ea6e. The README now says operator_hold claims velocity-only joints at the same priority and commands zero speed, and that joint_names restricts both position and velocity groups. This matches the preserved hold implementation; the final 307 passing affected tests include hold and scoped-hold coverage.

Comment thread dimos/utils/transform_utils.py Outdated
return Pose(position=offset_position, orientation=target_pose.orientation)


def twist_to_values(twist: Twist, joints: tuple[str, str, str], *, yaw: float) -> dict[str, float]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes can now be deleted

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 6d4ea6e: twist_to_values, twist_from_values, pose_to_values, pose_from_values and their private planar helpers, plus tests that only exercised those unused helpers. A repository-wide reference audit found no production callers. Kept twist_to_numpy and the navigation controllers' independently used world-to-body conversion. Navigation, conversion and affected control/connection tests are included in the final 307 passing tests.

TomCC7 and others added 18 commits November 10, 2026 23:51
…coordinator

A robot behind a ConnectionModule with a cmd_vel port had its base told 0
on every axis every tick, so no task could drive it. Now its base axes
(<robot>/base/vx, vy, wz) are registered like an adapter base's virtual
joints: tasks claim and win them in arbitration, the tick loop routes them
to their own per-robot entry so a velocity task on the base never
mode-conflicts with a position task on the joints, and ConnectionSource
fills each axis with the winner's speed or 0 when idle or stopped.

The coordinator's twist_command mapping covers connection robots too, so
the same teleop/nav publishers drive an adapter base or a connection base
unchanged. Estop semantics are unchanged: commands stop, the connection
module's own deadman halts the robot.

Arm-only coordinators and legacy adapter stacks are untouched: a robot
without a cmd_vel port has no base axes, and adapters never take the new
route.
Follow-up verification for the connection base winners: a velocity task or
twist drives vx, vy and wz each non-zero through to the robot's cmd_vel
port; two velocity tasks on the same base arbitrate by priority and the
loser resumes when the winner stops; and a task whose stream goes quiet
zeroes every axis, as an adapter base's does.
The velocity-task timeout can be observed by a later tick than the
sleep(0.1) boundary on slow or loaded runners (seen on macOS CI), so
keep ticking until the zeroed command shows up.
… hold

Greptile review on #5016:

- P1: OperatorHoldTask discovered axes only in the joint-state snapshot,
  which never contains a connection robot's base axes (they are commanded
  speeds, not read positions), so a running velocity task kept driving the
  base through a hold, including after an e-stop clear. The coordinator
  now names every connection base axis to the hold (on start and on
  add_task), and the hold zeroes them like an adapter base's velocity
  joints.
- P2: the base-timeout test slept 0.1s against a 0.2s reading timeout, so
  a paused runner stopped the command entirely and the zeroed twist never
  arrived (seen on macOS CI). The test now advances the task clock
  (perf_counter, the source of state.t_now) past the task timeout by hand
  and re-reports the base's feedback right before each tick, so only the
  task command expires.
- P2: the routing test drove an arm named mock and a base named go2, never
  exercising one robot's <robot>/base split and merge. A new test gives
  one robot joint ports and cmd_vel and asserts both commands leave in the
  same tick; another covers the route-level mode-conflict guard, which was
  the patch's only uncovered code.

Regressions: the hold zeroes a running base and keeps it after an e-stop
clear (connection level), and unit tests cover set_base_axes with and
without an e-stop.
The task clock (perf_counter) was already patched, but the connection
source stamps feedback receipts and checks their freshness with the real
monotonic clock, so a scheduling pause longer than reading_timeout_s
between the last report and the tick could still stale the feedback and
drop the zeroing command.

Patch time.monotonic with its own frozen clock too, and keep a real
0.25s pause between the report and the tick to prove a slow runner
cannot stale the feedback: with the monotonic patch removed the strict
assertions fail again.
@TomCC7
TomCC7 force-pushed the cc/feat/connection-base-winners branch from d7763c0 to 6d4ea6e Compare November 11, 2026 08:01

This branch was successfully deployed

No deployments
cachix — 13e26fbb Deployed Nov 10, 2026 by TomCC7 via cachix-build-macos #16826
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants