diff --git a/bashes/radar-driver-switch.sh b/bashes/radar-driver-switch.sh index c64b9ce..34413ab 100644 --- a/bashes/radar-driver-switch.sh +++ b/bashes/radar-driver-switch.sh @@ -1,101 +1,172 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -# 退出码说明 -# 0 - 成功 -# 1 - 错误:等待TTY设备就绪超时 -# 2 - 错误:未找到CH343驱动 -# 3 - 错误:雷达端口上未找到CH34x设备 -# 4 - 错误:未从cdc_acm找到雷达接口 -# 5 - 错误:解绑cdc_acm驱动失败 -# 6 - 错误:绑定usb_ch343驱动失败 +# Physical USB ports on the RDKx5 carrier. +RADAR_PORT="1-1.4" +IMU_PORT="1-1.1" -# ── 配置 ──────────────────────────────────────────── -TTY_DEVICE="/dev/ttyACM*" -CDC_ACM_PATH="/sys/bus/usb/drivers/cdc_acm" -USB_CH343_PATH="/sys/bus/usb/drivers/usb_ch343" -RADAR_USB_PORT="1-1.4" # 雷达 CH34x 的 USB 物理端口(固定不变) -CHECK_INTERVAL=1 -MAX_WAIT_SECONDS=30 +# RADAR -> usb_ch343, IMU -> cdc_acm. +RADAR_DRIVER="usb_ch343" +IMU_DRIVER="cdc_acm" -# ── 初始化 ────────────────────────────────────────── -start_time=$(cut -d. -f1 /proc/uptime) +DRIVER_ROOT="/sys/bus/usb/drivers" +MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-30}" +WAIT_INTERVAL_SECONDS="${WAIT_INTERVAL_SECONDS:-1}" +INTERFACES=() -# ── 1. 等待 TTY 设备就绪 ──────────────────────────── -while true; do - for dev in /dev/ttyACM*; do - [ -e "$dev" ] || continue - if [ -c "$dev" ] && [ -r "$dev" ] && [ -w "$dev" ]; then - echo "[雷达驱动自动切换程序] 设备 $dev 已就绪!" > /dev/kmsg - TTY_DEVICE="$dev" - break 2 +log() { + local message="[radar-driver-switch] $*" + if [ -w /dev/kmsg ]; then + printf '%s\n' "$message" > /dev/kmsg + else + printf '%s\n' "$message" + fi +} + +load_driver() { + local driver="$1" + case "$driver" in + cdc_acm) + modprobe cdc_acm 2>/dev/null || true + ;; + usb_ch343) + modprobe ch343 2>/dev/null || true + ;; + esac +} + +driver_path() { + printf '%s/%s\n' "$DRIVER_ROOT" "$1" +} + +require_driver() { + local driver="$1" + local path + path="$(driver_path "$driver")" + if [ ! -d "$path" ]; then + log "error: driver path missing: $path" + exit 2 + fi +} + +collect_interfaces() { + local port="$1" + shopt -s nullglob + INTERFACES=(/sys/bus/usb/devices/"${port}":*) + shopt -u nullglob + [ "${#INTERFACES[@]}" -gt 0 ] +} + +wait_for_interfaces() { + local label="$1" + local port="$2" + local attempt=0 + while [ "$attempt" -le "$MAX_WAIT_SECONDS" ]; do + if collect_interfaces "$port"; then + return 0 + fi + log "waiting for $label port $port interfaces (${attempt}s)" + sleep "$WAIT_INTERVAL_SECONDS" + attempt=$((attempt + WAIT_INTERVAL_SECONDS)) + done + log "error: timed out waiting for $label port $port interfaces" + exit 1 +} + +current_driver() { + local iface="$1" + local target + target="$(readlink -f "$iface/driver" 2>/dev/null || true)" + if [ -n "$target" ]; then + basename "$target" + fi +} + +unbind_from_current_driver() { + local iface="$1" + local iface_name + local driver + iface_name="$(basename "$iface")" + driver="$(current_driver "$iface")" + if [ -z "$driver" ]; then + return 0 + fi + if [ ! -w "$(driver_path "$driver")/unbind" ]; then + log "error: cannot unbind $iface_name from $driver" + exit 5 + fi + printf '%s\n' "$iface_name" > "$(driver_path "$driver")/unbind" + log "unbound $iface_name from $driver" +} + +bind_to_driver() { + local iface="$1" + local driver="$2" + local iface_name + local bind_path + iface_name="$(basename "$iface")" + bind_path="$(driver_path "$driver")/bind" + if [ ! -w "$bind_path" ]; then + log "error: cannot bind $iface_name to $driver" + exit 6 + fi + if printf '%s\n' "$iface_name" > "$bind_path" 2>/dev/null; then + log "bound $iface_name to $driver" + else + log "warning: $iface_name did not bind to $driver" + fi +} + +ensure_port_bound() { + local label="$1" + local port="$2" + local desired_driver="$3" + local iface + local iface_name + local driver + local bound_count=0 + + wait_for_interfaces "$label" "$port" + + for iface in "${INTERFACES[@]}"; do + iface_name="$(basename "$iface")" + driver="$(current_driver "$iface")" + if [ "$driver" = "$desired_driver" ]; then + log "$label $iface_name already uses $desired_driver" + bound_count=$((bound_count + 1)) + continue + fi + + if [ -n "$driver" ]; then + unbind_from_current_driver "$iface" + sleep 0.2 + fi + + bind_to_driver "$iface" "$desired_driver" + sleep 0.2 + driver="$(current_driver "$iface")" + if [ "$driver" = "$desired_driver" ]; then + bound_count=$((bound_count + 1)) fi done - if [ ${MAX_WAIT_SECONDS} -gt 0 ]; then - current_time=$(cut -d. -f1 /proc/uptime) - elapsed_seconds=$((current_time - start_time)) - if [ ${elapsed_seconds} -ge ${MAX_WAIT_SECONDS} ]; then - echo "[雷达驱动自动切换程序] 错误:等待 ${TTY_DEVICE} 超时(${MAX_WAIT_SECONDS}秒)!" > /dev/kmsg - exit 1 - fi - echo "[雷达驱动自动切换程序] 仍在等待${TTY_DEVICE}(已等待${elapsed_seconds}秒)..." > /dev/kmsg + if [ "$bound_count" -eq 0 ]; then + log "error: no $label interface on $port is bound to $desired_driver" + exit 7 fi - sleep ${CHECK_INTERVAL} -done +} -# ── 2. 确认 CH343 驱动已加载 ──────────────────────── -if [ -d "${USB_CH343_PATH}" ]; then - echo "[雷达驱动自动切换程序] CH343驱动已加载,继续..." > /dev/kmsg -else - echo "[雷达驱动自动切换程序] 错误:未找到CH343驱动!" > /dev/kmsg - exit 2 -fi +main() { + load_driver "$RADAR_DRIVER" + load_driver "$IMU_DRIVER" + require_driver "$RADAR_DRIVER" + require_driver "$IMU_DRIVER" -# ── 3. 通过物理端口定位雷达 CH34x ─────────────────── -radar_sysfs="/sys/bus/usb/devices/${RADAR_USB_PORT}" + ensure_port_bound "RADAR" "$RADAR_PORT" "$RADAR_DRIVER" + ensure_port_bound "IMU" "$IMU_PORT" "$IMU_DRIVER" -if [ ! -d "${radar_sysfs}" ]; then - echo "[雷达驱动自动切换程序] 错误:端口 ${RADAR_USB_PORT} 上无设备!" > /dev/kmsg - exit 3 -fi + udevadm settle --timeout=5 2>/dev/null || true + log "done; serial devices: $(ls /dev/ttyACM* /dev/ttyCH343USB* 2>/dev/null | tr '\n' ' ')" +} -vendor_id=$(cat "${radar_sysfs}/idVendor" 2>/dev/null) -product_id=$(cat "${radar_sysfs}/idProduct" 2>/dev/null) - -if [ "${vendor_id}" != "1a86" ] || [ "${product_id}" != "55d4" ]; then - echo "[雷达驱动自动切换程序] 错误:端口 ${RADAR_USB_PORT} 上 VID:PID=${vendor_id}:${product_id},非雷达CH34x!" > /dev/kmsg - exit 3 -fi - -echo "[雷达驱动自动切换程序] 雷达确认位于端口 ${RADAR_USB_PORT},VID:PID=${vendor_id}:${product_id}" > /dev/kmsg - -# ── 4. 查找 cdc_acm 下雷达的接口 ──────────────────── -cdc_acm_sub_addr=$(ls "${CDC_ACM_PATH}" 2>/dev/null | grep "^${RADAR_USB_PORT}:" | head -n 1) - -if [ -z "${cdc_acm_sub_addr}" ]; then - echo "[雷达驱动自动切换程序] 错误:cdc_acm驱动中未找到端口 ${RADAR_USB_PORT} 的接口" > /dev/kmsg - exit 4 -fi - -echo "[雷达驱动自动切换程序] 雷达 cdc_acm 接口:${cdc_acm_sub_addr}" > /dev/kmsg - -# ── 5. 解绑 cdc_acm ───────────────────────────────── -echo "${cdc_acm_sub_addr}" | sudo tee "${CDC_ACM_PATH}/unbind" > /dev/null 2>&1 -if [ $? -eq 0 ]; then - echo "[雷达驱动自动切换程序] 已解绑 cdc_acm:${cdc_acm_sub_addr}" > /dev/kmsg -else - echo "[雷达驱动自动切换程序] 错误:解绑 cdc_acm 失败" > /dev/kmsg - exit 5 -fi - -# ── 6. 绑定 ch343 ─────────────────────────────────── -echo "${cdc_acm_sub_addr}" | sudo tee "${USB_CH343_PATH}/bind" > /dev/null 2>&1 -if [ $? -eq 0 ]; then - echo "[雷达驱动自动切换程序] 已绑定 usb_ch343:${cdc_acm_sub_addr}" > /dev/kmsg -else - echo "[雷达驱动自动切换程序] 错误:绑定 usb_ch343 失败" > /dev/kmsg - exit 6 -fi - -echo "[雷达驱动自动切换程序] 雷达驱动切换完毕!" > /dev/kmsg -exit 0 +main "$@" diff --git a/src/car_usb_cam/image_web_bridge_README.md b/src/car_usb_cam/image_web_bridge_README.md index 76046e5..f03738f 100644 --- a/src/car_usb_cam/image_web_bridge_README.md +++ b/src/car_usb_cam/image_web_bridge_README.md @@ -38,6 +38,24 @@ It supports the image publishing modes used by `car_usb_cam`: - `/image_mjpeg` or `/image_jpeg` (`sensor_msgs/msg/CompressedImage`) from `hobot_codec` - `/hbmem_img` (`hbm_img_msgs/msg/HbmMsg1080P`, `HbmMsg540P`, or `HbmMsg480P`) from zero-copy camera paths +`car_usb_cam` can switch camera output modes using launch parameters only: + +```bash +# Native MJPEG output from the USB camera. +ros2 launch car_usb_cam hobot_usb_cam.launch.py \ + usb_zero_copy:=false \ + usb_pixel_format:=mjpeg +# Publishes /image as sensor_msgs/msg/CompressedImage. +``` + +```bash +# Zero-copy hbmem output for consumers such as hobot_llamacpp shared-memory mode. +ros2 launch car_usb_cam hobot_usb_cam.launch.py \ + usb_zero_copy:=true \ + usb_pixel_format:=mjpeg +# Publishes /hbmem_img as hbm_img_msgs/msg/HbmMsg1080P. +``` + Run on RDKx5: ```bash diff --git a/src/car_usb_cam/launch/hobot_usb_cam.launch.py b/src/car_usb_cam/launch/hobot_usb_cam.launch.py index 2588b88..b3d056c 100644 --- a/src/car_usb_cam/launch/hobot_usb_cam.launch.py +++ b/src/car_usb_cam/launch/hobot_usb_cam.launch.py @@ -35,6 +35,46 @@ def generate_launch_description(): default_value=camera_config, description="ROS parameter file for the USB camera", ), + DeclareLaunchArgument( + "usb_frame_id", + default_value="default_usb_cam", + description="Image message frame_id", + ), + DeclareLaunchArgument( + "usb_framerate", + default_value="30", + description="USB camera frame rate", + ), + DeclareLaunchArgument( + "usb_image_height", + default_value="720", + description="USB camera image height", + ), + DeclareLaunchArgument( + "usb_image_width", + default_value="1280", + description="USB camera image width", + ), + DeclareLaunchArgument( + "usb_io_method", + default_value="mmap", + description="USB camera io_method: mmap/read/userptr", + ), + DeclareLaunchArgument( + "usb_pixel_format", + default_value="mjpeg", + description="USB camera pixel format, such as mjpeg or yuyv2rgb", + ), + DeclareLaunchArgument( + "usb_video_device", + default_value="/dev/video0", + description="USB camera device", + ), + DeclareLaunchArgument( + "usb_zero_copy", + default_value="false", + description="Publish zero-copy hbmem image messages when true", + ), IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join( @@ -51,6 +91,14 @@ def generate_launch_description(): parameters=[ LaunchConfiguration("camera_config"), {"camera_calibration_file_path": calibration_file}, + {"frame_id": LaunchConfiguration("usb_frame_id")}, + {"framerate": LaunchConfiguration("usb_framerate")}, + {"image_height": LaunchConfiguration("usb_image_height")}, + {"image_width": LaunchConfiguration("usb_image_width")}, + {"io_method": LaunchConfiguration("usb_io_method")}, + {"pixel_format": LaunchConfiguration("usb_pixel_format")}, + {"video_device": LaunchConfiguration("usb_video_device")}, + {"zero_copy": LaunchConfiguration("usb_zero_copy")}, ], arguments=["--ros-args", "--log-level", "warn"], ), diff --git a/src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py b/src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py index 8f76bd2..29dda79 100644 --- a/src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py +++ b/src/car_usb_cam/launch/hobot_usb_cam_websocket.launch.py @@ -16,8 +16,9 @@ import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription -from launch.actions import IncludeLaunchDescription +from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch.substitutions import LaunchConfiguration def generate_launch_description(): @@ -28,7 +29,17 @@ def generate_launch_description(): "launch", "hobot_usb_cam.launch.py", ) - ) + ), + launch_arguments={ + "usb_frame_id": LaunchConfiguration("usb_frame_id"), + "usb_framerate": LaunchConfiguration("usb_framerate"), + "usb_image_height": LaunchConfiguration("usb_image_height"), + "usb_image_width": LaunchConfiguration("usb_image_width"), + "usb_io_method": LaunchConfiguration("usb_io_method"), + "usb_pixel_format": LaunchConfiguration("usb_pixel_format"), + "usb_video_device": LaunchConfiguration("usb_video_device"), + "usb_zero_copy": LaunchConfiguration("usb_zero_copy"), + }.items(), ) websocket_launch = IncludeLaunchDescription( @@ -47,4 +58,15 @@ def generate_launch_description(): }.items(), ) - return LaunchDescription([camera_launch, websocket_launch]) + return LaunchDescription([ + DeclareLaunchArgument("usb_frame_id", default_value="default_usb_cam"), + DeclareLaunchArgument("usb_framerate", default_value="30"), + DeclareLaunchArgument("usb_image_height", default_value="720"), + DeclareLaunchArgument("usb_image_width", default_value="1280"), + DeclareLaunchArgument("usb_io_method", default_value="mmap"), + DeclareLaunchArgument("usb_pixel_format", default_value="mjpeg"), + DeclareLaunchArgument("usb_video_device", default_value="/dev/video0"), + DeclareLaunchArgument("usb_zero_copy", default_value="false"), + camera_launch, + websocket_launch, + ]) diff --git a/src/car_usb_cam/test/__pycache__/test_unified_launch_config.cpython-310-pytest-6.2.5.pyc b/src/car_usb_cam/test/__pycache__/test_unified_launch_config.cpython-310-pytest-6.2.5.pyc new file mode 100644 index 0000000..0eaa20e Binary files /dev/null and b/src/car_usb_cam/test/__pycache__/test_unified_launch_config.cpython-310-pytest-6.2.5.pyc differ diff --git a/src/car_usb_cam/test/test_unified_launch_config.py b/src/car_usb_cam/test/test_unified_launch_config.py index 998311a..93068a8 100644 --- a/src/car_usb_cam/test/test_unified_launch_config.py +++ b/src/car_usb_cam/test/test_unified_launch_config.py @@ -82,6 +82,17 @@ class UnifiedLaunchConfigTest(unittest.TestCase): self.assertIn("usb_camera.yaml", constants) self.assertIn("usb_camera_calibration.yaml", constants) self.assertIn("hobot_usb_cam", constants) + for launch_arg in ( + "usb_frame_id", + "usb_framerate", + "usb_image_height", + "usb_image_width", + "usb_io_method", + "usb_pixel_format", + "usb_video_device", + "usb_zero_copy", + ): + self.assertIn(launch_arg, constants) node_calls = calls_named(BASE_LAUNCH, "Node") self.assertEqual(len(node_calls), 1) parameters = next( @@ -92,6 +103,17 @@ class UnifiedLaunchConfigTest(unittest.TestCase): parameters_tree = ast.dump(parameters) self.assertIn("camera_config", parameters_tree) self.assertIn("camera_calibration_file_path", parameters_tree) + for launch_arg in ( + "usb_frame_id", + "usb_framerate", + "usb_image_height", + "usb_image_width", + "usb_io_method", + "usb_pixel_format", + "usb_video_device", + "usb_zero_copy", + ): + self.assertIn(launch_arg, parameters_tree) def test_web_launch_only_adds_websocket(self): constants = string_constants(WEB_LAUNCH) @@ -100,14 +122,20 @@ class UnifiedLaunchConfigTest(unittest.TestCase): self.assertEqual(call_names(WEB_LAUNCH).count("Node"), 0) self.assertEqual(len(calls_named(WEB_LAUNCH, "IncludeLaunchDescription")), 2) camera_include = assigned_call(WEB_LAUNCH, "camera_launch") - self.assertNotIn("launch_arguments", {item.arg for item in camera_include.keywords}) + self.assertIn("launch_arguments", {item.arg for item in camera_include.keywords}) + camera_include_tree = ast.dump(camera_include) + for launch_arg in ( + "usb_zero_copy", + "usb_image_width", + "usb_image_height", + "usb_pixel_format", + "usb_video_device", + ): + self.assertIn(launch_arg, camera_include_tree) for forbidden in ( "hobot_codec", "/image_mjpeg", "yuyv2rgb", - "usb_image_width", - "usb_image_height", - "usb_pixel_format", ): self.assertNotIn(forbidden, constants) diff --git a/src/mtran/README.md b/src/mtran/README.md index 7eed681..5568887 100644 --- a/src/mtran/README.md +++ b/src/mtran/README.md @@ -56,7 +56,7 @@ cd ~/smart-healthcare-2026/src/mtran/dependencies ./auto_install.sh ``` -脚本会请求 `sudo` 安装非 ROS apt 包,在 `dependencies/tools` 安装固定版本 Bun, +不要使用 `sudo` 权限执行脚本,脚本会请求 `sudo` 安装非 ROS apt 包,在 `dependencies/tools` 安装固定版本 Bun, 从 `vendor/mtranserver` 构建当前 CPU 可运行的 sidecar,并只准备 `en -> zh-Hans` 模型。成功结尾类似: diff --git a/src/navigation/obstacle_nav2/config/nav2_params.yaml b/src/navigation/obstacle_nav2/config/nav2_params.yaml index 6033640..0c195b1 100755 --- a/src/navigation/obstacle_nav2/config/nav2_params.yaml +++ b/src/navigation/obstacle_nav2/config/nav2_params.yaml @@ -78,8 +78,8 @@ controller_server: batch_size: 1000 vx_std: 0.22 vy_std: 0.0 - wz_std: 0.4 - vx_max: 0.75 + wz_std: 0.5 + vx_max: 0.40 vx_min: -0.75 vy_max: 0.0 wz_max: 2.0 @@ -92,16 +92,16 @@ controller_server: trajectory_step: 5 time_step: 3 AckermannConstraints: - min_turning_r: 0.4 + min_turning_r: 0.6 critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"] - ConstraintCritic: + ConstraintCritic: enabled: true cost_power: 1 cost_weight: 4.0 GoalCritic: enabled: true cost_power: 1 - cost_weight: 5.0 + cost_weight: 3.0 threshold_to_consider: 1.4 GoalAngleCritic: enabled: true @@ -111,7 +111,7 @@ controller_server: PreferForwardCritic: enabled: true cost_power: 1 - cost_weight: 3.0 + cost_weight: 6.0 threshold_to_consider: 0.5 CostCritic: enabled: true @@ -140,7 +140,7 @@ controller_server: PathAngleCritic: enabled: true cost_power: 1 - cost_weight: 2.0 + cost_weight: 6.0 offset_from_furthest: 4 threshold_to_consider: 0.5 max_angle_to_furthest: 1.0 @@ -175,12 +175,12 @@ local_costmap: transform_tolerance: 0.2 default_obstacle_radius: 0.05 minimum_obstacle_radius: 0.02 - maximum_obstacle_radius: 0.50 + maximum_obstacle_radius: 0.06 extra_inflation: 0.02 inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 - inflation_radius: 0.55 + inflation_radius: 0.2 always_send_full_costmap: True local_costmap_client: ros__parameters: @@ -245,7 +245,7 @@ planner_server: angle_quantization_bins: 72 analytic_expansion_ratio: 3.5 analytic_expansion_max_length: 3.0 - minimum_turning_radius: 0.40 + minimum_turning_radius: 0.60 reverse_penalty: 1.9 change_penalty: 1.0 non_straight_penalty: 1.2 @@ -324,7 +324,7 @@ velocity_smoother: smoothing_frequency: 20.0 scale_velocities: False feedback: "OPEN_LOOP" - max_velocity: [0.75, 0.0, 2.5] + max_velocity: [0.40, 0.0, 2.5] min_velocity: [-0.75, 0.0, -2.5] max_accel: [2.5, 0.0, 3.2] max_decel: [-0.5, 0.0, -0.5] diff --git a/src/navigation/obstacle_nav2/launch/obstacle_nav2.launch.py b/src/navigation/obstacle_nav2/launch/obstacle_nav2.launch.py index 391609d..0bec55e 100755 --- a/src/navigation/obstacle_nav2/launch/obstacle_nav2.launch.py +++ b/src/navigation/obstacle_nav2/launch/obstacle_nav2.launch.py @@ -123,7 +123,7 @@ def generate_launch_description(): description='Reserved: path to map YAML file'), DeclareLaunchArgument( 'enable_motion', - default_value='false', + default_value='true', description='Route Nav2 cmd_vel to the real base topic'), DeclareLaunchArgument( 'start_base', diff --git a/src/origincar_base/launch/wall_localization.launch.py b/src/origincar_base/launch/wall_localization.launch.py index 2f7ce7b..cd5e79c 100644 --- a/src/origincar_base/launch/wall_localization.launch.py +++ b/src/origincar_base/launch/wall_localization.launch.py @@ -15,7 +15,7 @@ def generate_launch_description(): DeclareLaunchArgument('combined_odom_topic', default_value='/odom_combined'), DeclareLaunchArgument( 'wall_config', - default_value='/home/sunrise/yiliao_ws/src/origincar_base/config/wall_fit.json'), + default_value='/home/sunrise/yiliao_ws/src/origincar_base/config/wall_map_calibration (2).json'), DeclareLaunchArgument('port', default_value='8772'), Node( diff --git a/src/origincar_base/src/origincar_base.cpp b/src/origincar_base/src/origincar_base.cpp index 8613cc6..ea906de 100644 --- a/src/origincar_base/src/origincar_base.cpp +++ b/src/origincar_base/src/origincar_base.cpp @@ -126,7 +126,7 @@ void origincar_base::Akm_Cmd_Vel_Callback(const ackermann_msgs::msg::AckermannDr void origincar_base::Cmd_Vel_Callback(const geometry_msgs::msg::Twist::SharedPtr twist_aux) { - RCLCPP_INFO(this->get_logger(), "linarx: %.2f, angularz: %.2f ", twist_aux->linear.x, twist_aux->angular.z); +// RCLCPP_INFO(this->get_logger(), "linarx: %.2f, angularz: %.2f ", twist_aux->linear.x, twist_aux->angular.z); std::cout << "linerx" << twist_aux->linear.x << std::endl; std::cout << "angular" << twist_aux->angular.z << std::endl; short transition; diff --git a/src/vlm_detect/launch/local_vlm_adapter.launch.py b/src/vlm_detect/launch/local_vlm_adapter.launch.py new file mode 100644 index 0000000..4347570 --- /dev/null +++ b/src/vlm_detect/launch/local_vlm_adapter.launch.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Launch local VLM adapter backed by hobot_llamacpp. +""" + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, LogInfo +from launch.conditions import IfCondition +from launch.substitutions import LaunchConfiguration, PathJoinSubstitution +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory + + +def generate_launch_description(): + use_tts = LaunchConfiguration("use_tts") + use_hobot_llamacpp = LaunchConfiguration("use_hobot_llamacpp") + config_file = LaunchConfiguration("config_file") + + image_topic = LaunchConfiguration("image_topic") + trigger_topic = LaunchConfiguration("trigger_topic") + trigger_sign = LaunchConfiguration("trigger_sign") + prompt_text = LaunchConfiguration("prompt_text") + result_topic = LaunchConfiguration("result_topic") + + llamacpp_prompt_topic = LaunchConfiguration("llamacpp_prompt_topic") + llamacpp_image_topic = LaunchConfiguration("llamacpp_image_topic") + llamacpp_result_topic = LaunchConfiguration("llamacpp_result_topic") + llamacpp_text_topic = LaunchConfiguration("llamacpp_text_topic") + llamacpp_vit_model_file_name = LaunchConfiguration("llamacpp_vit_model_file_name") + llamacpp_gguf_model_file_name = LaunchConfiguration("llamacpp_gguf_model_file_name") + llamacpp_model_type = LaunchConfiguration("llamacpp_model_type") + llamacpp_threads = LaunchConfiguration("llamacpp_threads") + + audio_sink = LaunchConfiguration("audio_sink") + tts_speed = LaunchConfiguration("tts_speed") + + local_adapter = Node( + package="vlm_detect", + executable="local_vlm_adapter", + name="local_vlm_adapter", + output="screen", + parameters=[ + config_file, + { + "image_topic": image_topic, + "trigger_topic": trigger_topic, + "trigger_sign": trigger_sign, + "prompt_text": prompt_text, + "result_topic": result_topic, + "llamacpp_prompt_topic": llamacpp_prompt_topic, + "llamacpp_image_topic": llamacpp_image_topic, + "llamacpp_result_topic": llamacpp_result_topic, + }, + ], + ) + + hobot_llamacpp = Node( + package="hobot_llamacpp", + executable="hobot_llamacpp", + name="hobot_llamacpp", + output="screen", + condition=IfCondition(use_hobot_llamacpp), + parameters=[ + { + "feed_type": 1, + "is_shared_mem_sub": 0, + "pre_infer": 0, + "llm_threads": llamacpp_threads, + "model_type": llamacpp_model_type, + "user_prompt": "", + "system_prompt": "You are a helpful assistant.", + "ros_img_sub_topic_name": llamacpp_image_topic, + "ros_string_sub_topic_name": llamacpp_prompt_topic, + "ai_msg_pub_topic_name": llamacpp_result_topic, + "text_msg_pub_topic_name": llamacpp_text_topic, + "model_file_name": llamacpp_vit_model_file_name, + "llm_model_name": llamacpp_gguf_model_file_name, + } + ], + arguments=["--ros-args", "--log-level", "warn"], + ) + + tts_server = Node( + package="vlm_detect", + executable="tts_server", + name="tts_server", + output="screen", + condition=IfCondition(use_tts), + parameters=[ + config_file, + { + "audio_sink": audio_sink, + "tts_speed": tts_speed, + }, + ], + ) + + return LaunchDescription( + [ + DeclareLaunchArgument("use_tts", default_value="true"), + DeclareLaunchArgument("use_hobot_llamacpp", default_value="true"), + DeclareLaunchArgument( + "config_file", + default_value=PathJoinSubstitution( + [ + get_package_share_directory("vlm_detect"), + "config", + "vlm_detect.yaml", + ] + ), + ), + DeclareLaunchArgument("image_topic", default_value="/image_mjpeg"), + DeclareLaunchArgument("trigger_topic", default_value="/sign4return"), + DeclareLaunchArgument("trigger_sign", default_value="9"), + DeclareLaunchArgument( + "prompt_text", + default_value="描述图片中有一个病人的特征,字数控制在20字以内。", + ), + DeclareLaunchArgument("result_topic", default_value="/vlm_result"), + DeclareLaunchArgument("llamacpp_prompt_topic", default_value="/prompt_text"), + DeclareLaunchArgument("llamacpp_image_topic", default_value="/llamacpp/image"), + DeclareLaunchArgument("llamacpp_result_topic", default_value="/llama_cpp_node"), + DeclareLaunchArgument("llamacpp_text_topic", default_value="/tts_text"), + DeclareLaunchArgument( + "llamacpp_vit_model_file_name", + default_value="/home/sunrise/hobot_llamacpp/src/models/vit_model_int16_v2.bin", + ), + DeclareLaunchArgument( + "llamacpp_gguf_model_file_name", + default_value="/home/sunrise/hobot_llamacpp/src/models/Qwen2.5-0.5B-Instruct-Q4_0.gguf", + ), + DeclareLaunchArgument("llamacpp_model_type", default_value="0"), + DeclareLaunchArgument("llamacpp_threads", default_value="6"), + DeclareLaunchArgument( + "audio_sink", + default_value="alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo", + ), + DeclareLaunchArgument("tts_speed", default_value="1.5"), + LogInfo(msg=["Local VLM backend: hobot_llamacpp"]), + local_adapter, + hobot_llamacpp, + tts_server, + ] + ) diff --git a/src/vlm_detect/package.xml b/src/vlm_detect/package.xml index 3f6c53c..5d47caa 100644 --- a/src/vlm_detect/package.xml +++ b/src/vlm_detect/package.xml @@ -7,6 +7,13 @@ root TODO: License declaration + ai_msgs + cv_bridge + origincar_msg + rclpy + sensor_msgs + std_msgs + ament_copyright ament_flake8 ament_pep257 diff --git a/src/vlm_detect/setup.py b/src/vlm_detect/setup.py index b4b075d..4673135 100644 --- a/src/vlm_detect/setup.py +++ b/src/vlm_detect/setup.py @@ -25,6 +25,7 @@ setup( entry_points={ 'console_scripts': [ 'vlm_node = vlm_detect.vlm_node:main', + 'local_vlm_adapter = vlm_detect.local_vlm_adapter_node:main', 'test_publisher = vlm_detect.test_publisher:main', 'tts_node = vlm_detect.tts_node:main', 'tts_server = vlm_detect.tts_server:main', diff --git a/src/vlm_detect/test/__pycache__/test_local_adapter_utils.cpython-310-pytest-6.2.5.pyc b/src/vlm_detect/test/__pycache__/test_local_adapter_utils.cpython-310-pytest-6.2.5.pyc new file mode 100644 index 0000000..02f0f86 Binary files /dev/null and b/src/vlm_detect/test/__pycache__/test_local_adapter_utils.cpython-310-pytest-6.2.5.pyc differ diff --git a/src/vlm_detect/test/test_local_adapter_utils.py b/src/vlm_detect/test/test_local_adapter_utils.py new file mode 100644 index 0000000..26e274f --- /dev/null +++ b/src/vlm_detect/test/test_local_adapter_utils.py @@ -0,0 +1,25 @@ +from types import SimpleNamespace + +from vlm_detect.local_adapter_utils import extract_perception_text, is_trigger_match + + +def test_extracts_first_non_empty_target_type(): + msg = SimpleNamespace( + targets=[ + SimpleNamespace(type=""), + SimpleNamespace(type="病人坐在床边"), + ] + ) + + assert extract_perception_text(msg) == "病人坐在床边" + + +def test_returns_empty_string_when_no_target_text(): + msg = SimpleNamespace(targets=[SimpleNamespace(type="")]) + + assert extract_perception_text(msg) == "" + + +def test_trigger_match_casts_values_to_int(): + assert is_trigger_match("9", 9) + assert not is_trigger_match(8, 9) diff --git a/src/vlm_detect/vlm_backup/local_adapter_utils.py b/src/vlm_detect/vlm_backup/local_adapter_utils.py new file mode 100644 index 0000000..3e44ad8 --- /dev/null +++ b/src/vlm_detect/vlm_backup/local_adapter_utils.py @@ -0,0 +1,13 @@ +def extract_perception_text(msg): + for target in getattr(msg, "targets", []): + text = getattr(target, "type", "") + if text: + return text + return "" + + +def is_trigger_match(value, expected): + try: + return int(value) == int(expected) + except (TypeError, ValueError): + return False diff --git a/src/vlm_detect/vlm_backup/local_vlm_adapter_node.py b/src/vlm_detect/vlm_backup/local_vlm_adapter_node.py new file mode 100644 index 0000000..eeec997 --- /dev/null +++ b/src/vlm_detect/vlm_backup/local_vlm_adapter_node.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Local VLM adapter for hobot_llamacpp. + +This node keeps the vlm_detect external contract while delegating inference to +hobot_llamacpp over ROS topics. +""" + +import threading +import time + +import cv2 +import numpy as np +import rclpy +from ai_msgs.msg import PerceptionTargets +from cv_bridge import CvBridge +from origincar_msg.srv import Speak +from rclpy.node import Node +from sensor_msgs.msg import CompressedImage, Image +from std_msgs.msg import Int32, String + +from .local_adapter_utils import extract_perception_text, is_trigger_match + + +class LocalVLMAdapter(Node): + def __init__(self): + super().__init__("local_vlm_adapter") + + self.declare_parameter("image_topic", "/image_mjpeg") + self.declare_parameter("trigger_topic", "/sign4return") + self.declare_parameter("trigger_sign", 9) + self.declare_parameter( + "prompt_text", "请描述这张图片的内容,用一句简短的话概括,不超过20个字。" + ) + self.declare_parameter("result_topic", "/vlm_result") + self.declare_parameter("llamacpp_prompt_topic", "/prompt_text") + self.declare_parameter("llamacpp_image_topic", "/llamacpp/image") + self.declare_parameter("llamacpp_result_topic", "/llama_cpp_node") + self.declare_parameter("tts_service", "/tts/speak") + self.declare_parameter("enable_tts", True) + self.declare_parameter("inference_timeout_sec", 90.0) + self.declare_parameter("publish_delay_sec", 0.05) + + image_topic = self.get_parameter("image_topic").value + trigger_topic = self.get_parameter("trigger_topic").value + self.trigger_sign = self.get_parameter("trigger_sign").value + self.prompt_text = self.get_parameter("prompt_text").value + result_topic = self.get_parameter("result_topic").value + self.inference_timeout_sec = float( + self.get_parameter("inference_timeout_sec").value + ) + self.publish_delay_sec = float(self.get_parameter("publish_delay_sec").value) + self.enable_tts = bool(self.get_parameter("enable_tts").value) + + llamacpp_prompt_topic = self.get_parameter("llamacpp_prompt_topic").value + llamacpp_image_topic = self.get_parameter("llamacpp_image_topic").value + llamacpp_result_topic = self.get_parameter("llamacpp_result_topic").value + tts_service = self.get_parameter("tts_service").value + + self.bridge = CvBridge() + self.latest_image_msg = None + self.image_lock = threading.Lock() + self.inference_lock = threading.Lock() + self.waiting_for_result = False + self.pending_result = "" + self.result_event = threading.Event() + + self.image_sub = self.create_subscription( + CompressedImage, image_topic, self.image_callback, 10 + ) + self.trigger_sub = self.create_subscription( + Int32, trigger_topic, self.trigger_callback, 10 + ) + self.llamacpp_result_sub = self.create_subscription( + PerceptionTargets, llamacpp_result_topic, self.llamacpp_result_callback, 10 + ) + + self.prompt_pub = self.create_publisher(String, llamacpp_prompt_topic, 10) + self.image_pub = self.create_publisher(Image, llamacpp_image_topic, 10) + self.result_pub = self.create_publisher(String, result_topic, 10) + + self.tts_client = self.create_client(Speak, tts_service) + + self.get_logger().info( + "Local VLM adapter ready | image=%s | trigger=%s(sign=%s) | " + "prompt=%s | image_out=%s | result_in=%s | result_out=%s" + % ( + image_topic, + trigger_topic, + self.trigger_sign, + llamacpp_prompt_topic, + llamacpp_image_topic, + llamacpp_result_topic, + result_topic, + ) + ) + + def image_callback(self, msg): + with self.image_lock: + self.latest_image_msg = msg + + def trigger_callback(self, msg): + if not is_trigger_match(msg.data, self.trigger_sign): + return + + with self.inference_lock: + if self.waiting_for_result: + self.get_logger().warning("Local VLM inference already running") + return + self.waiting_for_result = True + self.pending_result = "" + self.result_event.clear() + + thread = threading.Thread(target=self._run_inference_once, daemon=True) + thread.start() + + def llamacpp_result_callback(self, msg): + text = extract_perception_text(msg) + if not text: + return + with self.inference_lock: + if not self.waiting_for_result: + return + self.pending_result = text + self.result_event.set() + + def _run_inference_once(self): + try: + image_msg = self._build_image_msg_from_latest() + if image_msg is None: + self.get_logger().warning("No cached image available for local VLM") + return + + prompt_msg = String() + prompt_msg.data = self.prompt_text + self.prompt_pub.publish(prompt_msg) + time.sleep(self.publish_delay_sec) + self.image_pub.publish(image_msg) + + self.get_logger().info("Local VLM request sent to hobot_llamacpp") + if not self.result_event.wait(timeout=self.inference_timeout_sec): + self.get_logger().error( + "Timed out waiting for hobot_llamacpp result after %.1fs" + % self.inference_timeout_sec + ) + return + + with self.inference_lock: + result = self.pending_result + + result_msg = String() + result_msg.data = result + self.result_pub.publish(result_msg) + self._speak_async(result) + self.get_logger().info("Local VLM result: %s" % result) + finally: + with self.inference_lock: + self.waiting_for_result = False + self.pending_result = "" + self.result_event.clear() + + def _build_image_msg_from_latest(self): + with self.image_lock: + compressed = self.latest_image_msg + if compressed is None: + return None + + np_arr = np.frombuffer(compressed.data, np.uint8) + cv_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) + if cv_image is None: + self.get_logger().error("Failed to decode cached compressed image") + return None + + image_msg = self.bridge.cv2_to_imgmsg(cv_image, encoding="bgr8") + image_msg.header = compressed.header + return image_msg + + def _speak_async(self, text): + if not self.enable_tts: + return + if not self.tts_client.service_is_ready(): + self.get_logger().warning("TTS service not available") + return + req = Speak.Request() + req.text = text + future = self.tts_client.call_async(req) + future.add_done_callback(self._tts_done_callback) + + def _tts_done_callback(self, future): + try: + resp = future.result() + if not resp.success: + self.get_logger().warning("TTS failed: %s" % resp.message) + except Exception as exc: + self.get_logger().error("TTS call error: %s" % exc) + + +def main(args=None): + rclpy.init(args=args) + node = LocalVLMAdapter() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/vlm_detect/vlm_detect/qr_tts_bridge.py b/src/vlm_detect/vlm_backup/qr_tts_bridge.py similarity index 100% rename from src/vlm_detect/vlm_detect/qr_tts_bridge.py rename to src/vlm_detect/vlm_backup/qr_tts_bridge.py diff --git a/src/vlm_detect/vlm_detect/tts_node.py b/src/vlm_detect/vlm_backup/tts_node.py similarity index 100% rename from src/vlm_detect/vlm_detect/tts_node.py rename to src/vlm_detect/vlm_backup/tts_node.py diff --git a/src/vlm_detect/vlm_detect/__pycache__/local_adapter_utils.cpython-310.pyc b/src/vlm_detect/vlm_detect/__pycache__/local_adapter_utils.cpython-310.pyc new file mode 100644 index 0000000..15a560f Binary files /dev/null and b/src/vlm_detect/vlm_detect/__pycache__/local_adapter_utils.cpython-310.pyc differ