From e027c58f676ee8c443addb37a7a0626a233a6a80 Mon Sep 17 00:00:00 2001 From: dev01 Date: Sun, 7 Jun 2026 13:33:14 +0800 Subject: [PATCH] feat: add chassis_control_pkg with serial communication node ChassisNode communicates via /dev/ttyCH341USB0 at 115200 bps: - Send 0xA7 command frames (auto/manual/debug mode switching) - Send 0xA6 manual control frames (motor_speed_base + angle_velocity) - Send 0xA8 debug tuning frames (speed/yaw/position PID params) - Receive 0xA9 feedforward frames from chassis MCU - Subscribe: /cmd_chassis, /chassis/mode, /web/chassis_pid_config - Publish: /chassis/feedforward (angle_velocity for gimbal feedforward) --- .../chassis_control_pkg/__init__.py | 0 .../chassis_control_pkg/chassis_node.py | 206 ++++++++++++++++++ workspace/src/chassis_control_pkg/package.xml | 21 ++ .../resource/chassis_control_pkg | 0 workspace/src/chassis_control_pkg/setup.cfg | 4 + workspace/src/chassis_control_pkg/setup.py | 28 +++ .../test/test_copyright.py | 24 ++ .../chassis_control_pkg/test/test_flake8.py | 25 +++ .../chassis_control_pkg/test/test_pep257.py | 23 ++ 9 files changed, 331 insertions(+) create mode 100644 workspace/src/chassis_control_pkg/chassis_control_pkg/__init__.py create mode 100644 workspace/src/chassis_control_pkg/chassis_control_pkg/chassis_node.py create mode 100644 workspace/src/chassis_control_pkg/package.xml create mode 100644 workspace/src/chassis_control_pkg/resource/chassis_control_pkg create mode 100644 workspace/src/chassis_control_pkg/setup.cfg create mode 100644 workspace/src/chassis_control_pkg/setup.py create mode 100644 workspace/src/chassis_control_pkg/test/test_copyright.py create mode 100644 workspace/src/chassis_control_pkg/test/test_flake8.py create mode 100644 workspace/src/chassis_control_pkg/test/test_pep257.py diff --git a/workspace/src/chassis_control_pkg/chassis_control_pkg/__init__.py b/workspace/src/chassis_control_pkg/chassis_control_pkg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workspace/src/chassis_control_pkg/chassis_control_pkg/chassis_node.py b/workspace/src/chassis_control_pkg/chassis_control_pkg/chassis_node.py new file mode 100644 index 0000000..81879e3 --- /dev/null +++ b/workspace/src/chassis_control_pkg/chassis_control_pkg/chassis_node.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +import json +import struct +import threading + +import serial +import rclpy +from rclpy.node import Node +from std_msgs.msg import Float32, Float32MultiArray, String + +CMD_AUTO = 0x01 +CMD_MANUAL = 0x02 +CMD_DEBUG = 0x03 + +FRAME_SOF_COMMAND = 0xA7 +FRAME_SOF_MANUAL = 0xA6 +FRAME_SOF_DEBUG = 0xA8 +FRAME_SOF_FEEDFORWARD = 0xA9 + +SERIAL_READ_TIMEOUT = 0.1 + + +def _checksum(data: bytearray) -> int: + return sum(data) & 0xFF + + +def _checksum_match(data: bytearray) -> bool: + return len(data) > 1 and data[-1] == _checksum(data[:-1]) + + +class ChassisNode(Node): + def __init__(self): + super().__init__('chassis_node') + + self.declare_parameter('serial_port', '/dev/ttyCH341USB0') + self.declare_parameter('baud_rate', 115200) + port = self.get_parameter('serial_port').value + baud = self.get_parameter('baud_rate').value + + self._ser = serial.Serial( + port=port, + baudrate=baud, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + timeout=SERIAL_READ_TIMEOUT, + ) + self.get_logger().info(f'Serial port {port} opened at {baud} baud') + + self._cmd_chassis_sub = self.create_subscription( + Float32MultiArray, '/cmd_chassis', self._cmd_chassis_callback, 10 + ) + self._mode_sub = self.create_subscription( + String, '/chassis/mode', self._mode_callback, 10 + ) + self._pid_config_sub = self.create_subscription( + String, '/web/chassis_pid_config', self._pid_config_callback, 10 + ) + + self._feedforward_pub = self.create_publisher( + Float32, '/chassis/feedforward', 10 + ) + + self._current_mode = 'auto' + + self._read_thread = threading.Thread( + target=self._serial_read_loop, daemon=True + ) + self._read_thread.start() + + self.get_logger().info( + 'ChassisNode ready - subs: /cmd_chassis, /chassis/mode, ' + '/web/chassis_pid_config, pub: /chassis/feedforward' + ) + + def _write_frame(self, data: bytearray): + try: + self._ser.write(data) + self._ser.flush() + except serial.SerialException as e: + self.get_logger().error(f'Serial write error: {e}') + + def _cmd_chassis_callback(self, msg: Float32MultiArray): + if self._current_mode != 'manual': + return + if len(msg.data) < 2: + self.get_logger().error( + f'/cmd_chassis requires 2 values, got {len(msg.data)}') + return + + motor_speed_base = int(max(-50, min(50, msg.data[0]))) + angle_velocity = float(msg.data[1]) + + data = bytearray() + data.append(FRAME_SOF_MANUAL) + data.extend(struct.pack(' bytearray: + while len(buf) > 0: + sof = buf[0] + if sof == FRAME_SOF_FEEDFORWARD: + frame_len = 6 + if len(buf) < frame_len: + break + frame = buf[:frame_len] + if _checksum_match(frame): + angle_vel = struct.unpack(' + + + chassis_control_pkg + 0.0.0 + Chassis serial communication node (UART 115200 8N1, /dev/ttyCH341USB0) + dev01 + TODO: License declaration + + rclpy + std_msgs + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/workspace/src/chassis_control_pkg/resource/chassis_control_pkg b/workspace/src/chassis_control_pkg/resource/chassis_control_pkg new file mode 100644 index 0000000..e69de29 diff --git a/workspace/src/chassis_control_pkg/setup.cfg b/workspace/src/chassis_control_pkg/setup.cfg new file mode 100644 index 0000000..27823c8 --- /dev/null +++ b/workspace/src/chassis_control_pkg/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/chassis_control_pkg +[install] +install_scripts=$base/lib/chassis_control_pkg diff --git a/workspace/src/chassis_control_pkg/setup.py b/workspace/src/chassis_control_pkg/setup.py new file mode 100644 index 0000000..ff0c1cb --- /dev/null +++ b/workspace/src/chassis_control_pkg/setup.py @@ -0,0 +1,28 @@ +from setuptools import find_packages, setup + +package_name = 'chassis_control_pkg' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='dev01', + maintainer_email='dev01@todo.todo', + description='Chassis serial communication node', + license='TODO: License declaration', + extras_require={ + 'test': ['pytest'], + }, + entry_points={ + 'console_scripts': [ + 'chassis_node = chassis_control_pkg.chassis_node:main' + ], + }, +) diff --git a/workspace/src/chassis_control_pkg/test/test_copyright.py b/workspace/src/chassis_control_pkg/test/test_copyright.py new file mode 100644 index 0000000..b992092 --- /dev/null +++ b/workspace/src/chassis_control_pkg/test/test_copyright.py @@ -0,0 +1,24 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/workspace/src/chassis_control_pkg/test/test_flake8.py b/workspace/src/chassis_control_pkg/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/workspace/src/chassis_control_pkg/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/workspace/src/chassis_control_pkg/test/test_pep257.py b/workspace/src/chassis_control_pkg/test/test_pep257.py new file mode 100644 index 0000000..b234a38 --- /dev/null +++ b/workspace/src/chassis_control_pkg/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings'