-
Notifications
You must be signed in to change notification settings - Fork 804
refactor(manipulation): localize device connection configuration #4007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
fe7a7de
spec: openspec init
TomCC7 76158b2
chore: revert change to doc folder
TomCC7 35c8b14
Merge branch 'main' into cc/feat/openspec
TomCC7 12d4346
Merge branch 'main' into cc/feat/openspec
TomCC7 6cd2fd3
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 45f7f73
Merge branch 'main' into cc/feat/openspec
TomCC7 86a600d
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 43fd853
Merge branch 'main' into cc/feat/openspec
TomCC7 8394a61
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 bae46c4
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 4cf815e
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 2c80dab
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 bc381cb
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 3a976da
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 4e25297
add mattskill
TomCC7 11f0d7f
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 9ffcd58
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 f873ddf
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 e87e93e
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 e689348
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 794e585
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 cfa3e3a
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 d221a4f
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 df82a32
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 9cac56d
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 f379a95
Merge remote-tracking branch 'origin/main' into cc/chore/manip-global…
TomCC7 1339b27
refactor(manipulation): localize device connection configuration
TomCC7 2574212
spec: remove
TomCC7 3a122cb
refactor(control): compose typed hardware connection backends
TomCC7 871d1f3
refactor(manipulation): simplify local connection configuration
TomCC7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # Copyright 2026 Dimensional Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Typed connection backends; importing schemas never loads robot implementations.""" | ||
|
|
||
| from typing import Annotated, Literal, Self | ||
|
|
||
| from pydantic import AfterValidator, Field, model_validator | ||
|
|
||
| from dimos.protocol.service.spec import BaseConfig | ||
|
|
||
|
|
||
| def validate_address(value: str | None) -> str | None: | ||
| if value is not None and (not value.strip() or value != value.strip()): | ||
| raise ValueError("Device address must be nonempty and have no surrounding whitespace") | ||
| return value | ||
|
|
||
|
|
||
| OptionalDeviceAddress = Annotated[str | None, AfterValidator(validate_address)] | ||
|
|
||
|
|
||
| class SingleAddressConfig(BaseConfig): | ||
| address: OptionalDeviceAddress = None | ||
|
|
||
|
|
||
| class XArmConnectionConfig(SingleAddressConfig): | ||
| # The hardware-only variant preserves blueprints without simulator wiring. | ||
| backend: Literal["xarm", "xarm_hardware"] = "xarm" | ||
| dof: Literal[6, 7] = 7 | ||
|
|
||
|
|
||
| class PiperConnectionConfig(SingleAddressConfig): | ||
| backend: Literal["piper", "piper_hardware"] = "piper" | ||
|
|
||
|
|
||
| class A1ZConnectionConfig(SingleAddressConfig): | ||
| backend: Literal["a1z"] = "a1z" | ||
|
|
||
|
|
||
| class A750ConnectionConfig(SingleAddressConfig): | ||
| backend: Literal["a750"] = "a750" | ||
|
|
||
|
|
||
| class OpenYamConnectionConfig(SingleAddressConfig): | ||
| backend: Literal["openyam"] = "openyam" | ||
|
|
||
|
|
||
| class DualXArmConnectionConfig(BaseConfig): | ||
| backend: Literal["dual_xarm"] = "dual_xarm" | ||
| left_address: OptionalDeviceAddress = None | ||
| right_address: OptionalDeviceAddress = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def complete_pair(self) -> Self: | ||
| if (self.left_address is None) != (self.right_address is None): | ||
| raise ValueError("Supply both left and right addresses, or neither for mock hardware") | ||
| return self | ||
|
|
||
|
|
||
| class PairedCanConfig(BaseConfig): | ||
| left_can_port: OptionalDeviceAddress = None | ||
| right_can_port: OptionalDeviceAddress = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def complete_pair(self) -> Self: | ||
| if (self.left_can_port is None) != (self.right_can_port is None): | ||
| raise ValueError("Supply both left and right CAN ports, or neither for mock hardware") | ||
| if self.left_can_port is not None and self.left_can_port == self.right_can_port: | ||
| raise ValueError("Left and right CAN ports must be distinct") | ||
| return self | ||
|
|
||
|
|
||
| class OpenArmConnectionConfig(PairedCanConfig): | ||
| backend: Literal["openarm"] = "openarm" | ||
|
|
||
|
|
||
| class DualOpenYamConnectionConfig(PairedCanConfig): | ||
| backend: Literal["dual_openyam"] = "dual_openyam" | ||
|
|
||
|
|
||
| class MixedArmConnectionConfig(BaseConfig): | ||
| backend: Literal["xarm_piper"] = "xarm_piper" | ||
| xarm_address: OptionalDeviceAddress = None | ||
| piper_address: OptionalDeviceAddress = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def complete_pair(self) -> Self: | ||
| if (self.xarm_address is None) != (self.piper_address is None): | ||
| raise ValueError("Supply both xArm and Piper addresses, or neither for mock hardware") | ||
| return self | ||
|
|
||
|
|
||
| HardwareConnectionConfig = Annotated[ | ||
| XArmConnectionConfig | ||
| | PiperConnectionConfig | ||
| | A1ZConnectionConfig | ||
| | A750ConnectionConfig | ||
| | OpenYamConnectionConfig | ||
| | DualXArmConnectionConfig | ||
| | OpenArmConnectionConfig | ||
| | DualOpenYamConnectionConfig | ||
| | MixedArmConnectionConfig, | ||
| Field(discriminator="backend"), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Copyright 2026 Dimensional Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Resolve connection descriptions without opening hardware. | ||
|
|
||
| Robot factories are imported on demand, just like manipulation backends. | ||
| """ | ||
|
|
||
| from dataclasses import replace | ||
| from importlib import import_module | ||
| from typing import Any | ||
|
|
||
| from dimos.control.components import HardwareComponent | ||
| from dimos.control.connection import ( | ||
| A1ZConnectionConfig, | ||
| A750ConnectionConfig, | ||
| DualOpenYamConnectionConfig, | ||
| DualXArmConnectionConfig, | ||
| HardwareConnectionConfig, | ||
| MixedArmConnectionConfig, | ||
| OpenArmConnectionConfig, | ||
| OpenYamConnectionConfig, | ||
| PiperConnectionConfig, | ||
| XArmConnectionConfig, | ||
| ) | ||
| from dimos.hardware.whole_body.damiao.config import DamiaoRuntimeConfig | ||
|
|
||
|
|
||
| def _hardware(robot: str, factory: str, **kwargs: Any) -> HardwareComponent: | ||
| module = import_module(f"dimos.robot.manipulators.{robot}.config") | ||
| result: HardwareComponent = getattr(module, factory)(**kwargs) | ||
| return result | ||
|
|
||
|
|
||
| def _resolved_component( | ||
| component: HardwareComponent, resolved: HardwareComponent | ||
| ) -> HardwareComponent: | ||
| adapter_kwargs = {**resolved.adapter_kwargs, **component.adapter_kwargs} | ||
| runtime = resolved.adapter_kwargs.get("runtime_config") | ||
| if isinstance(runtime, DamiaoRuntimeConfig): | ||
| configured = component.adapter_kwargs.get("runtime_config", runtime) | ||
| adapter_kwargs["runtime_config"] = replace(configured, bus_devices=runtime.bus_devices) | ||
| return replace( | ||
| component, | ||
| adapter_type=resolved.adapter_type, | ||
| address=resolved.address, | ||
| limits=component.limits if resolved.adapter_type in ("mock", "mock_whole_body") else None, | ||
| adapter_kwargs=adapter_kwargs, | ||
| ) | ||
|
|
||
|
|
||
| def _xarm( | ||
| component: HardwareComponent, dof: int, address: str | None, simulation: str | ||
| ) -> HardwareComponent: | ||
| resolved = _hardware("xarm", f"xarm{dof}_hardware", address=address, simulation=simulation) | ||
| # Keep the blueprint's gripper and other adapter options. | ||
| return replace( | ||
| _resolved_component(component, resolved), | ||
| adapter_kwargs={**component.adapter_kwargs, "arm_dof": dof}, | ||
| ) | ||
|
|
||
|
|
||
| def resolve_connection( | ||
| config: HardwareConnectionConfig, | ||
| hardware: list[HardwareComponent], | ||
| simulation: str, | ||
| ) -> list[HardwareComponent]: | ||
| """Select concrete hardware while preserving blueprint-owned descriptions.""" | ||
| if isinstance(config, (OpenArmConnectionConfig, DualOpenYamConnectionConfig)): | ||
| if len(hardware) > 1: | ||
| raise ValueError("Coupled connection requires at most one hardware description") | ||
| robot = config.backend | ||
| resolved = _hardware( | ||
| robot, | ||
| f"{robot}_hardware", | ||
| left_can_port=config.left_can_port, | ||
| right_can_port=config.right_can_port, | ||
| ) | ||
| return [_resolved_component(hardware[0], resolved)] if hardware else [resolved] | ||
|
|
||
| if isinstance(config, (DualXArmConnectionConfig, MixedArmConnectionConfig)): | ||
| if len(hardware) != 2: | ||
| raise ValueError("Dual-arm connection requires exactly two hardware descriptions") | ||
| left, right = hardware | ||
| if isinstance(config, DualXArmConnectionConfig): | ||
| return [ | ||
| _xarm(left, 7, config.left_address, simulation), | ||
| _xarm(right, 6, config.right_address, simulation), | ||
| ] | ||
| return [ | ||
| _xarm(left, 6, config.xarm_address, ""), | ||
| _resolved_component( | ||
| right, _hardware("piper", "piper_hardware", address=config.piper_address) | ||
| ), | ||
| ] | ||
|
|
||
| if len(hardware) != 1: | ||
| raise ValueError("Single-arm connection requires exactly one hardware description") | ||
| component = hardware[0] | ||
| if isinstance(config, XArmConnectionConfig): | ||
| return [ | ||
| _xarm( | ||
| component, | ||
| config.dof, | ||
| config.address, | ||
| simulation if config.backend == "xarm" else "", | ||
| ) | ||
| ] | ||
| if isinstance(config, PiperConnectionConfig): | ||
| resolved = _hardware( | ||
| "piper", | ||
| "piper_hardware", | ||
| address=config.address, | ||
| simulation=simulation if config.backend == "piper" else "", | ||
| ) | ||
| elif isinstance(config, A1ZConnectionConfig): | ||
| # A1Z's blueprint already owns the gripper/dynamics configuration. | ||
| return [ | ||
| replace( | ||
| component, | ||
| adapter_type="galaxea_a1z" | ||
| if config.address is not None and not simulation | ||
| else "mock", | ||
| address=config.address if not simulation else None, | ||
| limits=component.limits if simulation or config.address is None else None, | ||
| ) | ||
| ] | ||
| elif isinstance(config, A750ConnectionConfig): | ||
| resolved = _hardware("a750", "a750_hardware", address=config.address) | ||
| elif isinstance(config, OpenYamConnectionConfig): | ||
| resolved = _hardware( | ||
| "openyam", "openyam_hardware", address=config.address, simulation=simulation | ||
| ) | ||
| else: | ||
| raise TypeError(f"Unsupported connection config: {type(config).__name__}") | ||
| return [_resolved_component(component, resolved)] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.