Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e856de7
feat: add OpenYam LeRobot data collection flow
TomCC7 Aug 18, 2026
7d1f447
fix(imitation): skip TF for collection streams
TomCC7 Aug 18, 2026
2d44b09
fix(imitation): record OpenYAM wrist frames over SHM
TomCC7 Aug 19, 2026
8d4b2cd
feat(collection): configure wrist webcam from CLI
TomCC7 Aug 21, 2026
a5af199
feat(imitation): add native OpenYAM collection recorder
TomCC7 Aug 26, 2026
d7fa9ba
fix(native-recorder): omit disabled stream topics
TomCC7 Aug 28, 2026
918da4b
fix(lerobot): restore isolated runtime and dataset viz
TomCC7 Aug 28, 2026
2c6a34a
feat(imitation): validate OpenYAM collection datasets
TomCC7 Aug 28, 2026
2fd10d7
feat(imitation): support native MCAP collection
TomCC7 Aug 28, 2026
7cc6a0d
refactor(imitation): make OpenYAM collection native
TomCC7 Sep 1, 2026
9e22d7a
refactor(imitation): run LeRobot dataprep directly
TomCC7 Sep 1, 2026
55d3289
fix(dataprep): update isolated runtime import
TomCC7 Sep 1, 2026
7983c51
refactor(imitation): simplify native collection dataprep
TomCC7 Sep 1, 2026
2b34ce6
spec: remove
TomCC7 Sep 2, 2026
b401780
refactor(collection): establish profiles and portable recording direc…
TomCC7 Sep 11, 2026
c51fd3e
refactor(dataprep): separate source interpretation from feature align…
TomCC7 Sep 15, 2026
f28fe7f
test(collection): preserve source kinds in saved profiles
TomCC7 Sep 15, 2026
c33e61f
fix(collection): align native message pins and gate CI retries
TomCC7 Sep 15, 2026
df75811
ci: run collection interoperability with the native Rust tests
TomCC7 Sep 15, 2026
71ad4e5
fix(collection): resolve isolated runtime from shared checkout
TomCC7 Sep 17, 2026
b839103
refactor(collection): own applied-command recording in collection layer
TomCC7 Sep 18, 2026
e3aa4da
test(collection): include applied commands in coordinator outputs
TomCC7 Sep 18, 2026
7bfaeaa
fix(collection): simplify integration and restore native CI discovery
TomCC7 Sep 18, 2026
3c087ac
refactor(collection): replace Python recorder with RustRecorder subclass
TomCC7 Sep 18, 2026
46ced17
test(collection): exclude readiness probes from episode counts
TomCC7 Sep 18, 2026
8b64f4a
Update dimos/control/test_control.py
TomCC7 Sep 18, 2026
8fa41b2
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 18, 2026
b8f9bf6
fix(control): publish accepted commands from the base coordinator
TomCC7 Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Comment thread
TomCC7 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,10 @@ jobs:
with:
enable-cache: true
prune-cache: true
- name: Install dependency for pyaudio
- name: Install Python audio and image dependencies
run: |
sudo apt-get update
sudo apt-get install -y portaudio19-dev
sudo apt-get install -y portaudio19-dev libturbojpeg
- name: Build and test PyO3 bindings
# PyO3 extension modules (cdylib + `extension-module`) are the one case
# that still needs a plain build: pytest imports the compiled .so.
Expand All @@ -352,7 +352,10 @@ jobs:
- name: Native module e2e tests
run: |
cargo build --release --locked -p dimos-livox -p dimos-virtual-mid360
uv run pytest -m native_e2e dimos/hardware/sensors/lidar/livox/test_e2e.py --no-cov
uv run pytest -m native_e2e \
dimos/hardware/sensors/lidar/livox/test_e2e.py \
dimos/robot/manipulators/openyam/blueprints/test_learning_collection_e2e.py \
dimos/imitation/test_datacollection_e2e.py --no-cov

native:
name: Native builds (C++ and Rust)
Expand Down
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 6 additions & 10 deletions dimos/agents/demo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,21 @@
from dimos.agents.skills.observe_skill import ObserveSkill
from dimos.core.coordination.blueprints import autoconnect
from dimos.hardware.sensors.camera.module import CameraModule
from dimos.hardware.sensors.camera.webcam import Webcam
from dimos.hardware.sensors.camera.webcam import WebcamConfig
from dimos.hardware.sensors.camera.zed import compat as zed

demo_agent = autoconnect(McpServer.blueprint(), McpClient.blueprint())


def _create_webcam() -> Webcam:
return Webcam(
camera_index=0,
fps=15,
camera_info=zed.CameraInfo.SingleWebcam,
)


demo_agent_camera = autoconnect(
McpServer.blueprint(),
McpClient.blueprint(),
ObserveSkill.blueprint(),
CameraModule.blueprint(
hardware=_create_webcam,
hardware=WebcamConfig(
camera_index=0,
fps=15,
camera_info=zed.CameraInfo.SingleWebcam,
),
),
)
52 changes: 0 additions & 52 deletions dimos/cli/commands/dataprep.py

This file was deleted.

2 changes: 0 additions & 2 deletions dimos/cli/dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
from dimos.cli.commands.bake import bake
from dimos.cli.commands.cameracalibrate import cameracalibrate
from dimos.cli.commands.data import data_app
from dimos.cli.commands.dataprep import dataprep_app
from dimos.cli.commands.docs import docs
from dimos.cli.commands.global_options import create_dynamic_callback
from dimos.cli.commands.graph import graph
Expand Down Expand Up @@ -143,7 +142,6 @@ def cli_main() -> None:
from dimos.navigation.nav_3d.evaluator.cli import app as nav_eval_app

main.add_typer(nav_eval_app, name="nav-eval")
main.add_typer(dataprep_app, name="dataprep")

from dimos.memory.cli.app import mem_app

Expand Down
3 changes: 3 additions & 0 deletions dimos/control/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ class ControlCoordinator(Module):

# Output: Aggregated joint state for external consumers
coordinator_joint_state: Out[JointState]
# Sparse position updates accepted by hardware, for diagnostics and recording.
applied_joint_position_command: Out[JointState]

# Input: Streaming joint commands for real-time control
joint_command: In[JointState]
Expand Down Expand Up @@ -903,6 +905,7 @@ def start(self) -> None:
task_lock=self._task_lock,
joint_to_hardware=self._joint_to_hardware,
publish_callback=publish_cb,
publish_command_callback=self.applied_joint_position_command.publish,
publish_robot_callback=publish_robot_cb,
frame_id=self.config.joint_state_frame_id,
log_ticks=self.config.log_ticks,
Expand Down
95 changes: 92 additions & 3 deletions dimos/control/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,9 @@ def test_unready_whole_body_is_excluded_from_read_and_write(self, mocker):

state, per_hardware = tick_loop._read_all_hardware()
imu = tick_loop._read_all_imu()
tick_loop._write_all_hardware({"g1": ({"g1/joint1": 0.25}, ControlMode.SERVO_POSITION)})
tick_loop._write_all_hardware(
{"g1": ({"g1/joint1": 0.25}, ControlMode.SERVO_POSITION)}, timestamp=123.0
)

assert state.joint_positions == {}
assert per_hardware == {}
Expand Down Expand Up @@ -1163,7 +1165,9 @@ def test_ready_whole_body_reads_and_writes(self):
)

state, _per_hardware = tick_loop._read_all_hardware()
tick_loop._write_all_hardware({"g1": ({"g1/joint1": 0.25}, ControlMode.SERVO_POSITION)})
tick_loop._write_all_hardware(
{"g1": ({"g1/joint1": 0.25}, ControlMode.SERVO_POSITION)}, timestamp=123.0
)

assert state.joint_positions == {"g1/joint1": 0.5}
adapter.write_motor_commands.assert_called_once()
Expand Down Expand Up @@ -1296,12 +1300,97 @@ def test_write_all_hardware_rejected_command_logs_error(self, mocker):
joint_to_hardware={"arm/joint1": "arm"},
)

tick_loop._write_all_hardware({"arm": ({"arm/joint1": 0.25}, ControlMode.SERVO_POSITION)})
tick_loop._write_all_hardware(
{"arm": ({"arm/joint1": 0.25}, ControlMode.SERVO_POSITION)}, timestamp=123.0
)

log_error.assert_called_once_with(
"Hardware arm rejected SERVO_POSITION command from control task"
)

@pytest.mark.parametrize(
"mode,ready,accepted,error,expected",
[
(ControlMode.POSITION, True, True, None, [0.25]),
(ControlMode.SERVO_POSITION, True, True, None, [0.25]),
(ControlMode.VELOCITY, True, True, None, []),
(ControlMode.TORQUE, True, True, None, []),
(ControlMode.POSITION, False, True, None, []),
(ControlMode.POSITION, True, False, None, []),
(ControlMode.POSITION, True, True, RuntimeError("write failed"), []),
],
)
def test_command_feedback_reports_only_successful_position_writes(
self, mocker, mode, ready, accepted, error, expected
):
hardware = mocker.Mock(spec=ConnectedHardware)
hardware.ready_for_control.return_value = ready
hardware.write_command.return_value = accepted
hardware.write_command.side_effect = error
received = []
lock = threading.Lock()

def receive(message):
assert not lock.locked()
received.append(message)

tick_loop = TickLoop(
tick_rate=100.0,
hardware={"arm": hardware},
hardware_lock=lock,
tasks={},
task_lock=threading.Lock(),
joint_to_hardware={},
publish_command_callback=receive,
frame_id="robot",
)
tick_loop._write_all_hardware({"arm": ({"arm/joint1": 0.25}, mode)}, timestamp=123.0)

assert [value for msg in received for value in msg.position] == expected
assert [msg.name for msg in received] == ([["arm/joint1"]] if expected else [])
assert [(msg.ts, msg.frame_id) for msg in received] == (
[(123.0, "robot")] if expected else []
)
assert hardware.write_command.call_count == int(ready)

def test_command_feedback_combines_successful_writes_and_keeps_sparse_updates(self, mocker):
hardware = {
name: mocker.Mock(spec=ConnectedHardware) for name in ("left", "right", "failed")
}
for hw in hardware.values():
hw.ready_for_control.return_value = True
hw.write_command.return_value = True
hardware["failed"].write_command.return_value = False
received = []
tick_loop = TickLoop(
tick_rate=100.0,
hardware=hardware,
hardware_lock=threading.Lock(),
tasks={},
task_lock=threading.Lock(),
joint_to_hardware={},
publish_command_callback=received.append,
)
tick_loop._write_all_hardware(
{
"left": ({"left/joint1": 0.25}, ControlMode.POSITION),
"failed": ({"failed/joint1": 0.75}, ControlMode.POSITION),
"right": ({"right/gripper": 0.5}, ControlMode.SERVO_POSITION),
},
timestamp=123.0,
)
tick_loop._write_all_hardware(
{
"right": ({"right/gripper": 0.6}, ControlMode.SERVO_POSITION),
},
timestamp=124.0,
)

assert [(msg.name, msg.position) for msg in received] == [
(["left/joint1", "right/gripper"], [0.25, 0.5]),
(["right/gripper"], [0.6]),
]


class TestIntegration:
def test_full_trajectory_execution(self, mock_adapter, wait_until):
Expand Down
5 changes: 4 additions & 1 deletion dimos/control/test_per_robot_joint_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ def test_plain_coordinator_needs_no_per_robot_ports(self, make_coordinator):
coordinator.start()

assert sorted(coordinator.list_hardware()) == ["left_arm", "right_arm"]
assert sorted(coordinator.outputs) == ["coordinator_joint_state"]
assert sorted(coordinator.outputs) == [
"applied_joint_position_command",
"coordinator_joint_state",
]


class TestPerRobotPublishing:
Expand Down
Loading
Loading