Skip to content

refactor(control): retire shared transport adapters - #5017

Open
TomCC7 wants to merge 28 commits into
cc/feat/connection-base-winnersfrom
cc/refactor/r1pro-connection
Open

TomCC7 wants to merge 28 commits into
cc/feat/connection-base-winnersfrom
cc/refactor/r1pro-connection

Conversation

@TomCC7

@TomCC7 TomCC7 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Overview

Migrate concrete connections to the shared joint/interface contract and retire the two shared transport adapters. Stacked on #5016.

Problem

The old adapters duplicate routing, while rotating native body commands through odometry unnecessarily couples driving to pose age.

Changes

  • R1Pro and Go2 accept body forward/lateral/turn actions directly, without an odom-yaw gate.
  • Publish velocity-only actuator JointState separately from pose/twist Odometry.
  • Preserve R1Pro measured chassis-speed odometry and its source timestamps.
  • Estimate Go2 body rates from successive measured poses; do not invent first-sample rates.
  • Route navigation followers through independent Odometry feedback.
  • Preserve G1/GR00T migration and remove the retired adapter implementations and registrations.

Validation

  • 29 focused driver/consumer checks independently passed, including hardware-free assembly parsing.
  • Targeted production mypy and Ruff passed.
  • Go2 rates are pose-difference estimates, not native chassis-rate samples. Hardware timing/accuracy and new CI remain unverified.

@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: b61686c Previous: 14c62ab Ratio
peak memory 1755.18 MB 1762.355 MB 1.00
peak threads 370 threads 385 threads 0.96
network (transport) 2729.202 MB 2731.013 MB 1.00
disk write 1.602 MB 1.605 MB 1.00
instructions 141.047 G 138.791 G 1.02

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: 4/5

[High impact] The PR changes command and feedback paths for three robot platforms.

Fix the MuJoCo halt path before merging so the simulated Go2 stops when coordinator commands disappear.

Findings

  1. P1 Simulated Go2 ignores the stop ▶
  2. P2 Stale-feedback stop claim overstates protection ▶

Summary

Replaces shared transport adapters with direct joint commands for R1Pro, G1, and Go2.

  • R1Pro coordinators send joint commands straight to the robot connection.
  • G1 commands and feedback now use named robot joints.
  • Go2 controllers map planar commands using measured robot yaw.
  • Shared transport adapters are no longer available for robot control.

Reviews (2) · Last reviewed commit: "test(r1pro): keep focused driver smoke c..." · Reviewed by Greptile

Comment on lines +67 to +69
arms are held where they were last measured. Stale feedback
(`reading_timeout_s`, 0.2 s) also stops commands until fresh readings
return. This replaces the old adapter path, where an estopped robot simply

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 Stale-feedback stop claim overstates protection

The 0.2-second timeout does not cover lost vendor joint feedback. After all three segments first report, _publish_loop keeps publishing cached readings at 100 Hz. ConnectionSource.on_message checks arrival time, not JointState.ts, so commands keep flowing even if all vendor joint feedback stops. Operators could rely on a stop that never happens.

Limit this claim to loss of the connection's published readings, or stop publishing when any vendor segment goes stale.

@codecov

codecov Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

❌ 18 Tests Failed:

Tests completed Failed Passed Skipped
8043 18 8025 55
View the top 3 failed test(s) by shortest run time
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[xarm6-planner-coordinator]
Stack Traces | 0.003s run time
blueprint_name = 'xarm6-planner-coordinator'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb9df7e8f20>

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(
        blueprint_name: str, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        # The multi-robot blueprints read ROBOT_IPS at import time.
        monkeypatch.setattr(global_config, "robot_ips", "192.0.2.10,192.0.2.11")
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. "
                "use fields from the config model or explicitly declared `__init__` keyword names."
            )
E           AssertionError: Blueprint 'xarm6-planner-coordinator' passes unknown module kwargs:
E             - dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']
E           
E           Blueprint kwargs are forwarded into the module constructor. use fields from the config model or explicitly declared `__init__` keyword names.

atom       = BlueprintAtom(kwargs={'arm': 'xarm6', 'gripper': 'xarm'}, module=<class 'dimos.robot.manipulators.xarm.connection.XArm...e', type=<class 'dimos.msgs.sensor_msgs.JointState.JointState'>, direction='out')), module_refs=(), instance_name=None)
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={'model': RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrp...Coordinator', 'arm_joint_state'): 'joint_state'}), requirement_checks=(), configurator_checks=(), hosted_placements=())
blueprint_name = 'xarm6-planner-coordinator'
listing    = "  - dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']"
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb9df7e8f20>
unknown_kwargs = []
violations = ["dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']"]

dimos/codebase_checks/test_blueprint_kwargs.py:90: AssertionError
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[piper-teleop]
Stack Traces | 0.004s run time
blueprint_name = 'piper-teleop'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb9ec34f530>

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(
        blueprint_name: str, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        # The multi-robot blueprints read ROBOT_IPS at import time.
        monkeypatch.setattr(global_config, "robot_ips", "192.0.2.10,192.0.2.11")
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. "
                "use fields from the config model or explicitly declared `__init__` keyword names."
            )
E           AssertionError: Blueprint 'piper-teleop' passes unknown module kwargs:
E             - dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']
E           
E           Blueprint kwargs are forwarded into the module constructor. use fields from the config model or explicitly declared `__init__` keyword names.

atom       = BlueprintAtom(kwargs={'model': RobotModel(_source_path=PosixPath('.../home/runner/.cache.../robot_assets/sources/9e208...Ref(name='file_info', type=<class 'dimos.msgs.files.File.File'>, direction='out')), module_refs=(), instance_name=None)
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={'instance_name': 'ControlCoordinator', 'model': RobotModel(_source_path=Po...Coordinator', 'arm_joint_state'): 'joint_state'}), requirement_checks=(), configurator_checks=(), hosted_placements=())
blueprint_name = 'piper-teleop'
listing    = "  - dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']"
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb9ec34f530>
unknown_kwargs = []
violations = ["dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']"]

dimos/codebase_checks/test_blueprint_kwargs.py:90: AssertionError
dimos.robot.manipulators.piper.test_connection::test_a_failed_start_puts_the_arm_in_its_emergency_stop
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb1b52425c0>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b504a990>

    def test_a_failed_start_puts_the_arm_in_its_emergency_stop(
        rig: Callable[..., Rig], monkeypatch: pytest.MonkeyPatch
    ) -> None:
        def stuck(self: PiperConnection, home: list[float], timeout_s: float = 0.0) -> None:
            raise PiperError("the arm did not reach home")
    
        monkeypatch.setattr(PiperConnection, "_go_home", stuck)
        sdk = FakePiper()
        with pytest.raises(PiperError):
>           rig(sdk)

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b504a990>
rig        = <function rig.<locals>.build at 0xffb1b52425c0>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5049c10>
stuck      = <function test_a_failed_start_puts_the_arm_in_its_emergency_stop.<locals>.stuck at 0xffb1b5240e00>

.../manipulators/piper/test_connection.py:406: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5049c10>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5103fe0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b504a840>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b504a990>
        sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5049c10>
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_a_failed_start_puts_the_a0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b504a840>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b504a840>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_home_outside_the_limits_is_refused_before_the_arm_is_touched
Stack Traces | 0.008s run time
tmp_path = PosixPath('.../pytest-0/popen-gw0/test_a_home_outside_the_limits0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b511c4a0>

    def test_a_home_outside_the_limits_is_refused_before_the_arm_is_touched(
        tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        sdk = FakePiper()
        monkeypatch.setattr(connection, "_open_sdk", lambda port: sdk)
        module = PiperConnection(
            can_port=PORT, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), model=piper_like_model(tmp_path)
        )
        Rig(module, sdk)
        try:
            with pytest.raises(ValueError, match="joint1"):
>               module.start()

module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511e0f0>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b511c4a0>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5049a90>
tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_a_home_outside_the_limits0')

.../manipulators/piper/test_connection.py:421: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511e0f0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511e0f0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...n_port='can9', setup_can=True, gripper=False, speed_percent=50, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...n_port='can9', setup_can=True, gripper=False, speed_percent=50, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_silent_arm_is_not_reported_and_takes_no_joint_target
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb1b5241620>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b511efc0>

    def test_a_silent_arm_is_not_reported_and_takes_no_joint_target(
        rig: Callable[..., Rig], monkeypatch: pytest.MonkeyPatch
    ) -> None:
>       r = rig()

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b511efc0>
rig        = <function rig.<locals>.build at 0xffb1b5241620>

.../manipulators/piper/test_connection.py:366: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b511cbc0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b511cf80>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511c530>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b511efc0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_a_silent_arm_is_not_repor0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511c530>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b511c530>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_target_past_the_jump_guard_is_refused_and_nothing_reaches_the_sdk
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb189767e20>

    def test_a_target_past_the_jump_guard_is_refused_and_nothing_reaches_the_sdk(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb189767e20>

.../manipulators/piper/test_connection.py:322: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b500c3e0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b500c290>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b500c7d0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b4ffa270>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_a_target_past_the_jump_gu0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b500c7d0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b500c7d0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_an_arm_stopped_outside_dimos_takes_no_command_until_its_errors_are_cleared
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb1897676a0>

    def test_an_arm_stopped_outside_dimos_takes_no_command_until_its_errors_are_cleared(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb1897676a0>

.../manipulators/piper/test_connection.py:335: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b51033b0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5102810>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5102210>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b5102f90>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_an_arm_stopped_outside_di0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5102210>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5102210>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_joint_targets_go_out_in_thousandths_of_a_degree
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb189767d80>

    def test_joint_targets_go_out_in_thousandths_of_a_degree(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb189767d80>

.../manipulators/piper/test_connection.py:269: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5123e00>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5130e90>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123dd0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b5123aa0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_joint_targets_go_out_in_t0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123dd0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123dd0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_stopping_sends_the_arm_home_then_puts_it_in_its_emergency_stop
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb1b52420c0>

    def test_stopping_sends_the_arm_home_then_puts_it_in_its_emergency_stop(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb1b52420c0>

.../manipulators/piper/test_connection.py:387: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5103380>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5102300>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5103da0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b5102ab0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_stopping_sends_the_arm_ho0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5103da0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5103da0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_teach_mode_stops_every_command
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xffb1b5241580>

    def test_teach_mode_stops_every_command(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb1b5241580>

.../manipulators/piper/test_connection.py:355: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb189778cb0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1897794f0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb189778da0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1897787d0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_teach_mode_stops_every_co0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb189778da0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb189778da0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_an_arm_with_a_fault_is_not_sent_home_when_stopping
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xffb1b5241300>

    def test_an_arm_with_a_fault_is_not_sent_home_when_stopping(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb1b5241300>

.../manipulators/piper/test_connection.py:428: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b517b7d0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5179970>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b517b7a0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b517b5f0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_an_arm_with_a_fault_is_no0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b517b7a0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b517b7a0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_start_resets_the_arm_turns_its_motors_on_and_sends_it_home
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xffb189766fc0>

    def test_start_resets_the_arm_turns_its_motors_on_and_sends_it_home(
        rig: Callable[..., Rig],
    ) -> None:
        sdk = FakePiper(arm_status=COLLISION)
        sdk.positions = [10_000, 0, 0, 0, 0, 0]
>       r = rig(sdk)

rig        = <function rig.<locals>.build at 0xffb189766fc0>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb18979d070>

.../manipulators/piper/test_connection.py:244: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb18979d070>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb18977a7b0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b50d1430>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb18979d190>
        sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb18979d070>
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_start_resets_the_arm_turn0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b50d1430>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b50d1430>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_the_gripper_goes_out_as_its_gap_with_every_command
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xffb189767880>

    def test_the_gripper_goes_out_as_its_gap_with_every_command(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb189767880>

.../manipulators/piper/test_connection.py:295: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b50620c0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5123c80>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123aa0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b51329f0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_the_gripper_goes_out_as_i0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123aa0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5123aa0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_joint_state_names_each_joint_with_its_position_speed_and_torque
Stack Traces | 0.01s run time
rig = <function rig.<locals>.build at 0xffb189767420>

    def test_joint_state_names_each_joint_with_its_position_speed_and_torque(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb189767420>

.../manipulators/piper/test_connection.py:278: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5130f80>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb1b5131be0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5132180>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b5131430>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_joint_state_names_each_jo0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5132180>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b5132180>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_the_gripper_is_read_in_metres_in_a_message_of_its_own
Stack Traces | 0.01s run time
rig = <function rig.<locals>.build at 0xffb1b5226c00>

    def test_the_gripper_is_read_in_metres_in_a_message_of_its_own(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xffb1b5226c00>

.../manipulators/piper/test_connection.py:312: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xffb1b5123bf0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xffb189778e90>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b4ff8260>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xffb1b504aea0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_the_gripper_is_read_in_me0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b4ff8260>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xffb1b4ff8260>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.control.benchmarking.test_benchmark::test_end_to_end_controller_benchmark_scoring
Stack Traces | 0.16s run time
tmp_path = PosixPath('.../pytest-0/popen-gw3/test_end_to_end_controller_ben0')

    def test_end_to_end_controller_benchmark_scoring(tmp_path):
        """Prove the full decoupled chain in-process against the in-repo FOPDT sim:
        the benchmark publishes a path+speed (here driven directly), the RPP follower
        tracks it against TwistBasePlantSim, the benchmark records the executed odom,
        odom-based completion fires, a run is written, and the offline scorer emits
        metrics."""
        art = TuningConfig.from_json(DEFAULT_ARTIFACT_PATH)
        task = create_task(
            SimpleNamespace(
                name="rpp_follower", joint_names=_JOINTS, priority=10, params={"speed": 0.5}
            ),
            None,
        )
        plant = TwistBasePlantSim(
            TwistBasePlantParams(
                vx=FopdtChannelParams(art.plant.vx.K, art.plant.vx.tau, art.plant.vx.L),
                vy=FopdtChannelParams(art.plant.vy.K, art.plant.vy.tau, art.plant.vy.L),
                wz=FopdtChannelParams(art.plant.wz.K, art.plant.wz.tau, art.plant.wz.L),
            )
        )
        plant.reset(0.0, 0.0, 0.0, 0.1)
    
        # Benchmark anchors the path to the robot's first odom (here the origin).
        ref = shift_path_to_start_at_pose(straight_line(length=2.0), _pose(plant.x, plant.y, plant.yaw))
        task.start_path(ref, _pose(plant.x, plant.y, plant.yaw))
    
        recorder = OdomRecorder()
        monitor = _monitor(ref, dwell_s=0.3)
        done = False
        for k in range(800):
            out = task.compute(_state(plant.x, plant.y, plant.yaw, t=k * 0.1))
            cvx, cvy, cwz = out.velocities if out is not None else (0.0, 0.0, 0.0)
            # Benchmark records what it sees over the transport: cmd_vel + odom.
            recorder.on_cmd_vel(Twist(linear=Vector3(cvx, cvy, 0.0), angular=Vector3(0.0, 0.0, cwz)))
            recorder.on_odom(_pose(plant.x, plant.y, plant.yaw), now=k * 0.1)
            lin, ang = recorder.body_speed()
            if monitor.update(plant.x, plant.y, lin, ang, k * 0.1):
                done = True
                break
            plant.step(cvx, cvy, cwz, 0.1)
>       assert done, "odom-based completion never fired"
E       AssertionError: odom-based completion never fired
E       assert False

ang        = 0.0
art        = TuningConfig(provenance=Provenance(robot_id='go2', surface='concrete', mode='default', date='2026-06-23', git_sha='972...3, L=0.15, r2=0.9999577115547055, source='sweep')]), floor_probe_results=None, valid_for_tuning=True, schema_version=1)
cvx        = 0.0
cvy        = 0.0
cwz        = 0.0
done       = False
k          = 799
lin        = 0.0
monitor    = <dimos.control.benchmarking.benchmark.CompletionMonitor object at 0xffa4d5726300>
out        = None
plant      = <dimos.control.benchmarking.plant.TwistBasePlantSim object at 0xffa4c44dffe0>
recorder   = <dimos.control.benchmarking.benchmark.OdomRecorder object at 0xffa4d57261b0>
ref        = <dimos.msgs.nav_msgs.Path.Path object at 0xffa4d5727170>
task       = <dimos.control.tasks.rpp_path_follower_task.rpp_path_follower_task.RPPPathFollowerTask object at 0xffa4c5b7e030>
tmp_path   = PosixPath('.../pytest-0/popen-gw3/test_end_to_end_controller_ben0')

.../control/benchmarking/test_benchmark.py:326: AssertionError
dimos.control.benchmarking.test_benchmark::test_end_to_end_fullpose_benchmark_scoring
Stack Traces | 0.163s run time
tmp_path = PosixPath('.../pytest-0/popen-gw3/test_end_to_end_fullpose_bench0')

    def test_end_to_end_fullpose_benchmark_scoring(tmp_path):
        """Same decoupled chain, full-pose flavor: the holonomic follower tracks a
        translate-while-rotating path against the FOPDT sim, completion fires from
        odom, and the offline scorer reports heading error against the COMMANDED
        yaw (which the tangent-facing followers could not keep small)."""
        from dimos.control.tasks.holonomic_pose_follower_task.holonomic_pose_follower_task import (
            create_task as create_holonomic_task,
        )
    
        art = TuningConfig.from_json(DEFAULT_ARTIFACT_PATH)
        task = create_holonomic_task(
            SimpleNamespace(
                name="holonomic_follower", joint_names=_JOINTS, priority=10, params={"speed": 0.5}
            ),
            None,
        )
        plant = TwistBasePlantSim(
            TwistBasePlantParams(
                vx=FopdtChannelParams(art.plant.vx.K, art.plant.vx.tau, art.plant.vx.L),
                vy=FopdtChannelParams(art.plant.vy.K, art.plant.vy.tau, art.plant.vy.L),
                wz=FopdtChannelParams(art.plant.wz.K, art.plant.wz.tau, art.plant.wz.L),
            )
        )
        plant.reset(0.0, 0.0, 0.0, 0.1)
    
        ref = shift_path_to_start_at_pose(
            straight_rotate(length=3.0), _pose(plant.x, plant.y, plant.yaw)
        )
        task.start_path(ref, _pose(plant.x, plant.y, plant.yaw))
    
        recorder = OdomRecorder()
        monitor = _monitor(ref, dwell_s=0.3)
        done = False
        for k in range(800):
            out = task.compute(_state(plant.x, plant.y, plant.yaw, t=k * 0.1))
            cvx, cvy, cwz = out.velocities if out is not None else (0.0, 0.0, 0.0)
            recorder.on_cmd_vel(Twist(linear=Vector3(cvx, cvy, 0.0), angular=Vector3(0.0, 0.0, cwz)))
            recorder.on_odom(_pose(plant.x, plant.y, plant.yaw), now=k * 0.1)
            lin, ang = recorder.body_speed()
            if monitor.update(plant.x, plant.y, lin, ang, k * 0.1):
                done = True
                break
            plant.step(cvx, cvy, cwz, 0.1)
>       assert done, "odom-based completion never fired"
E       AssertionError: odom-based completion never fired
E       assert False

ang        = 0.0
art        = TuningConfig(provenance=Provenance(robot_id='go2', surface='concrete', mode='default', date='2026-06-23', git_sha='972...3, L=0.15, r2=0.9999577115547055, source='sweep')]), floor_probe_results=None, valid_for_tuning=True, schema_version=1)
create_holonomic_task = <function create_task at 0xffa550b31c60>
cvx        = 0.0
cvy        = 0.0
cwz        = 0.0
done       = False
k          = 799
lin        = 0.0
monitor    = <dimos.control.benchmarking.benchmark.CompletionMonitor object at 0xffa53e120410>
out        = JointCommandOutput(joint_names=['go2/vx', 'go2/vy', 'go2/wz'], positions=None, velocities=[0.0, 0.0, 0.0], efforts=None, mode=<ControlMode.VELOCITY: 'velocity'>)
plant      = <dimos.control.benchmarking.plant.TwistBasePlantSim object at 0xffa53e123560>
recorder   = <dimos.control.benchmarking.benchmark.OdomRecorder object at 0xffa53e120530>
ref        = <dimos.msgs.nav_msgs.Path.Path object at 0xffa53e121df0>
task       = <dimos.control.tasks.holonomic_pose_follower_task.holonomic_pose_follower_task.HolonomicPoseFollowerTask object at 0xffa4c5ab0fb0>
tmp_path   = PosixPath('.../pytest-0/popen-gw3/test_end_to_end_fullpose_bench0')

.../control/benchmarking/test_benchmark.py:396: AssertionError
dimos.robot.galaxea.r1pro.blueprints.manipulation.test_r1pro_planar_preview::test_real_control_declares_planar_velocity_joints_without_position_commands
Stack Traces | 6.89s run time
def test_real_control_declares_planar_velocity_joints_without_position_commands() -> None:
        """Measured planar joints are ordinary velocity-only hardware interfaces."""
        coordinator = _coordinator_config(r1pro_control())
    
        # The real robot is driven through R1ProConnection: no hardware adapters.
        assert coordinator.hardware == []
        claimed = {joint for task in coordinator.tasks for joint in task.joint_names}
        assert claimed == {*R1PRO_UPPER_BODY_JOINTS, *R1PRO_BASE_JOINTS}
        from dimos.control.contract.keys import Interface
    
        (declaration,) = coordinator.connections
>       assert all(
            declaration.joint_commands[joint] == (Interface.VELOCITY,)
            for joint in R1PRO_PLANAR_BASE.joint_names
        )

Interface  = <enum 'Interface'>
claimed    = {'r1pro/base_forward', 'r1pro/base_lateral', 'r1pro/base_turn', 'r1pro/left_arm_joint1', 'r1pro/left_arm_joint2', 'r1pro/left_arm_joint3', ...}
coordinator = ControlCoordinatorConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_..._turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)], reading_timeout_s=0.2, zero_stiffness_joints=[])
declaration = ConnectionConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts...': (<Interface.VELOCITY: 'velocity'>,), 'r1pro/base_turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)

.../blueprints/manipulation/test_r1pro_planar_preview.py:123: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

.0 = <tuple_iterator object at 0xffe68d1ce380>

    assert all(
>       declaration.joint_commands[joint] == (Interface.VELOCITY,)
        for joint in R1PRO_PLANAR_BASE.joint_names
    )
E   KeyError: 'r1pro/base_x'

.0         = <tuple_iterator object at 0xffe68d1ce380>
Interface  = <enum 'Interface'>
declaration = ConnectionConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts...': (<Interface.VELOCITY: 'velocity'>,), 'r1pro/base_turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)
joint      = 'r1pro/base_x'

.../blueprints/manipulation/test_r1pro_planar_preview.py:124: KeyError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

TomCC7 and others added 11 commits November 9, 2026 21:00
…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 changed the title refactor(r1pro): drive the R1 Pro through its connection module, no adapters refactor(control): retire shared transport adapters Nov 10, 2026
@TomCC7
TomCC7 marked this pull request as ready for review November 10, 2026 08:53
@TomCC7
TomCC7 requested a review from jeff-hykin as a code owner November 10, 2026 08:53
Comment on lines +497 to +498
def halt(self) -> None:
self.connection.stop_movement()

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 Simulated Go2 ignores the stop

GO2Connection.halt() calls MujocoConnection.stop_movement(), which does nothing. If the coordinator stops while the connection stays running, the new deadman calls this method, but the simulator keeps walking at its last speed. The removed adapter sent a zero Twist when it disconnected.

Make the MuJoCo stop path write zero speeds before relying on it here.

This branch was successfully deployed

No deployments
cachix — d83ddef9 Deployed Nov 10, 2026 by TomCC7 via cachix-build-macos #16876
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.

1 participant