1
0
forked from zbw/yiliao2026

清理了一些没有用到的vlm_detect文件

This commit is contained in:
2026-08-04 14:31:45 +08:00
parent 78cf69493b
commit f5f3d53ec0
21 changed files with 701 additions and 111 deletions

View File

@@ -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
if [ "$bound_count" -eq 0 ]; then
log "error: no $label interface on $port is bound to $desired_driver"
exit 7
fi
echo "[雷达驱动自动切换程序] 仍在等待${TTY_DEVICE}(已等待${elapsed_seconds}秒)..." > /dev/kmsg
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 "$@"

View File

@@ -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

View File

@@ -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"],
),

View File

@@ -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,
])

View File

@@ -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)

View File

@@ -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` 模型。成功结尾类似:

View File

@@ -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,7 +92,7 @@ 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:
enabled: true
@@ -101,7 +101,7 @@ controller_server:
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]

View File

@@ -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',

View File

@@ -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(

View File

@@ -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;

View File

@@ -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,
]
)

View File

@@ -7,6 +7,13 @@
<maintainer email="root@todo.todo">root</maintainer>
<license>TODO: License declaration</license>
<exec_depend>ai_msgs</exec_depend>
<exec_depend>cv_bridge</exec_depend>
<exec_depend>origincar_msg</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>sensor_msgs</exec_depend>
<exec_depend>std_msgs</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>

View File

@@ -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',

View File

@@ -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)

View File

@@ -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

View File

@@ -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()