forked from zbw/yiliao2026
清理了一些没有用到的vlm_detect文件
This commit is contained in:
@@ -1,101 +1,172 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# 退出码说明
|
# Physical USB ports on the RDKx5 carrier.
|
||||||
# 0 - 成功
|
RADAR_PORT="1-1.4"
|
||||||
# 1 - 错误:等待TTY设备就绪超时
|
IMU_PORT="1-1.1"
|
||||||
# 2 - 错误:未找到CH343驱动
|
|
||||||
# 3 - 错误:雷达端口上未找到CH34x设备
|
|
||||||
# 4 - 错误:未从cdc_acm找到雷达接口
|
|
||||||
# 5 - 错误:解绑cdc_acm驱动失败
|
|
||||||
# 6 - 错误:绑定usb_ch343驱动失败
|
|
||||||
|
|
||||||
# ── 配置 ────────────────────────────────────────────
|
# RADAR -> usb_ch343, IMU -> cdc_acm.
|
||||||
TTY_DEVICE="/dev/ttyACM*"
|
RADAR_DRIVER="usb_ch343"
|
||||||
CDC_ACM_PATH="/sys/bus/usb/drivers/cdc_acm"
|
IMU_DRIVER="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
|
|
||||||
|
|
||||||
# ── 初始化 ──────────────────────────────────────────
|
DRIVER_ROOT="/sys/bus/usb/drivers"
|
||||||
start_time=$(cut -d. -f1 /proc/uptime)
|
MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-30}"
|
||||||
|
WAIT_INTERVAL_SECONDS="${WAIT_INTERVAL_SECONDS:-1}"
|
||||||
|
INTERFACES=()
|
||||||
|
|
||||||
# ── 1. 等待 TTY 设备就绪 ────────────────────────────
|
log() {
|
||||||
while true; do
|
local message="[radar-driver-switch] $*"
|
||||||
for dev in /dev/ttyACM*; do
|
if [ -w /dev/kmsg ]; then
|
||||||
[ -e "$dev" ] || continue
|
printf '%s\n' "$message" > /dev/kmsg
|
||||||
if [ -c "$dev" ] && [ -r "$dev" ] && [ -w "$dev" ]; then
|
else
|
||||||
echo "[雷达驱动自动切换程序] 设备 $dev 已就绪!" > /dev/kmsg
|
printf '%s\n' "$message"
|
||||||
TTY_DEVICE="$dev"
|
fi
|
||||||
break 2
|
}
|
||||||
|
|
||||||
|
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
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ ${MAX_WAIT_SECONDS} -gt 0 ]; then
|
if [ "$bound_count" -eq 0 ]; then
|
||||||
current_time=$(cut -d. -f1 /proc/uptime)
|
log "error: no $label interface on $port is bound to $desired_driver"
|
||||||
elapsed_seconds=$((current_time - start_time))
|
exit 7
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
sleep ${CHECK_INTERVAL}
|
}
|
||||||
done
|
|
||||||
|
|
||||||
# ── 2. 确认 CH343 驱动已加载 ────────────────────────
|
main() {
|
||||||
if [ -d "${USB_CH343_PATH}" ]; then
|
load_driver "$RADAR_DRIVER"
|
||||||
echo "[雷达驱动自动切换程序] CH343驱动已加载,继续..." > /dev/kmsg
|
load_driver "$IMU_DRIVER"
|
||||||
else
|
require_driver "$RADAR_DRIVER"
|
||||||
echo "[雷达驱动自动切换程序] 错误:未找到CH343驱动!" > /dev/kmsg
|
require_driver "$IMU_DRIVER"
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. 通过物理端口定位雷达 CH34x ───────────────────
|
ensure_port_bound "RADAR" "$RADAR_PORT" "$RADAR_DRIVER"
|
||||||
radar_sysfs="/sys/bus/usb/devices/${RADAR_USB_PORT}"
|
ensure_port_bound "IMU" "$IMU_PORT" "$IMU_DRIVER"
|
||||||
|
|
||||||
if [ ! -d "${radar_sysfs}" ]; then
|
udevadm settle --timeout=5 2>/dev/null || true
|
||||||
echo "[雷达驱动自动切换程序] 错误:端口 ${RADAR_USB_PORT} 上无设备!" > /dev/kmsg
|
log "done; serial devices: $(ls /dev/ttyACM* /dev/ttyCH343USB* 2>/dev/null | tr '\n' ' ')"
|
||||||
exit 3
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
vendor_id=$(cat "${radar_sysfs}/idVendor" 2>/dev/null)
|
main "$@"
|
||||||
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
|
|
||||||
|
|||||||
@@ -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`
|
- `/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
|
- `/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:
|
Run on RDKx5:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -35,6 +35,46 @@ def generate_launch_description():
|
|||||||
default_value=camera_config,
|
default_value=camera_config,
|
||||||
description="ROS parameter file for the USB camera",
|
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(
|
IncludeLaunchDescription(
|
||||||
PythonLaunchDescriptionSource(
|
PythonLaunchDescriptionSource(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
@@ -51,6 +91,14 @@ def generate_launch_description():
|
|||||||
parameters=[
|
parameters=[
|
||||||
LaunchConfiguration("camera_config"),
|
LaunchConfiguration("camera_config"),
|
||||||
{"camera_calibration_file_path": calibration_file},
|
{"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"],
|
arguments=["--ros-args", "--log-level", "warn"],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import os
|
|||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
from ament_index_python.packages import get_package_share_directory
|
||||||
from launch import LaunchDescription
|
from launch import LaunchDescription
|
||||||
from launch.actions import IncludeLaunchDescription
|
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
|
from launch.substitutions import LaunchConfiguration
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
def generate_launch_description():
|
||||||
@@ -28,7 +29,17 @@ def generate_launch_description():
|
|||||||
"launch",
|
"launch",
|
||||||
"hobot_usb_cam.launch.py",
|
"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(
|
websocket_launch = IncludeLaunchDescription(
|
||||||
@@ -47,4 +58,15 @@ def generate_launch_description():
|
|||||||
}.items(),
|
}.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,
|
||||||
|
])
|
||||||
|
|||||||
Binary file not shown.
@@ -82,6 +82,17 @@ class UnifiedLaunchConfigTest(unittest.TestCase):
|
|||||||
self.assertIn("usb_camera.yaml", constants)
|
self.assertIn("usb_camera.yaml", constants)
|
||||||
self.assertIn("usb_camera_calibration.yaml", constants)
|
self.assertIn("usb_camera_calibration.yaml", constants)
|
||||||
self.assertIn("hobot_usb_cam", 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")
|
node_calls = calls_named(BASE_LAUNCH, "Node")
|
||||||
self.assertEqual(len(node_calls), 1)
|
self.assertEqual(len(node_calls), 1)
|
||||||
parameters = next(
|
parameters = next(
|
||||||
@@ -92,6 +103,17 @@ class UnifiedLaunchConfigTest(unittest.TestCase):
|
|||||||
parameters_tree = ast.dump(parameters)
|
parameters_tree = ast.dump(parameters)
|
||||||
self.assertIn("camera_config", parameters_tree)
|
self.assertIn("camera_config", parameters_tree)
|
||||||
self.assertIn("camera_calibration_file_path", 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):
|
def test_web_launch_only_adds_websocket(self):
|
||||||
constants = string_constants(WEB_LAUNCH)
|
constants = string_constants(WEB_LAUNCH)
|
||||||
@@ -100,14 +122,20 @@ class UnifiedLaunchConfigTest(unittest.TestCase):
|
|||||||
self.assertEqual(call_names(WEB_LAUNCH).count("Node"), 0)
|
self.assertEqual(call_names(WEB_LAUNCH).count("Node"), 0)
|
||||||
self.assertEqual(len(calls_named(WEB_LAUNCH, "IncludeLaunchDescription")), 2)
|
self.assertEqual(len(calls_named(WEB_LAUNCH, "IncludeLaunchDescription")), 2)
|
||||||
camera_include = assigned_call(WEB_LAUNCH, "camera_launch")
|
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 (
|
for forbidden in (
|
||||||
"hobot_codec",
|
"hobot_codec",
|
||||||
"/image_mjpeg",
|
"/image_mjpeg",
|
||||||
"yuyv2rgb",
|
"yuyv2rgb",
|
||||||
"usb_image_width",
|
|
||||||
"usb_image_height",
|
|
||||||
"usb_pixel_format",
|
|
||||||
):
|
):
|
||||||
self.assertNotIn(forbidden, constants)
|
self.assertNotIn(forbidden, constants)
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ cd ~/smart-healthcare-2026/src/mtran/dependencies
|
|||||||
./auto_install.sh
|
./auto_install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本会请求 `sudo` 安装非 ROS apt 包,在 `dependencies/tools` 安装固定版本 Bun,
|
不要使用 `sudo` 权限执行脚本,脚本会请求 `sudo` 安装非 ROS apt 包,在 `dependencies/tools` 安装固定版本 Bun,
|
||||||
从 `vendor/mtranserver` 构建当前 CPU 可运行的 sidecar,并只准备
|
从 `vendor/mtranserver` 构建当前 CPU 可运行的 sidecar,并只准备
|
||||||
`en -> zh-Hans` 模型。成功结尾类似:
|
`en -> zh-Hans` 模型。成功结尾类似:
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ controller_server:
|
|||||||
batch_size: 1000
|
batch_size: 1000
|
||||||
vx_std: 0.22
|
vx_std: 0.22
|
||||||
vy_std: 0.0
|
vy_std: 0.0
|
||||||
wz_std: 0.4
|
wz_std: 0.5
|
||||||
vx_max: 0.75
|
vx_max: 0.40
|
||||||
vx_min: -0.75
|
vx_min: -0.75
|
||||||
vy_max: 0.0
|
vy_max: 0.0
|
||||||
wz_max: 2.0
|
wz_max: 2.0
|
||||||
@@ -92,7 +92,7 @@ controller_server:
|
|||||||
trajectory_step: 5
|
trajectory_step: 5
|
||||||
time_step: 3
|
time_step: 3
|
||||||
AckermannConstraints:
|
AckermannConstraints:
|
||||||
min_turning_r: 0.4
|
min_turning_r: 0.6
|
||||||
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
|
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
|
||||||
ConstraintCritic:
|
ConstraintCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -101,7 +101,7 @@ controller_server:
|
|||||||
GoalCritic:
|
GoalCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 5.0
|
cost_weight: 3.0
|
||||||
threshold_to_consider: 1.4
|
threshold_to_consider: 1.4
|
||||||
GoalAngleCritic:
|
GoalAngleCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -111,7 +111,7 @@ controller_server:
|
|||||||
PreferForwardCritic:
|
PreferForwardCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 3.0
|
cost_weight: 6.0
|
||||||
threshold_to_consider: 0.5
|
threshold_to_consider: 0.5
|
||||||
CostCritic:
|
CostCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -140,7 +140,7 @@ controller_server:
|
|||||||
PathAngleCritic:
|
PathAngleCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 2.0
|
cost_weight: 6.0
|
||||||
offset_from_furthest: 4
|
offset_from_furthest: 4
|
||||||
threshold_to_consider: 0.5
|
threshold_to_consider: 0.5
|
||||||
max_angle_to_furthest: 1.0
|
max_angle_to_furthest: 1.0
|
||||||
@@ -175,12 +175,12 @@ local_costmap:
|
|||||||
transform_tolerance: 0.2
|
transform_tolerance: 0.2
|
||||||
default_obstacle_radius: 0.05
|
default_obstacle_radius: 0.05
|
||||||
minimum_obstacle_radius: 0.02
|
minimum_obstacle_radius: 0.02
|
||||||
maximum_obstacle_radius: 0.50
|
maximum_obstacle_radius: 0.06
|
||||||
extra_inflation: 0.02
|
extra_inflation: 0.02
|
||||||
inflation_layer:
|
inflation_layer:
|
||||||
plugin: "nav2_costmap_2d::InflationLayer"
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
cost_scaling_factor: 3.0
|
cost_scaling_factor: 3.0
|
||||||
inflation_radius: 0.55
|
inflation_radius: 0.2
|
||||||
always_send_full_costmap: True
|
always_send_full_costmap: True
|
||||||
local_costmap_client:
|
local_costmap_client:
|
||||||
ros__parameters:
|
ros__parameters:
|
||||||
@@ -245,7 +245,7 @@ planner_server:
|
|||||||
angle_quantization_bins: 72
|
angle_quantization_bins: 72
|
||||||
analytic_expansion_ratio: 3.5
|
analytic_expansion_ratio: 3.5
|
||||||
analytic_expansion_max_length: 3.0
|
analytic_expansion_max_length: 3.0
|
||||||
minimum_turning_radius: 0.40
|
minimum_turning_radius: 0.60
|
||||||
reverse_penalty: 1.9
|
reverse_penalty: 1.9
|
||||||
change_penalty: 1.0
|
change_penalty: 1.0
|
||||||
non_straight_penalty: 1.2
|
non_straight_penalty: 1.2
|
||||||
@@ -324,7 +324,7 @@ velocity_smoother:
|
|||||||
smoothing_frequency: 20.0
|
smoothing_frequency: 20.0
|
||||||
scale_velocities: False
|
scale_velocities: False
|
||||||
feedback: "OPEN_LOOP"
|
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]
|
min_velocity: [-0.75, 0.0, -2.5]
|
||||||
max_accel: [2.5, 0.0, 3.2]
|
max_accel: [2.5, 0.0, 3.2]
|
||||||
max_decel: [-0.5, 0.0, -0.5]
|
max_decel: [-0.5, 0.0, -0.5]
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def generate_launch_description():
|
|||||||
description='Reserved: path to map YAML file'),
|
description='Reserved: path to map YAML file'),
|
||||||
DeclareLaunchArgument(
|
DeclareLaunchArgument(
|
||||||
'enable_motion',
|
'enable_motion',
|
||||||
default_value='false',
|
default_value='true',
|
||||||
description='Route Nav2 cmd_vel to the real base topic'),
|
description='Route Nav2 cmd_vel to the real base topic'),
|
||||||
DeclareLaunchArgument(
|
DeclareLaunchArgument(
|
||||||
'start_base',
|
'start_base',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def generate_launch_description():
|
|||||||
DeclareLaunchArgument('combined_odom_topic', default_value='/odom_combined'),
|
DeclareLaunchArgument('combined_odom_topic', default_value='/odom_combined'),
|
||||||
DeclareLaunchArgument(
|
DeclareLaunchArgument(
|
||||||
'wall_config',
|
'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'),
|
DeclareLaunchArgument('port', default_value='8772'),
|
||||||
|
|
||||||
Node(
|
Node(
|
||||||
|
|||||||
@@ -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)
|
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 << "linerx" << twist_aux->linear.x << std::endl;
|
||||||
std::cout << "angular" << twist_aux->angular.z << std::endl;
|
std::cout << "angular" << twist_aux->angular.z << std::endl;
|
||||||
short transition;
|
short transition;
|
||||||
|
|||||||
145
src/vlm_detect/launch/local_vlm_adapter.launch.py
Normal file
145
src/vlm_detect/launch/local_vlm_adapter.launch.py
Normal 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,
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -7,6 +7,13 @@
|
|||||||
<maintainer email="root@todo.todo">root</maintainer>
|
<maintainer email="root@todo.todo">root</maintainer>
|
||||||
<license>TODO: License declaration</license>
|
<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_copyright</test_depend>
|
||||||
<test_depend>ament_flake8</test_depend>
|
<test_depend>ament_flake8</test_depend>
|
||||||
<test_depend>ament_pep257</test_depend>
|
<test_depend>ament_pep257</test_depend>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ setup(
|
|||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'vlm_node = vlm_detect.vlm_node:main',
|
'vlm_node = vlm_detect.vlm_node:main',
|
||||||
|
'local_vlm_adapter = vlm_detect.local_vlm_adapter_node:main',
|
||||||
'test_publisher = vlm_detect.test_publisher:main',
|
'test_publisher = vlm_detect.test_publisher:main',
|
||||||
'tts_node = vlm_detect.tts_node:main',
|
'tts_node = vlm_detect.tts_node:main',
|
||||||
'tts_server = vlm_detect.tts_server:main',
|
'tts_server = vlm_detect.tts_server:main',
|
||||||
|
|||||||
Binary file not shown.
25
src/vlm_detect/test/test_local_adapter_utils.py
Normal file
25
src/vlm_detect/test/test_local_adapter_utils.py
Normal 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)
|
||||||
13
src/vlm_detect/vlm_backup/local_adapter_utils.py
Normal file
13
src/vlm_detect/vlm_backup/local_adapter_utils.py
Normal 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
|
||||||
212
src/vlm_detect/vlm_backup/local_vlm_adapter_node.py
Normal file
212
src/vlm_detect/vlm_backup/local_vlm_adapter_node.py
Normal 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()
|
||||||
Binary file not shown.
Reference in New Issue
Block a user