分支开始

This commit is contained in:
flower
2026-02-02 10:41:39 +08:00
parent 9b5abe0877
commit 47016c00a6
463 changed files with 18 additions and 67629 deletions

68
skylink_ros2_ws/build.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
# SkyLink ROS 2 机载端 - 快速编译脚本
# 用法: ./build.sh [release|debug]
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_TYPE="${1:-release}"
echo "========================================"
echo "SkyLink ROS 2 编译脚本"
echo "========================================"
echo "工作目录: $SCRIPT_DIR"
echo "编译类型: $BUILD_TYPE"
echo ""
# 检查 ROS 2 环境
if [ -z "$ROS_DISTRO" ]; then
echo "❌ 错误:未检测到 ROS 2 环境"
echo "请先运行: source /opt/ros/humble/setup.bash"
exit 1
fi
echo "✓ ROS 2 发行版: $ROS_DISTRO"
# 检查依赖
echo ""
echo "正在检查系统依赖..."
if ! command -v colcon &> /dev/null; then
echo "❌ 错误:未找到 colcon请安装"
echo " sudo apt install -y python3-colcon-common"
exit 1
fi
echo "✓ colcon 已安装"
# 清理旧编译(可选)
if [ -d "$SCRIPT_DIR/build" ] && [ "$BUILD_TYPE" = "clean" ]; then
echo ""
echo "清理旧编译产物..."
rm -rf "$SCRIPT_DIR/build" "$SCRIPT_DIR/install" "$SCRIPT_DIR/log"
echo "✓ 清理完成"
fi
# 编译
echo ""
echo "正在编译 skylink_bridge 包..."
echo ""
if [ "$BUILD_TYPE" = "debug" ]; then
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Debug
else
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release
fi
echo ""
echo "========================================"
echo "✓ 编译成功!"
echo "========================================"
echo ""
echo "下一步:"
echo "1. 加载 ROS 环境:"
echo " source $SCRIPT_DIR/install/setup.bash"
echo ""
echo "2. 启动节点:"
echo " ros2 launch skylink_bridge bridge.launch.py"
echo ""

View File

@@ -0,0 +1,156 @@
cmake_minimum_required(VERSION 3.16)
project(skylink_bridge LANGUAGES CXX)
# ============================================================================
# 编译标准和优化选项
# ============================================================================
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
# 如果未指定编译类型,默认为 Release
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
# ============================================================================
# 查找依赖
# ============================================================================
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(image_transport REQUIRED)
find_package(mavros_msgs REQUIRED)
# OpenCV
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
# JPEG 库
find_package(JPEG REQUIRED)
# 线程库
find_package(Threads REQUIRED)
# ============================================================================
# 包含目录
# ============================================================================
include_directories(
include
)
# ============================================================================
# 源文件
# ============================================================================
set(SOURCES
src/udp_sender_node.cpp
src/camera_driver_node.cpp
src/device_info_sender_node.cpp
)
set(HEADERS
include/skylink_bridge/udp_sender_node.h
include/skylink_bridge/device_info_sender_node.h
include/protocol.h
)
# ============================================================================
# 创建节点可执行文件
# ============================================================================
add_executable(udp_sender_node src/udp_sender_node.cpp ${HEADERS})
add_executable(camera_driver_node src/camera_driver_node.cpp)
add_executable(device_info_sender_node src/device_info_sender_node.cpp)
# 链接库 - UDP Sender
target_link_libraries(udp_sender_node
${rclcpp_LIBRARIES}
${sensor_msgs_LIBRARIES}
${geometry_msgs_LIBRARIES}
${cv_bridge_LIBRARIES}
${image_transport_LIBRARIES}
${mavros_msgs_LIBRARIES}
${OpenCV_LIBRARIES}
${JPEG_LIBRARIES}
Threads::Threads
)
# 链接库 - Camera Driver
target_link_libraries(camera_driver_node
${rclcpp_LIBRARIES}
${sensor_msgs_LIBRARIES}
Threads::Threads
)
target_link_libraries(device_info_sender_node
${rclcpp_LIBRARIES}
Threads::Threads
)
# 目标级别的包含目录
ament_target_dependencies(udp_sender_node
rclcpp
sensor_msgs
geometry_msgs
cv_bridge
image_transport
mavros_msgs
)
ament_target_dependencies(camera_driver_node
rclcpp
sensor_msgs
)
ament_target_dependencies(device_info_sender_node
rclcpp
)
# ============================================================================
# 安装规则
# ============================================================================
# 安装可执行文件
install(TARGETS udp_sender_node camera_driver_node
DESTINATION lib/${PROJECT_NAME}
)
install(TARGETS device_info_sender_node
DESTINATION lib/${PROJECT_NAME}
)
# 安装启动文件
install(DIRECTORY launch/
DESTINATION share/${PROJECT_NAME}/launch
)
# 安装配置文件
install(DIRECTORY config/
DESTINATION share/${PROJECT_NAME}/config
)
# 安装头文件
install(DIRECTORY include/
DESTINATION include
)
# ============================================================================
# Ament 导出
# ============================================================================
ament_export_dependencies(
rclcpp
sensor_msgs
geometry_msgs
cv_bridge
image_transport
mavros_msgs
OpenCV
)
ament_package()

View File

@@ -0,0 +1,66 @@
/**:
ros__parameters:
# 全局默认参数(可被各节点覆盖)
log_level: "INFO"
enable_fps_stats: true
fps_stats_interval: 5
enable_drop_stats: true
/camera_driver:
ros__parameters:
# 相机驱动参数(对应 launch 中的 node name: camera_driver
video_device: "/dev/video6"
framerate: 30.0
image_width: 640
image_height: 480
camera_topic: "/camera/image_raw/compressed"
camera_frame: "camera"
enable_fps_stats: true
/udp_sender:
ros__parameters:
# 网络与发送配置(对应 udp_sender 节点)
target_ip: "255.255.255.255"
target_port: 9999
udp_buffer_size: 65536
sender_thread_name: "skylink_sender"
# 相机相关udp_sender 使用的 topic / 图像格式)
camera_topic: "/camera/image_raw/compressed"
camera_frame: "bgr8"
# JPEG 压缩配置
jpeg_quality: 80
adaptive_quality: false # 禁用自适应,使用固定质量以获得稳定帧率
quality_min: 30
quality_max: 95
# GPS / 定位
gps_topic: "/mavros/global_position/global"
enable_gps: true
default_latitude: 39.9042
default_longitude: 116.4074
default_altitude: 100.0
# 分片和重组
max_packet_size: 1472
frame_timeout_ms: 5000
# 性能优化参数
qos_keep_last: 10 # 增加消息队列深度
enable_batch_send: true # 启用批量发送优化
send_interval_ms: 0 # 发送间隔0 表示尽快发送)
/device_info_sender:
ros__parameters:
info_port: 10000
target_ip: "255.255.255.255"
broadcast_interval_ms: 1000
enable_broadcast: true
enable_request_response: true
device_path: "/dev/video6"
device_name: "Unknown"
width: 1280
height: 720
framerate: 30.0
pixel_format: "YUYV"

View File

@@ -0,0 +1,171 @@
/**
* @file protocol.h
* @brief SkyLink 多平台实时视频传输 - 公共协议定义
*
* 这个文件定义了机载端ROS 2、PC 端Qt和移动端Android之间的通信协议。
* 必须保持三端的字节对齐和字段顺序一致。
*
* @author flowzl
* @date 2026-01-19
*/
#pragma once
#include <cstdint>
#include <cstring>
/**
* @brief 强制1字节对齐避免编译器补齐导致的大小差异
*/
#pragma pack(push, 1)
/**
* @struct PacketHeader
* @brief UDP 数据包头部结构
*
* 总大小: 50 字节
* - 报头识别: 2 字节 (Magic)
* - 帧标识: 4 字节 (FrameID)
* - 分片信息: 4 字节 (TotalChunks + ChunkIndex)
* - 数据长度: 2 字节 (DataLen)
* - 时间戳: 8 字节 (Timestamp)
* - GPS 位置: 24 字节 (Lat, Lon, Alt)
* - 预留: 6 字节 (Reserve 用于将来扩展)
*/
struct PacketHeader {
// ============ 包头识别 ============
uint16_t magic; ///< 魔数 0xAA55 用于识别合法数据包
// ============ 帧标识 ============
uint32_t frame_id; ///< 唯一的帧编号,从 0 开始递增,用于帧同步
// ============ 分片信息 ============
uint16_t total_chunks; ///< 该帧总共被分成多少个 UDP 包
uint16_t chunk_index; ///< 当前数据包是第几个分片 (0-based)
// ============ 载荷信息 ============
uint16_t data_len; ///< 当前包的有效载荷长度(单位:字节)
// ============ 时间戳 ============
double timestamp; ///< GPS 时间戳 (秒数Unix 时间 或 ROS 时间)
// ============ GPS 位置数据 ============
double lat; ///< 纬度 (WGS84 坐标系)
double lon; ///< 经度 (WGS84 坐标系)
double alt; ///< 海拔高度 (米)
// ============ 校验与预留 ============
uint16_t crc16; ///< CRC16 校验码 (可选,当前未使用)
uint32_t reserve; ///< 保留字段,用于将来的扩展
/**
* @brief 获取包头大小(字节数)
* @return 包头固定大小50 字节
*/
static constexpr uint16_t size() {
return sizeof(PacketHeader);
}
};
/**
* @brief 设备信息包头结构UDP 10000
*/
struct DeviceInfoPacket {
uint16_t magic; ///< 魔数 0xAA56
uint32_t sequence; ///< 序列号
uint16_t width; ///< 图像宽度
uint16_t height; ///< 图像高度
float framerate; ///< 帧率
uint32_t pixel_format; ///< 像素格式FourCC
char device_path[32]; ///< 设备路径(/dev/videoX
char device_name[64]; ///< 设备名称
float current_fps; ///< 当前实际 FPS
uint32_t total_frames; ///< 总帧数
uint64_t timestamp; ///< 时间戳(毫秒)
uint16_t crc16; ///< CRC 校验(可选)
static constexpr uint16_t size() {
return sizeof(DeviceInfoPacket);
}
};
/**
* @brief 恢复默认的对齐方式
*/
#pragma pack(pop)
// ============================================================================
// 协议常量定义
// ============================================================================
/** @brief 数据包魔数,用于识别合法的 SkyLink 数据包 */
constexpr uint16_t SKYLINK_MAGIC = 0xAA55;
/** @brief 设备信息魔数,用于识别设备信息数据包 */
constexpr uint16_t DEVICE_INFO_MAGIC = 0xAA56;
/** @brief 设备信息请求魔数,用于请求立即发送设备信息 */
constexpr uint16_t DEVICE_INFO_REQUEST = 0xAA57;
/** @brief 最大的单个 UDP 包大小 (包含报头) - MTU 1500 字节 */
constexpr uint16_t MAX_UDP_PACKET_SIZE = 1472; // 1500 - 20 (IP) - 8 (UDP)
/** @brief 最大的载荷大小 */
constexpr uint16_t MAX_PAYLOAD_SIZE = MAX_UDP_PACKET_SIZE - PacketHeader::size();
/** @brief UDP 接收缓冲区大小 */
constexpr uint32_t RX_BUFFER_SIZE = 65536;
/** @brief 最大支持的分片数 */
constexpr uint16_t MAX_CHUNKS = 1000;
/** @brief 帧超时时间(毫秒),超过此时间的分片将被丢弃 */
constexpr uint32_t FRAME_TIMEOUT_MS = 5000;
// ============================================================================
// 图像编码相关常量
// ============================================================================
/** @brief JPEG 图像格式 ID */
constexpr uint8_t IMAGE_FORMAT_JPEG = 0x01;
/** @brief JPEG 最大压缩质量 */
constexpr uint8_t JPEG_MAX_QUALITY = 100;
/** @brief JPEG 最小压缩质量 */
constexpr uint8_t JPEG_MIN_QUALITY = 10;
// ============================================================================
// 辅助函数
// ============================================================================
/**
* @brief 验证包头魔数是否合法
* @param header 待验证的包头结构
* @return 如果魔数为 0xAA55 返回 true否则返回 false
*/
inline bool isValidPacket(const PacketHeader& header) {
return header.magic == SKYLINK_MAGIC;
}
/**
* @brief 计算 CRC16 校验码 (可选)
* @param data 数据指针
* @param len 数据长度
* @return 16 位 CRC 校验结果
*/
inline uint16_t crc16_ccitt(const uint8_t* data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = crc << 1;
}
crc &= 0xFFFF;
}
}
return crc;
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include <rclcpp/rclcpp.hpp>
#include <atomic>
#include <netinet/in.h>
#include <string>
#include <thread>
#include "protocol.h"
/**
* @brief 设备信息发送节点
*
* 通过 UDP 端口 10000 定期广播设备信息并响应请求包DEVICE_INFO_REQUEST
*/
class DeviceInfoSenderNode : public rclcpp::Node {
public:
DeviceInfoSenderNode();
~DeviceInfoSenderNode();
private:
void load_parameters();
void start_listener();
void stop_listener();
void broadcast_timer_callback();
void handle_request_loop();
void send_device_info(const sockaddr_in& addr);
DeviceInfoPacket build_packet();
static uint64_t htonll(uint64_t value);
static uint32_t fourcc_from_string(const std::string& fmt);
private:
// 套接字
int udp_socket_{-1};
std::atomic<bool> running_{false};
std::thread listener_thread_;
// 参数
uint16_t info_port_{10000};
std::string target_ip_{"255.255.255.255"};
bool enable_broadcast_{true};
bool enable_request_response_{true};
int broadcast_interval_ms_{1000};
// 设备信息
std::string device_path_{"/dev/video0"};
std::string device_name_{"Unknown"};
uint16_t width_{0};
uint16_t height_{0};
float framerate_{0.0f};
uint32_t pixel_format_{0};
float current_fps_{0.0f};
uint32_t total_frames_{0};
uint32_t sequence_{0};
rclcpp::TimerBase::SharedPtr broadcast_timer_;
};

View File

@@ -0,0 +1,237 @@
/**
* @file udp_sender_node.hpp
* @brief SkyLink ROS 2 UDP 发送节点 - 头文件
*
* 这个节点订阅相机和 GPS Topic将图像编码为 JPEG 并通过 UDP 广播发送。
*
* @author flowzl
* @date 2026-01-19
*/
#pragma once
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/compressed_image.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <mavros_msgs/msg/global_position_target.hpp>
#include <sensor_msgs/msg/nav_sat_fix.hpp>
#include <cv_bridge/cv_bridge.h>
#include <opencv2/opencv.hpp>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
#include <cstring>
// 协议定义
#include "protocol.h"
/**
* @class UdpSenderNode
* @brief ROS 2 UDP 发送节点
*
* 负责:
* 1. 订阅相机和 GPS 数据
* 2. 编码图像为 JPEG
* 3. 分片打包为 UDP 数据包
* 4. 通过网络发送数据包
*/
class UdpSenderNode : public rclcpp::Node {
public:
/**
* @brief 构造函数
*/
UdpSenderNode();
/**
* @brief 析构函数
*/
~UdpSenderNode();
private:
// ========================================================================
// ROS 2 订阅和发布
// ========================================================================
/** 相机图像订阅 */
rclcpp::Subscription<sensor_msgs::msg::CompressedImage>::SharedPtr camera_subscriber_;
/** GPS 全球位置订阅 */
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr gps_subscriber_;
// ========================================================================
// 参数
// ========================================================================
std::string target_ip_; ///< 目标 IP 地址
uint16_t target_port_; ///< 目标 UDP 端口
std::string camera_topic_; ///< 相机 Topic 名称
std::string gps_topic_; ///< GPS Topic 名称
int jpeg_quality_; ///< JPEG 压缩质量 (1-100)
bool adaptive_quality_; ///< 自适应质量标志
int quality_min_, quality_max_; ///< 质量范围
bool enable_gps_; ///< 是否启用 GPS
uint16_t max_packet_size_; ///< 最大 UDP 包大小
uint32_t frame_timeout_ms_; ///< 帧超时时间
bool enable_fps_stats_; ///< FPS 统计
bool enable_drop_stats_; ///< 丢弃帧统计
// ========================================================================
// 套接字和网络
// ========================================================================
int udp_socket_; ///< UDP 套接字文件描述符
uint32_t frame_counter_; ///< 帧计数器
std::atomic<uint32_t> bytes_sent_{0}; ///< 已发送字节数
std::atomic<uint32_t> packets_sent_{0}; ///< 已发送包数
std::atomic<uint32_t> frames_dropped_{0}; ///< 丢弃的帧数
// ========================================================================
// 异步发送队列和线程
// ========================================================================
/** UDP 数据包队列项 */
struct PacketQueueItem {
std::vector<uint8_t> data;
std::chrono::system_clock::time_point enqueue_time;
};
std::queue<PacketQueueItem> send_queue_; ///< 发送队列
std::mutex send_queue_mutex_; ///< 队列互斥锁
std::condition_variable send_queue_cv_; ///< 队列条件变量
std::thread sender_thread_; ///< 后台发送线程
std::atomic<bool> sender_running_{true}; ///< 线程运行标志
static const size_t MAX_QUEUE_SIZE = 5000; ///< 队列最大大小
static const size_t BATCH_SEND_SIZE = 128; ///< 批量发送大小 (增加以减少锁竞争)
/**
* @brief 后台发送工作函数
*/
void sender_worker();
/**
* @brief 将数据包加入发送队列
* @param data 数据指针
* @param len 数据长度
*/
void enqueue_packet(const uint8_t* data, uint16_t len);
/**
* @brief 批量将数据包加入发送队列
* @param packets 数据包列表 (将被移动)
*/
void enqueue_packets(std::vector<std::vector<uint8_t>>& packets);
// ========================================================================
// GPS 数据缓存
// ========================================================================
struct {
double latitude = 39.9042; ///< 纬度
double longitude = 116.4074; ///< 经度
double altitude = 100.0; ///< 海拔
std::mutex mutex;
} current_gps_data_;
// ========================================================================
// 统计信息
// ========================================================================
struct {
uint32_t total_frames = 0;
uint32_t total_bytes = 0;
uint32_t total_packets = 0;
std::chrono::system_clock::time_point last_report_time;
} stats_;
std::mutex stats_mutex_;
// ========================================================================
// 回调函数
// ========================================================================
/**
* @brief 相机图像回调
*/
void camera_callback(const sensor_msgs::msg::CompressedImage::SharedPtr msg);
/**
* @brief GPS 回调
* @param msg GPS 消息
*/
void gps_callback(const sensor_msgs::msg::NavSatFix::SharedPtr msg);
// ========================================================================
// 网络函数
// ========================================================================
/**
* @brief 初始化 UDP 套接字
* @return 成功返回 true
*/
bool init_udp_socket();
/**
* @brief 关闭 UDP 套接字
*/
void close_udp_socket();
/**
* @brief 发送 UDP 数据包
* @param data 数据指针
* @param len 数据长度
* @return 发送的字节数,失败返回 -1
*/
int send_udp_packet(const uint8_t* data, uint16_t len);
// ========================================================================
// 图像处理函数
// ========================================================================
/**
* @brief 将 OpenCV Mat 编码为 JPEG
* @param frame OpenCV 图像
* @param quality 压缩质量
* @param output 输出 JPEG 数据
* @return 成功返回 true
*/
bool encode_to_jpeg(const cv::Mat& frame, int quality, std::vector<uint8_t>& output);
/**
* @brief 将 JPEG 数据分片为 UDP 包并发送
* @param jpeg_data JPEG 数据指针
* @param jpeg_len JPEG 数据长度
* @param timestamp 时间戳
* @param lat 纬度
* @param lon 经度
* @param alt 海拔
* @return 发送的包数
*/
uint16_t fragment_and_send(
const uint8_t* jpeg_data,
uint32_t jpeg_len,
double timestamp,
double lat,
double lon,
double alt
);
// ========================================================================
// 统计和监控
// ========================================================================
/**
* @brief 更新统计信息
* @param bytes_count 新增字节数
* @param packet_count 新增包数
*/
void update_stats(uint32_t bytes_count, uint32_t packet_count);
/**
* @brief 定期报告统计信息
*/
void report_stats();
};

View File

@@ -0,0 +1,108 @@
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
import os
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
"""
SkyLink Bridge 启动配置
用法:
ros2 launch skylink_bridge bridge.launch.py
ros2 launch skylink_bridge bridge.launch.py target_ip:=192.168.1.100 jpeg_quality:=80
"""
# 获取包共享目录
pkg_share_dir = get_package_share_directory('skylink_bridge')
config_file = os.path.join(pkg_share_dir, 'config', 'params.yaml')
# ========================================================================
# 启动参数声明 (可通过命令行覆盖)
# ========================================================================
target_ip_arg = DeclareLaunchArgument(
'target_ip',
default_value='255.255.255.255',
description='目标 IP 地址 (广播)'
)
target_port_arg = DeclareLaunchArgument(
'target_port',
default_value='9999',
description='UDP 目标端口'
)
jpeg_quality_arg = DeclareLaunchArgument(
'jpeg_quality',
default_value='70',
description='JPEG 压缩质量 (1-100)'
)
camera_topic_arg = DeclareLaunchArgument(
'camera_topic',
default_value='/camera/image_raw',
description='相机 Topic 名称'
)
gps_topic_arg = DeclareLaunchArgument(
'gps_topic',
default_value='/mavros/global_position/global',
description='GPS Topic 名称'
)
# ========================================================================
# 相机 节点 (OpenCV 驱动)
# ========================================================================
camera_driver_node = Node(
package='skylink_bridge',
executable='camera_driver_node',
name='camera_driver',
output='screen',
parameters=[
config_file, # 从 YAML 配置文件加载参数
{
'camera_topic': LaunchConfiguration('camera_topic'),
}
],
)
# ========================================================================
# UDP Sender 节点
# ========================================================================
udp_sender_node = Node(
package='skylink_bridge',
executable='udp_sender_node',
name='udp_sender',
output='screen',
parameters=[
config_file, # 从 YAML 配置文件加载参数
{
'target_ip': LaunchConfiguration('target_ip'),
'target_port': LaunchConfiguration('target_port'),
'jpeg_quality': LaunchConfiguration('jpeg_quality'),
'camera_topic': LaunchConfiguration('camera_topic'),
'gps_topic': LaunchConfiguration('gps_topic'),
}
],
remappings=[
# 如果需要重映射 Topic在此添加
# ('/camera/image_raw', '/my_camera/image'),
]
)
# ========================================================================
# 构造启动描述
# ========================================================================
return LaunchDescription([
target_ip_arg,
target_port_arg,
jpeg_quality_arg,
camera_topic_arg,
gps_topic_arg,
camera_driver_node,
udp_sender_node,
])

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>skylink_bridge</name>
<version>1.0.0</version>
<description>
SkyLink 实时视频传输系统 - ROS 2 机载端
负责采集相机和 GPS 数据,通过 UDP 广播发送到地面站和移动端
</description>
<!-- 维护者信息 -->
<maintainer email="support@skylink.dev">flowzl</maintainer>
<license>MIT</license>
<!-- 作者 -->
<author email="dev@skylink.dev">SkyLink Development</author>
<!-- 编译工具 -->
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- ROS 2 依赖 -->
<depend>rclcpp</depend>
<depend>sensor_msgs</depend>
<depend>geometry_msgs</depend>
<depend>cv_bridge</depend>
<depend>image_transport</depend>
<!-- 系统库依赖 -->
<exec_depend>opencv-dev</exec_depend>
<exec_depend>libjpeg-turbo-dev</exec_depend>
<!-- 可选MAVROS GPS 支持 -->
<depend>mavros_msgs</depend>
<!-- 可选USB 相机支持 -->
<exec_depend>usb_cam</exec_depend>
<!-- 导出信息 -->
<export>
<build_type>ament_cmake</build_type>
</export>
</package>

View File

@@ -0,0 +1,494 @@
/**
* @file camera_driver_node.cpp
* @brief V4L2 相机驱动节点 - 发布 ROS 2 CompressedImage 消息
*
* 使用 V4L2 直接读取 USB 摄像头 MJPEG 流,发布为 ROS 2 sensor_msgs/CompressedImage
*
* @author flowzl
* @date 2026-01-19
*/
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/compressed_image.hpp>
#include <thread>
#include <atomic>
#include <chrono>
#include <vector>
// V4L2 includes
#include <linux/videodev2.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/stat.h>
/**
* @class CameraDriverNode
* @brief V4L2 相机驱动 ROS 2 节点 (Raw MJPEG)
*/
class CameraDriverNode : public rclcpp::Node {
public:
/**
* @brief 构造函数
*/
CameraDriverNode() : rclcpp::Node("camera_driver") {
// ====================================================================
// 声明参数
// ====================================================================
this->declare_parameter<std::string>("video_device", "/dev/video4");
this->declare_parameter<double>("framerate", 30.0);
this->declare_parameter<int>("image_width", 640);
this->declare_parameter<int>("image_height", 480);
this->declare_parameter<std::string>("camera_topic", "/camera/image_raw/compressed");
this->declare_parameter<std::string>("camera_frame", "camera");
this->declare_parameter<bool>("enable_fps_stats", true);
// 读取参数
video_device_ = this->get_parameter("video_device").as_string();
target_framerate_ = this->get_parameter("framerate").as_double();
image_width_ = this->get_parameter("image_width").as_int();
image_height_ = this->get_parameter("image_height").as_int();
camera_topic_ = this->get_parameter("camera_topic").as_string();
camera_frame_ = this->get_parameter("camera_frame").as_string();
enable_fps_stats_ = this->get_parameter("enable_fps_stats").as_bool();
// ====================================================================
// 初始化摄像头
// ====================================================================
RCLCPP_INFO(this->get_logger(), "======================================");
RCLCPP_INFO(this->get_logger(), "V4L2 Native Camera Driver Node 启动");
RCLCPP_INFO(this->get_logger(), "======================================");
RCLCPP_INFO(this->get_logger(), "设备: %s", video_device_.c_str());
RCLCPP_INFO(this->get_logger(), "分辨率: %dx%d", image_width_, image_height_);
RCLCPP_INFO(this->get_logger(), "帧率: %.1f", target_framerate_);
if (!init_camera()) {
RCLCPP_ERROR(this->get_logger(), "❌ 摄像头初始化失败");
rclcpp::shutdown();
return;
}
RCLCPP_INFO(this->get_logger(), "✓ 摄像头初始化成功");
// ====================================================================
// 创建发布者
// ====================================================================
// 修改为默认 QoS (Reliable, KeepLast=10) 以匹配 Python 行为和 ros2 topic hz 工具
// 之前使用 SensorDataQoS (BestEffort) 导致 ros2 topic hz (默认 Reliable) 无法正常接收
rclcpp::QoS qos_profile(10);
// 使用 CompressedImage 发布
image_publisher_ = this->create_publisher<sensor_msgs::msg::CompressedImage>(
camera_topic_,
qos_profile
);
RCLCPP_INFO(this->get_logger(), "✓ CompressedImage 发布器已创建: %s (Reliable)", camera_topic_.c_str());
// ====================================================================
// 启动捕获线程
// ====================================================================
capturing_ = true;
capture_thread_ = std::thread(&CameraDriverNode::capture_worker, this);
RCLCPP_INFO(this->get_logger(), "✓ 捕获线程已启动");
RCLCPP_INFO(this->get_logger(), "✓ 节点初始化完成");
RCLCPP_INFO(this->get_logger(), "======================================");
}
/**
* @brief 析构函数
*/
~CameraDriverNode() {
capturing_ = false;
if (capture_thread_.joinable()) {
capture_thread_.join();
}
stop_capturing();
uninit_device();
close_device();
RCLCPP_INFO(this->get_logger(), "Camera Driver Node 已关闭");
}
// 设备信息查询接口
const std::string& device_path() const { return video_device_; }
const std::string& device_name() const { return device_name_; }
uint16_t image_width() const { return static_cast<uint16_t>(image_width_); }
uint16_t image_height() const { return static_cast<uint16_t>(image_height_); }
float target_fps() const { return static_cast<float>(target_framerate_); }
uint32_t pixel_format_fourcc() const { return pixel_format_fourcc_; }
float current_fps() const { return current_fps_; }
uint32_t total_frames() const { return frame_counter_; }
private:
// ========================================================================
// V4L2 结构
// ========================================================================
struct Buffer {
void *start;
size_t length;
};
// ========================================================================
// 成员变量
// ========================================================================
// V4L2 members
int fd_ = -1;
std::vector<Buffer> buffers_;
/** 摄像头参数 */
std::string video_device_;
std::string device_name_{"Unknown"};
double target_framerate_;
int image_width_;
int image_height_;
std::string camera_topic_;
std::string camera_frame_;
bool enable_fps_stats_;
/** 发布者 */
rclcpp::Publisher<sensor_msgs::msg::CompressedImage>::SharedPtr image_publisher_;
/** 捕获线程 */
std::thread capture_thread_;
std::atomic<bool> capturing_{false};
/** 统计信息 */
uint32_t frame_counter_ = 0;
std::chrono::system_clock::time_point last_stats_time_;
float current_fps_ = 0.0f;
uint32_t pixel_format_fourcc_ = V4L2_PIX_FMT_MJPEG;
// ========================================================================
// 私有方法
// ========================================================================
// xioctl helper
static int xioctl(int fh, int request, void *arg) {
int r;
do {
r = ioctl(fh, request, arg);
} while (-1 == r && EINTR == errno);
return r;
}
/**
* @brief 关闭设备
*/
void close_device() {
if (fd_ != -1) {
close(fd_);
fd_ = -1;
}
}
/**
* @brief 释放内存映射
*/
void uninit_device() {
for (const auto& buffer : buffers_) {
if (-1 == munmap(buffer.start, buffer.length)) {
perror("munmap");
}
}
buffers_.clear();
}
/**
* @brief 停止流
*/
void stop_capturing() {
if (fd_ != -1) {
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
xioctl(fd_, VIDIOC_STREAMOFF, &type);
}
}
/**
* @brief 初始化摄像头
*/
bool init_camera() {
struct stat st;
if (-1 == stat(video_device_.c_str(), &st)) {
RCLCPP_ERROR(this->get_logger(), "无法识别 '%s': %d, %s", video_device_.c_str(), errno, strerror(errno));
return false;
}
if (!S_ISCHR(st.st_mode)) {
RCLCPP_ERROR(this->get_logger(), "%s 不是字符设备", video_device_.c_str());
return false;
}
fd_ = open(video_device_.c_str(), O_RDWR | O_NONBLOCK, 0);
if (-1 == fd_) {
RCLCPP_ERROR(this->get_logger(), "无法打开 '%s': %d, %s", video_device_.c_str(), errno, strerror(errno));
return false;
}
struct v4l2_capability cap;
if (-1 == xioctl(fd_, VIDIOC_QUERYCAP, &cap)) {
RCLCPP_ERROR(this->get_logger(), "不是 V4L2 设备");
return false;
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
RCLCPP_ERROR(this->get_logger(), "不是视频捕获设备");
return false;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
RCLCPP_ERROR(this->get_logger(), "不支持流 I/O");
return false;
}
device_name_ = reinterpret_cast<const char*>(cap.card);
// 设置格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = image_width_;
fmt.fmt.pix.height = image_height_;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // 强制 MJPEG
fmt.fmt.pix.field = V4L2_FIELD_ANY; // V4L2_FIELD_NONE? ANY common for USB cams
if (-1 == xioctl(fd_, VIDIOC_S_FMT, &fmt)) {
RCLCPP_ERROR(this->get_logger(), "设置像素格式失败");
return false;
}
// 检查实际设置的格式
if (-1 == xioctl(fd_, VIDIOC_G_FMT, &fmt)) {
RCLCPP_WARN(this->get_logger(), "无法获取实际格式");
} else {
char fourcc[5] = {0};
fourcc[0] = (fmt.fmt.pix.pixelformat) & 0xFF;
fourcc[1] = (fmt.fmt.pix.pixelformat >> 8) & 0xFF;
fourcc[2] = (fmt.fmt.pix.pixelformat >> 16) & 0xFF;
fourcc[3] = (fmt.fmt.pix.pixelformat >> 24) & 0xFF;
RCLCPP_INFO(this->get_logger(), "实际设置的格式: %s (0x%08X), 分辨率: %dx%d",
fourcc, fmt.fmt.pix.pixelformat,
fmt.fmt.pix.width, fmt.fmt.pix.height);
if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG) {
RCLCPP_WARN(this->get_logger(),
"⚠️ 设备不支持MJPEG格式实际格式: %s可能需要使用其他格式或软件编码",
fourcc);
}
pixel_format_fourcc_ = fmt.fmt.pix.pixelformat;
}
// 设置帧率
struct v4l2_streamparm streamparm;
memset(&streamparm, 0, sizeof(streamparm));
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = (unsigned int)target_framerate_;
if (xioctl(fd_, VIDIOC_S_PARM, &streamparm) != -1) {
double actual_fps = (double)streamparm.parm.capture.timeperframe.denominator /
streamparm.parm.capture.timeperframe.numerator;
RCLCPP_INFO(this->get_logger(), "尝试设置FPS: %.1f, 实际FPS: %.1f", target_framerate_, actual_fps);
}
// 请求 buffers
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd_, VIDIOC_REQBUFS, &req)) {
RCLCPP_ERROR(this->get_logger(), "VIDIOC_REQBUFS 失败");
return false;
}
if (req.count < 2) {
RCLCPP_WARN(this->get_logger(), "Buffer 数量过少: %d", req.count);
}
buffers_.resize(req.count);
for (size_t i = 0; i < req.count; ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (-1 == xioctl(fd_, VIDIOC_QUERYBUF, &buf)) {
RCLCPP_ERROR(this->get_logger(), "VIDIOC_QUERYBUF 失败");
return false;
}
buffers_[i].length = buf.length;
buffers_[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
fd_, buf.m.offset);
if (MAP_FAILED == buffers_[i].start) {
RCLCPP_ERROR(this->get_logger(), "mmap 失败");
return false;
}
}
RCLCPP_INFO(this->get_logger(), "实际设置 - 分辨率: %dx%d, MJPEG",
fmt.fmt.pix.width, fmt.fmt.pix.height);
// 开始流
for (size_t i = 0; i < buffers_.size(); ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (-1 == xioctl(fd_, VIDIOC_QBUF, &buf)) {
RCLCPP_ERROR(this->get_logger(), "VIDIOC_QBUF 失败");
return false;
}
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd_, VIDIOC_STREAMON, &type)) {
RCLCPP_ERROR(this->get_logger(), "VIDIOC_STREAMON 失败");
return false;
}
return true;
}
/**
* @brief 捕获工作线程
*/
void capture_worker() {
RCLCPP_INFO(this->get_logger(), "📷 捕获线程开始工作 (V4L2)");
last_stats_time_ = std::chrono::system_clock::now();
uint32_t last_frame_counter = 0;
while (capturing_ && rclcpp::ok()) {
fd_set fds;
struct timeval tv;
int r;
FD_ZERO(&fds);
FD_SET(fd_, &fds);
tv.tv_sec = 2; // Timeout
tv.tv_usec = 0;
r = select(fd_ + 1, &fds, NULL, NULL, &tv);
if (-1 == r) {
if (EINTR == errno) continue;
RCLCPP_ERROR(this->get_logger(), "select error");
break;
}
if (0 == r) {
RCLCPP_WARN(this->get_logger(), "select timeout");
continue;
}
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd_, VIDIOC_DQBUF, &buf)) {
if (errno == EAGAIN) continue;
RCLCPP_ERROR(this->get_logger(), "VIDIOC_DQBUF error: %d, %s", errno, strerror(errno));
continue; // try next
}
// 数据在 buffers_[buf.index].start长度 buf.bytesused
try {
// 验证MJPEG数据格式每300帧检查一次
if (frame_counter_ % 300 == 0 && buf.bytesused >= 2) {
const uint8_t* data_ptr = (const uint8_t*)buffers_[buf.index].start;
RCLCPP_INFO(this->get_logger(),
"V4L2 MJPEG数据检查 - 大小: %u bytes, 前2字节: 0x%02X 0x%02X",
buf.bytesused, data_ptr[0], data_ptr[1]);
}
auto msg = std::make_unique<sensor_msgs::msg::CompressedImage>();
msg->header.stamp = this->now();
msg->header.frame_id = camera_frame_;
msg->format = "jpeg"; // MJPEG stream
// 直接使用V4L2原始数据不进行二次解码
msg->data.resize(buf.bytesused);
memcpy(msg->data.data(), buffers_[buf.index].start, buf.bytesused);
// 发布消息
image_publisher_->publish(std::move(msg));
frame_counter_++;
// 定期报告统计 (每 300 帧,约 10 秒)
if (enable_fps_stats_ && (frame_counter_ % 300 == 0)) {
auto now = std::chrono::system_clock::now();
auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(
now - last_stats_time_
).count();
if (elapsed_seconds > 0) {
uint32_t current_frames = frame_counter_ - last_frame_counter;
uint32_t fps = current_frames / elapsed_seconds;
current_fps_ = static_cast<float>(fps);
RCLCPP_INFO(this->get_logger(),
"相机统计 - 实时 FPS: %u, 总帧: %u, 每帧: %u KB",
fps, frame_counter_, buf.bytesused/1024
);
last_stats_time_ = now;
last_frame_counter = frame_counter_;
}
}
} catch (const std::exception& e) {
RCLCPP_ERROR(this->get_logger(), "发布失败: %s", e.what());
}
// 放回队列
if (-1 == xioctl(fd_, VIDIOC_QBUF, &buf)) {
RCLCPP_ERROR(this->get_logger(), "VIDIOC_QBUF error");
}
}
RCLCPP_INFO(this->get_logger(), "📷 捕获线程已停止");
}
/**
* @brief 报告统计信息 (Legacy)
*/
void report_stats() {
auto now = std::chrono::system_clock::now();
auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(
now - last_stats_time_
).count();
if (elapsed_seconds > 0) {
uint32_t fps = frame_counter_ / elapsed_seconds;
RCLCPP_INFO(this->get_logger(),
"相机统计 - FPS: %u, 总帧: %u",
fps, frame_counter_
);
}
}
};
/**
* @brief 主函数
*/
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = std::make_shared<CameraDriverNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

View File

@@ -0,0 +1,251 @@
#include "skylink_bridge/device_info_sender_node.h"
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
using namespace std::chrono_literals;
namespace {
uint16_t read_uint16(const std::string& s) {
try {
return static_cast<uint16_t>(std::stoi(s));
} catch (...) {
return 0;
}
}
} // namespace
DeviceInfoSenderNode::DeviceInfoSenderNode()
: rclcpp::Node("device_info_sender_node") {
load_parameters();
// 创建 UDP 套接字
udp_socket_ = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket_ < 0) {
RCLCPP_FATAL(this->get_logger(), "创建 UDP 套接字失败: %s", strerror(errno));
throw std::runtime_error("socket create failed");
}
// 允许广播
int opt = 1;
setsockopt(udp_socket_, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
// 绑定监听端口
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(info_port_);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(udp_socket_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
RCLCPP_FATAL(this->get_logger(), "绑定端口 %u 失败: %s", info_port_, strerror(errno));
throw std::runtime_error("bind failed");
}
// 定期广播
if (enable_broadcast_) {
broadcast_timer_ = this->create_wall_timer(
std::chrono::milliseconds(broadcast_interval_ms_),
std::bind(&DeviceInfoSenderNode::broadcast_timer_callback, this));
}
// 请求响应线程
if (enable_request_response_) {
running_.store(true);
listener_thread_ = std::thread(&DeviceInfoSenderNode::handle_request_loop, this);
}
RCLCPP_INFO(this->get_logger(),
"设备信息发送节点启动,端口: %u, 广播: %s, 请求响应: %s",
info_port_, enable_broadcast_ ? "开启" : "关闭",
enable_request_response_ ? "开启" : "关闭");
}
DeviceInfoSenderNode::~DeviceInfoSenderNode() {
running_.store(false);
if (listener_thread_.joinable()) {
listener_thread_.join();
}
if (udp_socket_ >= 0) {
close(udp_socket_);
}
}
void DeviceInfoSenderNode::load_parameters() {
this->declare_parameter<int>("info_port", 10000);
this->declare_parameter<std::string>("target_ip", "255.255.255.255");
this->declare_parameter<bool>("enable_broadcast", true);
this->declare_parameter<bool>("enable_request_response", true);
this->declare_parameter<int>("broadcast_interval_ms", 1000);
this->declare_parameter<std::string>("device_path", "/dev/video0");
this->declare_parameter<std::string>("device_name", "Unknown");
this->declare_parameter<int>("width", 640);
this->declare_parameter<int>("height", 480);
this->declare_parameter<double>("framerate", 30.0);
this->declare_parameter<std::string>("pixel_format", "YUYV");
// 读取
info_port_ = static_cast<uint16_t>(this->get_parameter("info_port").as_int());
target_ip_ = this->get_parameter("target_ip").as_string();
enable_broadcast_ = this->get_parameter("enable_broadcast").as_bool();
enable_request_response_ = this->get_parameter("enable_request_response").as_bool();
broadcast_interval_ms_ = this->get_parameter("broadcast_interval_ms").as_int();
device_path_ = this->get_parameter("device_path").as_string();
device_name_ = this->get_parameter("device_name").as_string();
width_ = static_cast<uint16_t>(this->get_parameter("width").as_int());
height_ = static_cast<uint16_t>(this->get_parameter("height").as_int());
framerate_ = static_cast<float>(this->get_parameter("framerate").as_double());
pixel_format_ = fourcc_from_string(this->get_parameter("pixel_format").as_string());
current_fps_ = 0.0f;
total_frames_ = 0;
}
void DeviceInfoSenderNode::broadcast_timer_callback() {
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(info_port_);
addr.sin_addr.s_addr = inet_addr(target_ip_.c_str());
send_device_info(addr);
}
void DeviceInfoSenderNode::send_device_info(const sockaddr_in& addr) {
DeviceInfoPacket packet = build_packet();
// 手动序列化为网络字节序
std::vector<uint8_t> buffer(sizeof(DeviceInfoPacket), 0);
uint8_t* p = buffer.data();
auto write_u16 = [](uint8_t*& ptr, uint16_t v) {
uint16_t n = htons(v);
std::memcpy(ptr, &n, sizeof(n));
ptr += sizeof(n);
};
auto write_u32 = [](uint8_t*& ptr, uint32_t v) {
uint32_t n = htonl(v);
std::memcpy(ptr, &n, sizeof(n));
ptr += sizeof(n);
};
auto write_float = [](uint8_t*& ptr, float f) {
uint32_t bits;
std::memcpy(&bits, &f, sizeof(bits));
uint32_t n = htonl(bits);
std::memcpy(ptr, &n, sizeof(n));
ptr += sizeof(n);
};
auto write_u64 = [](uint8_t*& ptr, uint64_t v) {
uint64_t n = DeviceInfoSenderNode::htonll(v);
std::memcpy(ptr, &n, sizeof(n));
ptr += sizeof(n);
};
write_u16(p, packet.magic);
write_u32(p, packet.sequence);
write_u16(p, packet.width);
write_u16(p, packet.height);
write_float(p, packet.framerate);
write_u32(p, packet.pixel_format);
std::memcpy(p, packet.device_path, sizeof(packet.device_path));
p += sizeof(packet.device_path);
std::memcpy(p, packet.device_name, sizeof(packet.device_name));
p += sizeof(packet.device_name);
write_float(p, packet.current_fps);
write_u32(p, packet.total_frames);
write_u64(p, packet.timestamp);
write_u16(p, packet.crc16);
ssize_t sent = sendto(udp_socket_, buffer.data(), buffer.size(), 0,
reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (sent < 0) {
RCLCPP_WARN(this->get_logger(), "发送设备信息失败: %s", strerror(errno));
}
}
void DeviceInfoSenderNode::handle_request_loop() {
// 设置非阻塞
int flags = fcntl(udp_socket_, F_GETFL, 0);
fcntl(udp_socket_, F_SETFL, flags | O_NONBLOCK);
std::vector<uint8_t> buf(512);
while (running_.load()) {
sockaddr_in sender{};
socklen_t sender_len = sizeof(sender);
ssize_t n = recvfrom(udp_socket_, buf.data(), buf.size(), 0,
reinterpret_cast<sockaddr*>(&sender), &sender_len);
if (n <= 0) {
std::this_thread::sleep_for(50ms);
continue;
}
if (n < 2) {
continue;
}
uint16_t magic = ntohs(*reinterpret_cast<uint16_t*>(buf.data()));
if (magic == DEVICE_INFO_REQUEST) {
send_device_info(sender);
}
}
}
DeviceInfoPacket DeviceInfoSenderNode::build_packet() {
DeviceInfoPacket packet{};
packet.magic = DEVICE_INFO_MAGIC;
packet.sequence = sequence_++;
packet.width = width_;
packet.height = height_;
packet.framerate = framerate_;
packet.pixel_format = pixel_format_;
std::memset(packet.device_path, 0, sizeof(packet.device_path));
std::strncpy(packet.device_path, device_path_.c_str(), sizeof(packet.device_path) - 1);
std::memset(packet.device_name, 0, sizeof(packet.device_name));
std::strncpy(packet.device_name, device_name_.c_str(), sizeof(packet.device_name) - 1);
packet.current_fps = current_fps_;
packet.total_frames = total_frames_;
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch());
packet.timestamp = static_cast<uint64_t>(now.count());
packet.crc16 = 0;
return packet;
}
uint64_t DeviceInfoSenderNode::htonll(uint64_t value) {
static const int num = 42;
if (*reinterpret_cast<const char*>(&num) == num) {
// little-endian
uint32_t high_part = htonl(static_cast<uint32_t>(value >> 32));
uint32_t low_part = htonl(static_cast<uint32_t>(value & 0xFFFFFFFFULL));
return (static_cast<uint64_t>(low_part) << 32) | high_part;
} else {
return value;
}
}
uint32_t DeviceInfoSenderNode::fourcc_from_string(const std::string& fmt) {
if (fmt.size() >= 4) {
return (static_cast<uint32_t>(fmt[0]) & 0xFF) |
((static_cast<uint32_t>(fmt[1]) & 0xFF) << 8) |
((static_cast<uint32_t>(fmt[2]) & 0xFF) << 16) |
((static_cast<uint32_t>(fmt[3]) & 0xFF) << 24);
}
// 默认 YUYV
return ('Y') | ('U' << 8) | ('Y' << 16) | ('V' << 24);
}
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = std::make_shared<DeviceInfoSenderNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

View File

@@ -0,0 +1,555 @@
/**
* @file udp_sender_node.cpp
* @brief SkyLink ROS 2 UDP 发送节点 - 实现文件
*
* 完整实现相机和 GPS 数据采集、JPEG 编码、UDP 分片发送的功能。
*
* @author flowzl
* @date 2026-01-19
*/
#include "skylink_bridge/udp_sender_node.h"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <sensor_msgs/msg/nav_sat_fix.hpp>
#include <thread>
/**
* @brief 构造函数
*/
UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1), frame_counter_(0) {
// ========================================================================
// 声明和读取参数
// ========================================================================
this->declare_parameter<std::string>("target_ip", "255.255.255.255");
this->declare_parameter<int>("target_port", 9999);
this->declare_parameter<std::string>("camera_topic", "/camera/image_raw");
this->declare_parameter<std::string>("gps_topic", "/mavros/global_position/global");
this->declare_parameter<int>("jpeg_quality", 70);
this->declare_parameter<bool>("adaptive_quality", true);
this->declare_parameter<int>("quality_min", 30);
this->declare_parameter<int>("quality_max", 95);
this->declare_parameter<bool>("enable_gps", true);
this->declare_parameter<int>("max_packet_size", 500);
this->declare_parameter<int>("frame_timeout_ms", 5000);
this->declare_parameter<bool>("enable_fps_stats", true);
this->declare_parameter<bool>("enable_drop_stats", true);
// 读取参数
target_ip_ = this->get_parameter("target_ip").as_string();
target_port_ = static_cast<uint16_t>(this->get_parameter("target_port").as_int());
camera_topic_ = this->get_parameter("camera_topic").as_string();
gps_topic_ = this->get_parameter("gps_topic").as_string();
jpeg_quality_ = this->get_parameter("jpeg_quality").as_int();
adaptive_quality_ = this->get_parameter("adaptive_quality").as_bool();
quality_min_ = this->get_parameter("quality_min").as_int();
quality_max_ = this->get_parameter("quality_max").as_int();
enable_gps_ = this->get_parameter("enable_gps").as_bool();
max_packet_size_ = static_cast<uint16_t>(this->get_parameter("max_packet_size").as_int());
frame_timeout_ms_ = static_cast<uint32_t>(this->get_parameter("frame_timeout_ms").as_int());
enable_fps_stats_ = this->get_parameter("enable_fps_stats").as_bool();
enable_drop_stats_ = this->get_parameter("enable_drop_stats").as_bool();
// ========================================================================
// 打印初始化信息
// ========================================================================
RCLCPP_INFO(this->get_logger(), "======================================");
RCLCPP_INFO(this->get_logger(), "SkyLink UDP Sender Node 启动");
RCLCPP_INFO(this->get_logger(), "======================================");
RCLCPP_INFO(this->get_logger(), "目标 IP: %s:%d", target_ip_.c_str(), target_port_);
RCLCPP_INFO(this->get_logger(), "相机 Topic: %s", camera_topic_.c_str());
RCLCPP_INFO(this->get_logger(), "GPS Topic: %s", gps_topic_.c_str());
RCLCPP_INFO(this->get_logger(), "JPEG 质量: %d (自适应: %s)", jpeg_quality_, adaptive_quality_ ? "true" : "false");
RCLCPP_INFO(this->get_logger(), "最大包大小: %d 字节", max_packet_size_);
// ========================================================================
// 初始化 UDP 套接字
// ========================================================================
if (!init_udp_socket()) {
RCLCPP_ERROR(this->get_logger(), "❌ UDP 套接字初始化失败,节点退出");
rclcpp::shutdown();
return;
}
RCLCPP_INFO(this->get_logger(), "✓ UDP 套接字初始化成功");
// ========================================================================
// 创建订阅
// ========================================================================
// 使用更激进的 QoS 设置,保留更多消息
rclcpp::QoS qos_profile = rclcpp::SensorDataQoS();
qos_profile.keep_last(10); // 保留最后 10 条消息
camera_subscriber_ = this->create_subscription<sensor_msgs::msg::CompressedImage>(
camera_topic_,
qos_profile,
[this](const sensor_msgs::msg::CompressedImage::SharedPtr msg) {
this->camera_callback(msg);
}
);
RCLCPP_INFO(this->get_logger(), "✓ 订阅相机 Topic: %s (Compressed)", camera_topic_.c_str());
if (enable_gps_) {
gps_subscriber_ = this->create_subscription<sensor_msgs::msg::NavSatFix>(
gps_topic_,
rclcpp::SensorDataQoS(),
[this](const sensor_msgs::msg::NavSatFix::SharedPtr msg) {
this->gps_callback(msg);
}
);
RCLCPP_INFO(this->get_logger(), "✓ 订阅 GPS Topic: %s", gps_topic_.c_str());
}
// ========================================================================
// 启动后台发送线程
// ========================================================================
sender_running_ = true;
sender_thread_ = std::thread(&UdpSenderNode::sender_worker, this);
RCLCPP_INFO(this->get_logger(), "✓ 后台发送线程已启动");
// ========================================================================
// 初始化统计
// ========================================================================
stats_.last_report_time = std::chrono::system_clock::now();
RCLCPP_INFO(this->get_logger(), "✓ 节点初始化完成,等待数据...");
RCLCPP_INFO(this->get_logger(), "======================================");
}
/**
* @brief 析构函数
*/
UdpSenderNode::~UdpSenderNode() {
// 停止后台发送线程
sender_running_ = false;
send_queue_cv_.notify_one(); // 唤醒等待的线程
if (sender_thread_.joinable()) {
sender_thread_.join();
RCLCPP_INFO(this->get_logger(), "✓ 后台发送线程已停止");
}
close_udp_socket();
RCLCPP_INFO(this->get_logger(), "UDP Sender Node 已关闭");
}
/**
* @brief 初始化 UDP 套接字
*/
bool UdpSenderNode::init_udp_socket() {
// 创建 UDP 套接字
udp_socket_ = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket_ < 0) {
RCLCPP_ERROR(this->get_logger(), "套接字创建失败: %s", strerror(errno));
return false;
}
// 开启广播权限
int broadcast_permission = 1;
if (setsockopt(udp_socket_, SOL_SOCKET, SO_BROADCAST, &broadcast_permission, sizeof(broadcast_permission)) < 0) {
RCLCPP_WARN(this->get_logger(), "设置 SO_BROADCAST 失败");
}
// 设置套接字可重用
int opt = 1;
if (setsockopt(udp_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
RCLCPP_WARN(this->get_logger(), "设置 SO_REUSEADDR 失败");
}
// 设置发送缓冲区
// 增加缓冲区大小到 4MB (4 * 1024 * 1024) 以应对高码率 MJPEG 流
int buffer_size = 4 * 1024 * 1024;
if (setsockopt(udp_socket_, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size)) < 0) {
RCLCPP_WARN(this->get_logger(), "设置发送缓冲区失败 (尝试 4MB): %s", strerror(errno));
// Fallback to smaller buffer if failed
buffer_size = 512 * 1024;
setsockopt(udp_socket_, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size));
} else {
// 验证实际设置的大小 (Linux 内核通常会将其翻倍)
int actual_buf_size = 0;
socklen_t opt_len = sizeof(actual_buf_size);
if (getsockopt(udp_socket_, SOL_SOCKET, SO_SNDBUF, &actual_buf_size, &opt_len) == 0) {
RCLCPP_INFO(this->get_logger(), "UDP 发送缓冲区已设置为: %d bytes", actual_buf_size);
}
}
// 设置非阻塞模式
int flags = fcntl(udp_socket_, F_GETFL, 0);
fcntl(udp_socket_, F_SETFL, flags | O_NONBLOCK);
return true;
}
/**
* @brief 关闭 UDP 套接字
*/
void UdpSenderNode::close_udp_socket() {
if (udp_socket_ >= 0) {
close(udp_socket_);
udp_socket_ = -1;
}
}
/**
* @brief 发送 UDP 数据包
*/
int UdpSenderNode::send_udp_packet(const uint8_t* data, uint16_t len) {
if (udp_socket_ < 0 || !data || len == 0) {
return -1;
}
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(target_port_);
addr.sin_addr.s_addr = inet_addr(target_ip_.c_str());
int retries = 0;
const int MAX_RETRIES = 5;
ssize_t sent = -1;
while (retries < MAX_RETRIES) {
sent = sendto(udp_socket_, data, len, 0, (struct sockaddr*)&addr, sizeof(addr));
if (sent > 0) {
bytes_sent_ += sent;
packets_sent_++;
return sent;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 资源暂时不可用,等待并重试
retries++;
// 短暂休眠 1ms给内核一点时间清空缓冲区
std::this_thread::sleep_for(std::chrono::microseconds(1000));
continue;
} else {
// 其他错误,直接退出
break;
}
}
// 如果重试后仍然失败
if (sent <= 0) {
// 简单的错误限流日志
static auto last_error_time = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - last_error_time).count() > 5) {
RCLCPP_WARN(this->get_logger(), "UDP 发送失败 (即使重试后): %s (errno=%d)", strerror(errno), errno);
last_error_time = now;
}
}
return sent;
}
/**
* @brief 批量将数据包加入发送队列
*/
void UdpSenderNode::enqueue_packets(std::vector<std::vector<uint8_t>>& packets) {
{
std::lock_guard<std::mutex> lock(send_queue_mutex_);
if (send_queue_.size() + packets.size() > MAX_QUEUE_SIZE) {
frames_dropped_++;
// 队列满时,丢弃整个 batch避免部分帧发送
return;
}
for (auto& packet_data : packets) {
PacketQueueItem item;
// 使用 move 避免拷贝
item.data = std::move(packet_data);
item.enqueue_time = std::chrono::system_clock::now();
send_queue_.push(std::move(item));
}
}
// 通知发送线程有新数据
send_queue_cv_.notify_one();
}
/**
* @brief 将数据包加入发送队列
*/
void UdpSenderNode::enqueue_packet(const uint8_t* data, uint16_t len) {
{
std::lock_guard<std::mutex> lock(send_queue_mutex_);
if (send_queue_.size() >= MAX_QUEUE_SIZE) {
frames_dropped_++;
return;
}
PacketQueueItem item;
item.data.assign(data, data + len);
item.enqueue_time = std::chrono::system_clock::now();
send_queue_.push(item);
}
// 通知发送线程有新数据
send_queue_cv_.notify_one();
}
/**
* @brief 后台发送工作线程
*/
void UdpSenderNode::sender_worker() {
RCLCPP_INFO(this->get_logger(), "发送线程开始运行 PID: %d", (int)getpid());
// 预分配 batch 内存以减少循环中的分配
std::vector<PacketQueueItem> batch;
batch.reserve(BATCH_SEND_SIZE);
while (sender_running_) {
try {
batch.clear();
{
std::unique_lock<std::mutex> lock(send_queue_mutex_);
// 如果队列为空,等待条件变量唤醒(等待最多 100ms
if (send_queue_.empty()) {
send_queue_cv_.wait_for(lock, std::chrono::milliseconds(100),
[this] { return !send_queue_.empty() || !sender_running_; });
}
// 批量获取数据包(一次最多 BATCH_SEND_SIZE 个)
while (!send_queue_.empty() && batch.size() < BATCH_SEND_SIZE) {
// 使用 move 避免通过拷贝构造函数复制数据,大幅提高性能
batch.push_back(std::move(send_queue_.front()));
send_queue_.pop();
}
}
// 发送批量数据包(不在锁内进行)
for (const auto& item : batch) {
send_udp_packet(item.data.data(), item.data.size());
}
} catch (const std::exception& e) {
RCLCPP_ERROR(this->get_logger(), "发送线程发生异常: %s", e.what());
// 发生异常后稍微休眠一下避免死循环疯狂报错
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} catch (...) {
RCLCPP_ERROR(this->get_logger(), "发送线程发生未知异常");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
RCLCPP_INFO(this->get_logger(), "发送线程已退出");
}
/**
* @brief 相机图像回调
*/
void UdpSenderNode::camera_callback(const sensor_msgs::msg::CompressedImage::SharedPtr msg) {
try {
if (msg->data.empty()) {
RCLCPP_WARN(this->get_logger(), "收到空帧");
return;
}
// 直接使用压缩数据,不再解码/重编码
const std::vector<uint8_t>& jpeg_data = msg->data;
// 获取当前 GPS 数据
double lat, lon, alt;
{
std::lock_guard<std::mutex> lock(current_gps_data_.mutex);
lat = current_gps_data_.latitude;
lon = current_gps_data_.longitude;
alt = current_gps_data_.altitude;
}
// 分片并发送
double timestamp = rclcpp::Clock().now().seconds();
uint16_t chunks_sent_local = fragment_and_send(
jpeg_data.data(),
jpeg_data.size(),
timestamp,
lat,
lon,
alt
);
(void)chunks_sent_local;
// 记录本帧发送统计,便于调试(每帧输出可能较多,建议仅在调试时启用)
// RCLCPP_INFO(this->get_logger(), "帧信息: jpeg_size=%zu bytes, chunks_sent=%u", jpeg_data.size(), chunks_sent_local);
frame_counter_++;
stats_.total_frames++;
stats_.total_bytes += jpeg_data.size();
// 定期打印统计
if (enable_fps_stats_ && (frame_counter_ % 300 == 0)) {
report_stats();
}
} catch (const std::exception& e) {
RCLCPP_ERROR(this->get_logger(), "相机回调异常: %s", e.what());
frames_dropped_++;
}
}
/**
* @brief GPS 回调
*/
void UdpSenderNode::gps_callback(const sensor_msgs::msg::NavSatFix::SharedPtr msg) {
std::lock_guard<std::mutex> lock(current_gps_data_.mutex);
current_gps_data_.latitude = msg->latitude;
current_gps_data_.longitude = msg->longitude;
current_gps_data_.altitude = msg->altitude;
}
/**
* @brief 将 Mat 编码为 JPEG
*/
bool UdpSenderNode::encode_to_jpeg(const cv::Mat& frame, int quality, std::vector<uint8_t>& output) {
try {
std::vector<int> params{cv::IMWRITE_JPEG_QUALITY, quality};
cv::imencode(".jpg", frame, output, params);
return !output.empty();
} catch (const cv::Exception& e) {
RCLCPP_ERROR(this->get_logger(), "OpenCV 编码异常: %s", e.what());
return false;
}
}
/**
* @brief 分片并发送
*/
uint16_t UdpSenderNode::fragment_and_send(
const uint8_t* jpeg_data,
uint32_t jpeg_len,
double timestamp,
double lat,
double lon,
double alt) {
// 计算需要的分片数
uint16_t payload_size = max_packet_size_ - sizeof(PacketHeader);
uint16_t total_chunks = (jpeg_len + payload_size - 1) / payload_size;
if (total_chunks > MAX_CHUNKS) {
RCLCPP_WARN(this->get_logger(), "帧太大,超过最大分片数");
return 0;
}
// 预分配所有分片,进行批量入队
std::vector<std::vector<uint8_t>> frame_packets;
frame_packets.reserve(total_chunks);
uint16_t chunks_sent = 0;
// 创建每个分片
for (uint16_t i = 0; i < total_chunks; i++) {
uint16_t current_payload_size = std::min(
(uint32_t)payload_size,
jpeg_len - (uint32_t)i * payload_size
);
// 构造包头
PacketHeader header;
header.magic = SKYLINK_MAGIC;
header.frame_id = frame_counter_;
header.total_chunks = total_chunks;
header.chunk_index = i;
header.data_len = current_payload_size;
header.timestamp = timestamp;
header.lat = lat;
header.lon = lon;
header.alt = alt;
header.crc16 = 0; // TODO: 计算 CRC
header.reserve = 0;
// 创建完整数据包
std::vector<uint8_t> packet(sizeof(PacketHeader) + current_payload_size);
std::memcpy(packet.data(), &header, sizeof(PacketHeader));
std::memcpy(
packet.data() + sizeof(PacketHeader),
jpeg_data + i * payload_size,
current_payload_size
);
frame_packets.push_back(std::move(packet)); // Move 到 vector 中
chunks_sent++;
}
// 批量入队,减少锁的竞争
enqueue_packets(frame_packets);
return chunks_sent;
}
/**
* @brief 报告统计信息
*/
void UdpSenderNode::report_stats() {
auto now = std::chrono::system_clock::now();
auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(
now - stats_.last_report_time
).count();
// 获取当前队列深度
size_t current_queue_depth = 0;
{
std::lock_guard<std::mutex> lock(send_queue_mutex_);
current_queue_depth = send_queue_.size();
}
if (elapsed_seconds > 0) {
// 计算本周期的帧数和字节数(不是累计值)
uint32_t frames_this_period = stats_.total_frames;
uint32_t bytes_this_period = stats_.total_bytes;
// 计算实时 FPS 和吞吐量
uint32_t fps = frames_this_period / elapsed_seconds;
uint32_t kbps = (bytes_this_period / elapsed_seconds) / 1024;
// 计算队列使用率(百分比)
float queue_utilization = (current_queue_depth * 100.0f) / MAX_QUEUE_SIZE;
RCLCPP_INFO(this->get_logger(),
"发送统计 - FPS: %u, 速率: %u KB/s, 总帧: %u, 总包: %u, 丢弃: %u, 队列: %zu/%zu (%.1f%%)",
fps, kbps, frame_counter_, packets_sent_.load(), frames_dropped_.load(),
current_queue_depth, MAX_QUEUE_SIZE, queue_utilization
);
// 如果队列接近满,发出警告
if (queue_utilization > 80.0f) {
RCLCPP_WARN(this->get_logger(),
"发送队列接近饱和 (%.1f%%),建议检查网络或增加发送线程",
queue_utilization
);
}
// 重置周期统计
stats_.total_frames = 0;
stats_.total_bytes = 0;
stats_.last_report_time = now;
}
}
/**
* @brief 主函数
*/
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = std::make_shared<UdpSenderNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

View File

@@ -0,0 +1,234 @@
import cv2
import sys
import platform
def list_available_cameras(max_test=10):
"""
检测所有可用的相机设备
"""
available_cameras = []
print("=== 开始检测可用相机设备 ===")
for i in range(max_test):
# 根据操作系统选择不同的后端
if platform.system() == "Windows":
cap = cv2.VideoCapture(i, cv2.CAP_DSHOW)
elif platform.system() == "Linux":
cap = cv2.VideoCapture(i, cv2.CAP_V4L2)
else: # macOS
cap = cv2.VideoCapture(i, cv2.CAP_AVFOUNDATION)
if cap.isOpened():
# 获取相机基本信息
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
print(f"✅ 相机 {i}: 可用")
available_cameras.append(i)
cap.release()
else:
cap.release()
print(f"检测完成,找到 {len(available_cameras)} 个可用相机: {available_cameras}")
return available_cameras
def get_camera_properties(camera_index):
"""
获取相机的基本属性和当前设置
"""
if platform.system() == "Windows":
cap = cv2.VideoCapture(camera_index, cv2.CAP_DSHOW)
elif platform.system() == "Linux":
cap = cv2.VideoCapture(camera_index, cv2.CAP_V4L2)
else:
cap = cv2.VideoCapture(camera_index, cv2.CAP_AVFOUNDATION)
if not cap.isOpened():
print(f"❌ 无法打开相机 {camera_index}")
return None
print(f"\n{'='*60}")
print(f"相机 {camera_index} 详细信息")
print(f"{'='*60}")
# 相机基本属性列表 [5,8](@ref)
properties = [
(cv2.CAP_PROP_FRAME_WIDTH, "帧宽度"),
(cv2.CAP_PROP_FRAME_HEIGHT, "帧高度"),
(cv2.CAP_PROP_FPS, "帧率(FPS)"),
(cv2.CAP_PROP_BRIGHTNESS, "亮度"),
(cv2.CAP_PROP_CONTRAST, "对比度"),
(cv2.CAP_PROP_SATURATION, "饱和度"),
(cv2.CAP_PROP_HUE, "色调"),
(cv2.CAP_PROP_GAIN, "增益"),
(cv2.CAP_PROP_EXPOSURE, "曝光度"),
(cv2.CAP_PROP_AUTO_EXPOSURE, "自动曝光"),
(cv2.CAP_PROP_AUTOFOCUS, "自动对焦"),
(cv2.CAP_PROP_FOCUS, "焦距"),
(cv2.CAP_PROP_ZOOM, "变焦"),
(cv2.CAP_PROP_WHITE_BALANCE_BLUE_U, "白平衡蓝色"),
(cv2.CAP_PROP_WHITE_BALANCE_RED_V, "白平衡红色"),
(cv2.CAP_PROP_BACKLIGHT, "背光补偿"),
(cv2.CAP_PROP_BUFFERSIZE, "缓冲区大小"),
]
# 打印各属性当前值
print("📊 当前参数设置:")
for prop_id, prop_name in properties:
value = cap.get(prop_id)
# 特殊处理曝光属性(可能为负值)
if prop_id == cv2.CAP_PROP_EXPOSURE:
if value < 0:
print(f" {prop_name}: 自动曝光")
else:
print(f" {prop_name}: {value}")
else:
print(f" {prop_name}: {value}")
return cap
def test_supported_resolutions(cap, camera_index):
"""
测试相机支持的分辨率 [5,6](@ref)
"""
print(f"\n🔍 测试相机 {camera_index} 支持的分辨率:")
# 常见分辨率列表
common_resolutions = [
(160, 120), # QQVGA
(320, 240), # QVGA
(640, 480), # VGA
(800, 600), # SVGA
(1024, 768), # XGA
(1280, 720), # 720p HD
(1280, 800), # WXGA
(1366, 768), # HD
(1920, 1080), # 1080p Full HD
(2560, 1440), # 2K
(3840, 2160), # 4K UHD
]
supported_resolutions = []
for width, height in common_resolutions:
# 尝试设置分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# 读取实际设置的分辨率
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 检查是否设置成功
if actual_width == width and actual_height == height:
supported_resolutions.append((width, height))
print(f"{width}x{height}")
else:
print(f"{width}x{height} (实际: {actual_width}x{actual_height})")
return supported_resolutions
def test_supported_fps(cap, camera_index, test_width=640, test_height=480):
"""
测试相机支持的帧率范围 [10](@ref)
"""
print(f"\n🎬 测试相机 {camera_index} 支持的帧率 (分辨率: {test_width}x{test_height}):")
# 设置测试分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, test_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, test_height)
test_fps_values = [15, 24, 25, 30, 48, 50, 60, 90, 120]
supported_fps = []
for fps in test_fps_values:
# 尝试设置帧率
if cap.set(cv2.CAP_PROP_FPS, fps):
actual_fps = cap.get(cv2.CAP_PROP_FPS)
# 允许一定的误差
if abs(actual_fps - fps) <= 1.0:
supported_fps.append(fps)
print(f"{fps} FPS")
else:
print(f" ⚠️ {fps} FPS (实际: {actual_fps:.1f} FPS)")
else:
print(f"{fps} FPS")
return supported_fps
def get_camera_backend_info(cap, camera_index):
"""
获取相机后端信息 [1,8](@ref)
"""
try:
# 尝试获取后端名称某些OpenCV版本支持
backend_name = str(cap.getBackendName()) if hasattr(cap, 'getBackendName') else "未知"
# 根据后端判断相机类型
if "rtsp" in backend_name.lower() or "ffmpeg" in backend_name.lower():
camera_type = "网络相机"
else:
camera_type = "物理相机"
print(f"\n🔧 相机 {camera_index} 后端信息:")
print(f" 后端类型: {backend_name}")
print(f" 相机类型: {camera_type}")
return backend_name, camera_type
except:
print(f"\n🔧 相机 {camera_index} 后端信息: 无法获取")
return "未知", "未知"
def main():
"""
主函数
"""
print("📷 OpenCV相机信息检测工具")
print("=" * 50)
# 检查OpenCV版本
print(f"OpenCV版本: {cv2.__version__}")
print(f"Python版本: {sys.version}")
print(f"操作系统: {platform.system()} {platform.release()}")
# 1. 检测可用相机
available_cameras = list_available_cameras()
if not available_cameras:
print("❌ 未找到任何可用的相机设备")
return
# 2. 为每个相机获取详细信息
for camera_index in available_cameras:
cap = get_camera_properties(camera_index)
if cap is None:
continue
# 获取后端信息
backend_name, camera_type = get_camera_backend_info(cap, camera_index)
# 测试支持的分辨率
supported_res = test_supported_resolutions(cap, camera_index)
# 测试支持的帧率使用VGA分辨率进行测试
supported_fps = test_supported_fps(cap, camera_index, 640, 480)
# 恢复默认设置
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
# 释放相机资源
cap.release()
print(f"\n📋 相机 {camera_index} 支持汇总:")
print(f" 分辨率: {len(supported_res)}")
for res in supported_res:
print(f" - {res[0]}x{res[1]}")
print(f" 帧率: {supported_fps}")
print(f" 类型: {camera_type}")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,48 @@
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
class OpenCVCam(Node):
def __init__(self):
super().__init__('opencv_cam_node')
# 发布话题,名字和 params.yaml 里的一致
self.publisher_ = self.create_publisher(Image, '/camera/image_raw', 10)
self.timer = self.create_timer(0.033, self.timer_callback) # 30 FPS
self.bridge = CvBridge()
# 打开 0 号摄像头
self.cap = cv2.VideoCapture(6)
# ====================================================
# 核心:强制使用 MJPEG解决卡顿的关键
# ====================================================
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
self.cap.set(cv2.CAP_PROP_FPS, 60)
if not self.cap.isOpened():
print("❌ 无法打开摄像头!")
exit()
print(f"✅ 摄像头已打开 (MJPEG模式): 640x480 @ 60FPS")
def timer_callback(self):
ret, frame = self.cap.read()
if ret:
# 转成 ROS 消息并发布
msg = self.bridge.cv2_to_imgmsg(frame, encoding="bgr8")
self.publisher_.publish(msg)
# print("已发布一帧...") # 可以在这里看有没有在跑
def main(args=None):
rclpy.init(args=args)
node = OpenCVCam()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()