Skip to content

Booster#2438

Open
AlexWUrobot wants to merge 65 commits intomainfrom
booster
Open

Booster#2438
AlexWUrobot wants to merge 65 commits intomainfrom
booster

Conversation

@AlexWUrobot
Copy link

Overview

[Provide a brief overview of the changes in this pull request.]
(If applicable, linked issue: [ ])

Type of change

  • Bug fix
  • New feature
  • Documentation improvement
  • Bounty issue submission
  • Other:

Changes

[Detail the changes you have made in this pull request. Include any new features, bug fixes, or improvements.]

Checklist

  • Code complies with style guidelines
  • Self-review completed
  • Documentation updated (if applicable)
  • Local testing completed
  • Tests added/updated (if applicable)

Impact

[Explain the impact of your changes. Include any potential risks or side effects that reviewers should be aware of.]

Additional Information

[Include any additional information that may be relevant to reviewers. This could include links to related issues or pull requests, references to documentation, or other context that may help reviewers understand your changes.]

AlexWUrobot and others added 30 commits February 4, 2026 12:49
Introduce UnitreeGo2- and TurtleBot4-specific odom/rplidar inputs and backgrounds and switch configs to use them. Many config JSON5 files updated to replace generic "Odom"/"RPLidar" types with UnitreeGo2 or TurtleBot4 variants (including renaming fabric_gps -> unitree_go2_fabric_gps and GPS reader to UnitreeGo2GPSOdomReader). Action connectors and background code imports were adjusted to use providers.unitree_go2_* and providers.turtlebot4_* implementations; RPLidar and odom background/input plugins were renamed or added (unitree_go2_rplidar, unitree_go2_odom, turtlebot4_odom, turtlebot4_rplidar). Several legacy test config files and the old generic odom background were removed. Tests and provider references were updated/renamed accordingly to align with the new provider/plugin names.
Refactor test patches to match renamed plugin/provider modules and classes with the unitree_go2_* prefix. Updated patch targets for IOProvider, OdomProvider/UnitreeGo2OdomProvider, RPLidarProvider/UnitreeGo2RPLidarProvider and related time/asyncio imports, adjusted assertion expectations (e.g. add_input source name and provider init args). These changes keep tests aligned with the refactored module and class names.
Rename mock and update references to match the UnitreeGo2RPLidar plugin. Renamed tests/integration/mock_inputs/mock_rplidar.py -> mock_unitree_go2_rplidar.py and class MockRPLidar -> MockUnitreeGo2RPLidar; updated tests/integration/mock_inputs/input_registry.py to import the new mock, register the mock under the "UnitreeGo2RPLidar" key, adjust mock module mappings, and update the unregister list; updated tests/integration/data/test_cases/rplidar_test.json5 to use type "UnitreeGo2RPLidar". This keeps mock names consistent with the real plugin identifier.
Remove Zenoh-related settings from the mock Unitree Go2 RPLidar input: drop the `use_zenoh` flag from the lidar config and remove the conditional handling that added `URID` and logging. This simplifies the test mock by eliminating Zenoh-specific configuration and behavior.
Remove the deprecated simple_paths option from RPLidar configs and providers (TurtleBot4 and Unitree Go2). Simplified path selection logic to always use the single default path and removed related config fields (use_zenoh, URID, machine_type where applicable). Update MoveZenoh connector to import and instantiate the TurtleBot4 RPLidar provider. Rename/cleanup the turtlebot4_odom background and add comprehensive unit tests for TurtleBot4 and Unitree odom and RPLidar backgrounds and inputs.
Add a comprehensive test suite for the TurtleBot4 RPLidar plugin. Tests cover RPLidarConfig defaults and custom values, TurtleBot4RPLidar initialization and provider parameter passing, async polling (_poll) with and without data, _raw_to_text and raw_to_text buffering behavior, formatted_latest_buffer (including buffer clearing and IOProvider interaction), and _extract_lidar_config. Uses unittest.mock and pytest (including async tests).
Add comprehensive unit tests for OdomProviderBase, TurtleBot4OdomProvider, and TurtleBot4RPLidarProvider. New test modules mock multiprocessing/threading and external dependencies (Zenoh, D435, sensor messages) and cover initialization, singleton behavior, start/stop, odometry processing (including quaternion->euler/yaw conversions), file logging, zenoh scan handling, path processing, and RPLidar config. Also remove an unused debug logging line from turtlebot4_rplidar_provider.py to tidy the implementation.
Fix incorrect tuple unpacking in tests/providers/test_turtlebot4_odom_provider.py so mock_process is assigned from the correct position in the mock_multiprocessing fixture. This ensures the test uses the intended mock process object when verifying that logging config is passed to the processor.
Clean up tests/providers/test_turtlebot4_rplidar_provider.py by removing redundant inline comments that described obvious assertions (subscriber declaration and path/angle checks). No functional changes—only test file comment cleanup to reduce noise.
Introduce a new UnitreeG1OdomProvider implementing OdomProviderBase to retrieve odometry/pose from Unitree G1 robots via CycloneDDS. Adds g1_odom_processor (runs in a separate process) which initializes the CycloneDDS channel, subscribes to rt/utlidar/robot_pose (PoseStamped_) and pushes messages into a multiprocessing queue; logging is configurable via existing logging helpers. The provider is a singleton that starts a multiprocessing subscriber and a local thread to process queue data, and includes _update_body_state to derive body height (cm) and RobotState (STANDING/SITTING). Optional imports are guarded with a warning if the Unitree SDK or CycloneDDS are not available. Requires unitree_sdk2py/CycloneDDS to function at runtime.
- Updated unitree_g1_autonomy.json5 to change the robot name from "Bits" to "Iris" and modified the system prompt accordingly.
- Introduced UnitreeG1Odom background plugin for odometry data handling.
- Added TurtleBot4Battery plugin for battery status monitoring with Zenoh integration.
- Implemented UnitreeGo2Battery plugin for battery status with error handling for SDK absence.
- Enhanced unit tests for TurtleBot4Battery and UnitreeGo2Battery plugins to ensure proper functionality and error handling.
Copilot AI review requested due to automatic review settings March 5, 2026 22:20
@AlexWUrobot AlexWUrobot requested review from a team as code owners March 5, 2026 22:20
@github-actions github-actions bot added robotics Robotics code changes python Python code tests Test files config Configuration files labels Mar 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Booster (K1) Zenoh/ROS2 bridge support and associated tooling to send movement RPC commands and consume odometry via a new Odometer message, plus configs/scripts to exercise the integration.

Changes:

  • Introduce Booster IDL message wrappers (Odometer, RPC request/response, RemoteControllerState) and export them via zenoh_msgs.
  • Add a K1 odometry provider and Booster autonomy action connector that calls movement via Zenoh query/reply to a ROS2 service bridge.
  • Add hardware-test scripts and new/updated configs for Booster autonomy + Zenoh diagnostics.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
system_hw_test/zenoh_echo.py Utility to subscribe/pretty-print Zenoh topics with basic deserialization.
system_hw_test/test_booster_zenoh_odom_sub.py Minimal subscriber script to verify odom topic traffic.
system_hw_test/test_booster_move_zenoh_service.py Test client for sending movement RPC commands via Zenoh query/reply.
system_hw_test/test_booster_move.py Test client for sending direct remote-controller-style movement messages.
system_hw_test/booster_zenoh_ros2_bridge.py ROS2↔Zenoh bridge for odom/paths topics and RPC service query handling.
system_hw_test/booster_zenoh_mock.py Mock Zenoh query responder for booster RPC service testing.
src/zenoh_msgs/idl/booster_interface.py Adds Booster-specific IDL structs (odometer + RPC wrappers + controller state).
src/zenoh_msgs/idl/init.py Re-exports Booster IDL types from zenoh_msgs.idl.
src/zenoh_msgs/init.py Re-exports Booster IDL types at top-level zenoh_msgs.
src/providers/k1_odom_provider.py New Zenoh-based odom provider that publishes OdomProviderBase.position.
src/inputs/plugins/unitree_go2_battery.py Adjusts Go2 battery subscriber topic/type (currently broken).
src/inputs/plugins/booster_odom.py New input plugin that surfaces Booster odom/movement status to the fuser.
src/actions/move_k1_autonomy/interface.py Adds Booster movement action interface (LLM-facing enum + IO dataclasses).
src/actions/move_k1_autonomy/connector/k1_sdk.py Implements movement connector using Zenoh RPC service + odom gating/safety logic.
config/greeting_local.json5 Updates greeting local config (currently contains merge-conflict markers).
config/greeting.json5 Changes greeting config globals (switches away from env interpolation).
config/booster_autonomy.json5 New autonomy config for Booster using BoosterOdom + move_k1_autonomy.

Comment on lines +11 to +17
<<<<<<< HEAD
api_key: "openmind_free",
unitree_ethernet: "",
=======
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-enP2p1s0}",
>>>>>>> origin/main
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

This file contains unresolved git merge-conflict markers (<<<<<<<, =======, >>>>>>>), which makes the JSON5 invalid and will break config loading. Resolve the conflict and keep a single intended value for api_key and unitree_ethernet.

Suggested change
<<<<<<< HEAD
api_key: "openmind_free",
unitree_ethernet: "",
=======
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-enP2p1s0}",
>>>>>>> origin/main
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-enP2p1s0}",

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +12
api_key: "openmind_free",
unitree_ethernet: "",
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

api_key and unitree_ethernet were changed from env-interpolated values to hard-coded strings. This breaks the established pattern used across other configs (e.g., config/conversation.json5) and makes it harder to run in different environments without editing tracked files; consider restoring ${OM_API_KEY:-...} / ${UNITREE_ETHERNET:-...} here (and keeping hard-coded defaults only in *_local configs if needed).

Suggested change
api_key: "openmind_free",
unitree_ethernet: "",
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-}",

Copilot uses AI. Check for mistakes.
replies = self.session.get(
self.service_name,
payload=request_payload,
timeout=5.0,
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The timeout parameter on _call_service() is ignored: session.get(..., timeout=5.0) uses a hard-coded value instead of the function argument. Use the timeout parameter (or remove it) so callers can control service-call timeouts consistently.

Suggested change
timeout=5.0,
timeout=timeout,

Copilot uses AI. Check for mistakes.

try:
self.lowstate_subscriber = ChannelSubscriber("rt/lowstate", LowState_) # type: ignore
self.lowstate_subscriber = ChannelSubscriber("rt/_BmsState_", BmsState_) # type: ignore
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

BmsState_ is referenced when creating the ChannelSubscriber, but it is never imported or defined (including in the ImportError fallback). This will raise a NameError at runtime; if the intent is to switch from LowState_ to a BMS-only message, update the imports/fallback stubs and ensure LowStateMessageHandler matches the subscribed message type.

Suggested change
self.lowstate_subscriber = ChannelSubscriber("rt/_BmsState_", BmsState_) # type: ignore
self.lowstate_subscriber = ChannelSubscriber("rt/_BmsState_", LowState_) # type: ignore

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Mar 5, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
3144 2 3142 0
View the top 2 failed test(s) by shortest run time
tests/config/schema_test.py::test_json_file_valid[/home/runner/work/OM1/OM1/tests/config/../../config/greeting_local.json5]
Stack Traces | 0.007s run time
json_file = '.../tests/config/../../config/greeting_local.json5'
single_mode_schema = {'$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': True, 'properties': {'URID': {'type': '...rray'}, ...}, 'required': ['version', 'hertz', 'name', 'api_key', 'system_prompt_base', 'system_governance', ...], ...}
multi_mode_schema = {'$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'definitions': {'cortex_llm': {'...tions/cortex_llm'}, 'default_mode': {'description': 'The mode to initialize on startup.', 'type': 'string'}, ...}, ...}

    @pytest.mark.parametrize("json_file", get_all_json_files())
    def test_json_file_valid(json_file, single_mode_schema, multi_mode_schema):
        with open(json_file) as f:
>           data = json5.load(f)

tests/config/schema_test.py:39: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10....../site-packages/json5/lib.py:99: in load
    return loads(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

s = '{\n  // Configuration version\n  version: "v1.0.3",\n\n  // Mode system configuration for Unitree Go2\n  default_mode...itions: { greeting_conversation_finished: true },\n      priority: 0,\n      cooldown_seconds: 5.0,\n    },\n  ],\n}\n'

    def loads(
        s: str,
        *,
        encoding: Optional[str] = None,
        cls: Any = None,
        object_hook: Optional[Callable[[Mapping[str, Any]], Any]] = None,
        parse_float: Optional[Callable[[str], Any]] = None,
        parse_int: Optional[Callable[[str], Any]] = None,
        parse_constant: Optional[Callable[[str], Any]] = None,
        strict: bool = True,
        object_pairs_hook: Optional[
            Callable[[Iterable[Tuple[str, Any]]], Any]
        ] = None,
        allow_duplicate_keys: bool = True,
    ):
        """Deserialize ``s`` (a string containing a JSON5 document) to a Python
        object.
    
        Supports the same arguments as ``json.load()`` except that:
            - the `cls` keyword is ignored.
            - an extra `allow_duplicate_keys` parameter supports checking for
              duplicate keys in a object; by default, this is True for
              compatibility with ``json.load()``, but if set to False and
              the object contains duplicate keys, a ValueError will be raised.
        """
    
        assert cls is None, 'Custom decoders are not supported'
    
        if isinstance(s, bytes):
            encoding = encoding or 'utf-8'
            s = s.decode(encoding)
    
        if not s:
            raise ValueError('Empty strings are not legal JSON5')
        parser = Parser(s, '<string>')
        ast, err, _ = parser.parse(global_vars={'_strict': strict})
        if err:
>           raise ValueError(err)
E           ValueError: <string>:11 Unexpected "<" at column 2

.venv/lib/python3.10....../site-packages/json5/lib.py:150: ValueError
tests/config/test_config.py::test_configs
Stack Traces | 0.48s run time
def test_configs():
        """Test that all config files can be loaded."""
        config_folder_path = os.path.join(os.path.dirname(__file__), "../../config")
        files_names = [
            entry.name for entry in os.scandir(config_folder_path) if entry.is_file()
        ]
    
        for file_name in files_names:
            if file_name.endswith(".DS_Store"):
                continue
            assert file_name.endswith(".json5")
            with open(os.path.join(config_folder_path, file_name), "r") as f:
>               raw_config = json5.load(f)

tests/config/test_config.py:26: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10....../site-packages/json5/lib.py:99: in load
    return loads(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

s = '{\n  // Configuration version\n  version: "v1.0.3",\n\n  // Mode system configuration for Unitree Go2\n  default_mode...itions: { greeting_conversation_finished: true },\n      priority: 0,\n      cooldown_seconds: 5.0,\n    },\n  ],\n}\n'

    def loads(
        s: str,
        *,
        encoding: Optional[str] = None,
        cls: Any = None,
        object_hook: Optional[Callable[[Mapping[str, Any]], Any]] = None,
        parse_float: Optional[Callable[[str], Any]] = None,
        parse_int: Optional[Callable[[str], Any]] = None,
        parse_constant: Optional[Callable[[str], Any]] = None,
        strict: bool = True,
        object_pairs_hook: Optional[
            Callable[[Iterable[Tuple[str, Any]]], Any]
        ] = None,
        allow_duplicate_keys: bool = True,
    ):
        """Deserialize ``s`` (a string containing a JSON5 document) to a Python
        object.
    
        Supports the same arguments as ``json.load()`` except that:
            - the `cls` keyword is ignored.
            - an extra `allow_duplicate_keys` parameter supports checking for
              duplicate keys in a object; by default, this is True for
              compatibility with ``json.load()``, but if set to False and
              the object contains duplicate keys, a ValueError will be raised.
        """
    
        assert cls is None, 'Custom decoders are not supported'
    
        if isinstance(s, bytes):
            encoding = encoding or 'utf-8'
            s = s.decode(encoding)
    
        if not s:
            raise ValueError('Empty strings are not legal JSON5')
        parser = Parser(s, '<string>')
        ast, err, _ = parser.parse(global_vars={'_strict': strict})
        if err:
>           raise ValueError(err)
E           ValueError: <string>:11 Unexpected "<" at column 2

.venv/lib/python3.10....../site-packages/json5/lib.py:150: ValueError

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

Comment on lines -11 to +12
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-enP2p1s0}",
api_key: "openmind_free",
unitree_ethernet: "",
Copy link
Contributor

Choose a reason for hiding this comment

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

Please restore these changes.

Comment on lines +11 to +17
<<<<<<< HEAD
api_key: "openmind_free",
unitree_ethernet: "",
=======
api_key: "${OM_API_KEY:-openmind_free}",
unitree_ethernet: "${UNITREE_ETHERNET:-enP2p1s0}",
>>>>>>> origin/main
Copy link
Contributor

Choose a reason for hiding this comment

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

Please solve the conflicts.

Comment on lines +24 to +28
odom_topic : str
Zenoh topic for odometry data.
rpc_service_name : str
Zenoh key for the ROS2 RPC service (request/reply via session.get).
Defaults to "booster_rpc_service".
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix the docstring.

Comment on lines +40 to +50
# Backward-compat: older configs used cmd_vel_topic for topic-based control.
# If provided, we treat it as the RPC service name.
cmd_vel_topic: Optional[str] = Field(
default=None,
description="DEPRECATED. Previously used for remote_controller_state topic; now interpreted as rpc_service_name.",
)

allow_move_without_odom: bool = Field(
default=False,
description="TESTING ONLY. If true, bypass odom/body-attitude gating and send movement RPC commands even when odom is missing.",
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove it if you don't need it

Comment on lines +150 to +157
import json

from zenoh_msgs import (
BoosterApiReqMsg,
RpcServiceRequest,
RpcServiceResponse,
)

Copy link
Contributor

Choose a reason for hiding this comment

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

Please put all import to the top.


try:
self.lowstate_subscriber = ChannelSubscriber("rt/lowstate", LowState_) # type: ignore
self.lowstate_subscriber = ChannelSubscriber("rt/_BmsState_", BmsState_) # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Please restore it

Comment on lines +87 to +92
Parameters
----------
topic : str
The Zenoh topic to subscribe to for odometry data.
Defaults to "odometer_state".
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove it.

Comment on lines +1 to +2
"""Booster Interface Messages."""

Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration files python Python code robotics Robotics code changes tests Test files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants