From 70e7360ba2d7e068d0d330b3a869b83543e8861f Mon Sep 17 00:00:00 2001 From: Orange <2314753575@qq.com> Date: Thu, 23 Jul 2026 15:15:59 +0800 Subject: [PATCH] docs: add unified usb camera launch plan --- .../2026-07-23-unified-usb-camera-launch.md | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-23-unified-usb-camera-launch.md diff --git a/docs/superpowers/plans/2026-07-23-unified-usb-camera-launch.md b/docs/superpowers/plans/2026-07-23-unified-usb-camera-launch.md new file mode 100644 index 0000000..d197a63 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-unified-usb-camera-launch.md @@ -0,0 +1,329 @@ +# Unified USB Camera Launch Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make both USB camera launch files use one 1280x720 native-MJPEG YAML configuration, with WebSocket as their only behavioral difference. + +**Architecture:** The base launch owns all camera behavior and loads a single ROS parameter YAML. The WebSocket launch includes the base launch unchanged and adds only the WebSocket include. A minimal CMake file installs both `launch/` and `config/`, replacing reliance on stale install-space symlinks. + +**Tech Stack:** ROS 2 Humble launch, ROS 2 parameter YAML, ament_cmake, Python `ast`/`unittest`, D-Robotics `hobot_usb_cam` and `websocket` packages. + +--- + +## File Structure + +- Create `src/car_usb_cam/config/usb_camera.yaml`: sole camera runtime configuration. +- Keep `src/car_usb_cam/config/usb_camera_calibration.yaml`: calibration data only. +- Modify `src/car_usb_cam/launch/hobot_usb_cam.launch.py`: load YAML and start one camera plus shared-memory environment. +- Modify `src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py`: include the base launch and add WebSocket only. +- Create `src/car_usb_cam/CMakeLists.txt`: install launch/config directories and register tests. +- Create `src/car_usb_cam/test/test_unified_launch_config.py`: structural regression check. +- Append `D:/Programme/agent/projects/yiliao/log.md`: timestamped implementation and verification record. + +### Task 1: Add a failing structural regression test + +**Files:** +- Create: `src/car_usb_cam/test/test_unified_launch_config.py` + +- [ ] **Step 1: Write the failing test** + +```python +import ast +import pathlib +import unittest + +import yaml + + +PACKAGE_ROOT = pathlib.Path(__file__).resolve().parents[1] +BASE_LAUNCH = PACKAGE_ROOT / "launch" / "hobot_usb_cam.launch.py" +WEB_LAUNCH = PACKAGE_ROOT / "launch" / "hobot_usb_cam_websocket.launch.py" +CAMERA_CONFIG = PACKAGE_ROOT / "config" / "usb_camera.yaml" + + +def string_constants(path): + tree = ast.parse(path.read_text(encoding="utf-8")) + return { + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + } + + +def call_names(path): + tree = ast.parse(path.read_text(encoding="utf-8")) + names = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if isinstance(node.func, ast.Name): + names.append(node.func.id) + elif isinstance(node.func, ast.Attribute): + names.append(node.func.attr) + return names + + +class UnifiedLaunchConfigTest(unittest.TestCase): + def test_yaml_defines_final_native_mjpeg_configuration(self): + document = yaml.safe_load(CAMERA_CONFIG.read_text(encoding="utf-8")) + params = document["hobot_usb_cam"]["ros__parameters"] + self.assertEqual(params["video_device"], "/dev/video0") + self.assertEqual(params["image_width"], 1280) + self.assertEqual(params["image_height"], 720) + self.assertEqual(params["framerate"], 30) + self.assertEqual(params["pixel_format"], "mjpeg") + self.assertEqual(params["io_method"], "mmap") + self.assertFalse(params["zero_copy"]) + + def test_base_launch_owns_the_only_camera_node(self): + constants = string_constants(BASE_LAUNCH) + self.assertIn("usb_camera.yaml", constants) + self.assertEqual(call_names(BASE_LAUNCH).count("Node"), 1) + self.assertIn("hobot_usb_cam", constants) + + def test_web_launch_only_adds_websocket(self): + constants = string_constants(WEB_LAUNCH) + self.assertIn("hobot_usb_cam.launch.py", constants) + self.assertIn("websocket.launch.py", constants) + self.assertEqual(call_names(WEB_LAUNCH).count("Node"), 0) + for forbidden in ( + "hobot_codec", + "/image_mjpeg", + "yuyv2rgb", + "usb_image_width", + "usb_image_height", + "usb_pixel_format", + ): + self.assertNotIn(forbidden, constants) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Run it and verify RED** + +Run: + +```bash +cd /home/sunrise/yiliao_ws +python3 -m unittest src/car_usb_cam/test/test_unified_launch_config.py -v +``` + +Expected: errors because `config/usb_camera.yaml` does not exist, plus failure because the current WebSocket launch contains codec and camera override literals. + +### Task 2: Create the shared runtime YAML and install rules + +**Files:** +- Create: `src/car_usb_cam/config/usb_camera.yaml` +- Create: `src/car_usb_cam/CMakeLists.txt` + +- [ ] **Step 1: Add the shared camera parameter YAML** + +```yaml +hobot_usb_cam: + ros__parameters: + camera_calibration_file_path: /home/sunrise/yiliao_ws/src/car_usb_cam/config/usb_camera_calibration.yaml + frame_id: default_usb_cam + framerate: 30 + image_height: 720 + image_width: 1280 + io_method: mmap + pixel_format: mjpeg + video_device: /dev/video0 + zero_copy: false +``` + +- [ ] **Step 2: Add reproducible package installation and test registration** + +```cmake +cmake_minimum_required(VERSION 3.8) +project(car_usb_cam) + +find_package(ament_cmake REQUIRED) + +install( + DIRECTORY config launch + DESTINATION share/${PROJECT_NAME} +) + +if(BUILD_TESTING) + find_package(ament_cmake_pytest REQUIRED) + ament_add_pytest_test( + test_unified_launch_config + test/test_unified_launch_config.py + ) +endif() + +ament_package() +``` + +- [ ] **Step 3: Run the test and confirm the remaining failure is the old launch structure** + +Run the same unittest command. Expected: YAML test passes; WebSocket structure test still fails because the old file contains `hobot_codec` and camera overrides. + +### Task 3: Refactor the launch files + +**Files:** +- Modify: `src/car_usb_cam/launch/hobot_usb_cam.launch.py` +- Modify: `src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py` + +- [ ] **Step 1: Replace the base launch with the YAML-backed camera definition** + +```python +import os + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description(): + package_share = get_package_share_directory("car_usb_cam") + camera_config = os.path.join(package_share, "config", "usb_camera.yaml") + + return LaunchDescription([ + DeclareLaunchArgument( + "camera_config", + default_value=camera_config, + description="ROS parameter file for the USB camera", + ), + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join( + get_package_share_directory("hobot_shm"), + "launch", + "hobot_shm.launch.py", + ) + ) + ), + Node( + package="hobot_usb_cam", + executable="hobot_usb_cam", + name="hobot_usb_cam", + parameters=[LaunchConfiguration("camera_config")], + arguments=["--ros-args", "--log-level", "warn"], + ), + ]) +``` + +- [ ] **Step 2: Replace the WebSocket launch with a base include plus WebSocket include** + +```python +import os + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.launch_description_sources import PythonLaunchDescriptionSource + + +def generate_launch_description(): + camera_launch = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join( + get_package_share_directory("car_usb_cam"), + "launch", + "hobot_usb_cam.launch.py", + ) + ) + ) + + websocket_launch = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join( + get_package_share_directory("websocket"), + "launch", + "websocket.launch.py", + ) + ), + launch_arguments={ + "websocket_image_topic": "/image", + "websocket_image_type": "mjpeg", + "websocket_only_show_image": "True", + "websocket_output_fps": "30", + }.items(), + ) + + return LaunchDescription([camera_launch, websocket_launch]) +``` + +- [ ] **Step 3: Run the structural test and verify GREEN** + +Run: + +```bash +python3 -m unittest src/car_usb_cam/test/test_unified_launch_config.py -v +``` + +Expected: all three tests pass. + +- [ ] **Step 4: Build the package and run package tests** + +Run: + +```bash +colcon build --symlink-install --packages-select car_usb_cam +colcon test --packages-select car_usb_cam --event-handlers console_direct+ +colcon test-result --verbose +``` + +Expected: build succeeds and `test_unified_launch_config` passes with zero failures. + +- [ ] **Step 5: Commit implementation** + +```bash +git add src/car_usb_cam/CMakeLists.txt \ + src/car_usb_cam/config/usb_camera.yaml \ + src/car_usb_cam/launch/hobot_usb_cam.launch.py \ + src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py \ + src/car_usb_cam/test/test_unified_launch_config.py +git commit -m "refactor: unify usb camera launch configuration" +``` + +### Task 4: Sequential runtime verification + +**Files:** +- Append: `D:/Programme/agent/projects/yiliao/log.md` + +- [ ] **Step 1: Stop current camera, codec, generic USB camera, and WebSocket processes** + +Use targeted `pkill -TERM -f` patterns and verify `/dev/video0` is not held with `fuser -v /dev/video0`. + +- [ ] **Step 2: Start the base launch and verify camera-only behavior** + +Run `ros2 launch car_usb_cam hobot_usb_cam.launch.py`, then verify: + +- one `/hobot_usb_cam` node; +- `/image` type is `sensor_msgs/msg/CompressedImage`; +- decoded image dimensions are 1280x720; +- `ros2 topic hz /image` is approximately 30 FPS; +- no `/websocket` node and no listener on port 8080. + +- [ ] **Step 3: Stop the base launch and start the WebSocket launch** + +Run `ros2 launch car_usb_cam hobot_usb_cam_websocket.launch.py`, then verify the same camera node parameters, image type, dimensions, and FPS. Confirm exactly one `/websocket` node and one listener on port 8080. + +- [ ] **Step 4: Compare both captured camera parameter sets** + +Capture `ros2 param dump /hobot_usb_cam` for both runs, normalize volatile output, and use `diff -u`. Expected: no differences. + +- [ ] **Step 5: Leave the WebSocket launch running and verify `.164` routing** + +Confirm the webpage receives a 1280x720 frame and `ip route get from 192.168.10.164` returns `dev wlx200db0c3fca5 table 164`. + +- [ ] **Step 6: Append the dated implementation and verification results to `log.md`** + +Record changed files, test outputs, both launch measurements, parameter diff result, final process state, and the `.164` route result. + +--- + +## Self-Review + +- Spec coverage: shared YAML, base include architecture, removal of codec, native MJPEG flow, sequential runtime validation, and single WebSocket difference are all mapped to Tasks 1-4. +- Scope: all changes are confined to `car_usb_cam`, its test/install metadata, and the requested local operation log. +- Type consistency: both launch variants publish `/image` as `sensor_msgs/msg/CompressedImage`; WebSocket consumes `/image` as `mjpeg`. +- Operational constraint: validation explicitly handles exclusive ownership of `/dev/video0`.