拍照version

This commit is contained in:
flower
2026-02-02 17:06:20 +08:00
parent 47016c00a6
commit d2b7b833b6
111 changed files with 19371 additions and 997 deletions

View File

@@ -22,7 +22,7 @@ if [ -z "$ROS_DISTRO" ]; then
exit 1
fi
echo " ROS 2 发行版: $ROS_DISTRO"
echo " ROS 2 发行版: $ROS_DISTRO"
# 检查依赖
echo ""
@@ -33,14 +33,14 @@ if ! command -v colcon &> /dev/null; then
exit 1
fi
echo " colcon 已安装"
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 " 清理完成"
echo " 清理完成"
fi
# 编译
@@ -56,7 +56,7 @@ fi
echo ""
echo "========================================"
echo " 编译成功!"
echo " 编译成功!"
echo "========================================"
echo ""
echo "下一步:"

View File

@@ -0,0 +1,72 @@
--- Session Started: 2026-02-02 15:40:47 ---
--- Session Started: 2026-02-02 16:12:35 ---
--- Session Started: 2026-02-02 16:14:58 ---
--- Session Started: 2026-02-02 16:16:59 ---
[2026-02-02 16:17:00] Sent: photo_20260202_161700_000.jpg Size: 551838 Total Sent: 1
[2026-02-02 16:17:00] Sent: photo_20260202_161700_001.jpg Size: 411948 Total Sent: 2
[2026-02-02 16:17:01] Sent: photo_20260202_161701_002.jpg Size: 376731 Total Sent: 3
[2026-02-02 16:17:01] Sent: photo_20260202_161701_003.jpg Size: 377754 Total Sent: 4
[2026-02-02 16:17:02] Sent: photo_20260202_161702_004.jpg Size: 371593 Total Sent: 5
[2026-02-02 16:17:02] Sent: photo_20260202_161702_005.jpg Size: 385420 Total Sent: 6
--- Session Started: 2026-02-02 16:23:14 ---
[2026-02-02 16:23:14] Sent: photo_20260202_161700_000.jpg Size: 551838 Total Sent: 1
[2026-02-02 16:23:14] Sent: photo_20260202_161700_001.jpg Size: 411948 Total Sent: 2
[2026-02-02 16:23:14] Sent: photo_20260202_161701_002.jpg Size: 376731 Total Sent: 3
[2026-02-02 16:23:14] Sent: photo_20260202_161701_003.jpg Size: 377754 Total Sent: 4
[2026-02-02 16:23:14] Sent: photo_20260202_161702_004.jpg Size: 371593 Total Sent: 5
[2026-02-02 16:23:14] Sent: photo_20260202_161702_005.jpg Size: 385420 Total Sent: 6
[2026-02-02 16:23:14] Sent: photo_20260202_162314_000.jpg Size: 594448 Total Sent: 7
[2026-02-02 16:23:15] Sent: photo_20260202_162315_001.jpg Size: 416092 Total Sent: 8
[2026-02-02 16:23:15] Sent: photo_20260202_162315_002.jpg Size: 448451 Total Sent: 9
[2026-02-02 16:23:16] Sent: photo_20260202_162316_003.jpg Size: 447612 Total Sent: 10
[2026-02-02 16:23:16] Sent: photo_20260202_162316_004.jpg Size: 441193 Total Sent: 11
[2026-02-02 16:23:17] Sent: photo_20260202_162317_005.jpg Size: 465881 Total Sent: 12
[2026-02-02 16:23:17] Sent: photo_20260202_162317_006.jpg Size: 446361 Total Sent: 13
[2026-02-02 16:23:18] Sent: photo_20260202_162318_007.jpg Size: 415256 Total Sent: 14
[2026-02-02 16:23:18] Sent: photo_20260202_162318_008.jpg Size: 415862 Total Sent: 15
[2026-02-02 16:23:19] Sent: photo_20260202_162319_009.jpg Size: 415374 Total Sent: 16
--- Session Started: 2026-02-02 16:28:39 ---
[2026-02-02 16:28:39] Sent: photo_000_20260202_162839_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 584359 Total Sent: 1
[2026-02-02 16:28:40] Sent: photo_001_20260202_162840_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 583744 Total Sent: 2
[2026-02-02 16:28:40] Sent: photo_002_20260202_162840_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 583338 Total Sent: 3
[2026-02-02 16:28:41] Sent: photo_003_20260202_162841_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 571135 Total Sent: 4
[2026-02-02 16:28:41] Sent: photo_004_20260202_162841_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 570012 Total Sent: 5
[2026-02-02 16:28:42] Sent: photo_005_20260202_162842_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 570419 Total Sent: 6
[2026-02-02 16:28:42] Sent: photo_006_20260202_162842_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 582702 Total Sent: 7
[2026-02-02 16:28:43] Sent: photo_007_20260202_162843_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 582108 Total Sent: 8
--- Session Started: 2026-02-02 16:32:10 ---
[2026-02-02 16:32:13] Sent: photo_000_20260202_163213_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 559873 Total Sent: 1
[2026-02-02 16:32:15] Sent: photo_001_20260202_163215_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 559766 Total Sent: 2
--- Session Started: 2026-02-02 16:32:54 ---
[2026-02-02 16:32:55] Sent: photo_000_20260202_163213_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 559873 Total Sent: 1
[2026-02-02 16:32:55] Sent: photo_001_20260202_163215_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 559766 Total Sent: 2
[2026-02-02 16:32:56] Sent: photo_000_20260202_163256_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 558412 Total Sent: 3
[2026-02-02 16:32:57] Sent: photo_001_20260202_163257_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 601159 Total Sent: 4
[2026-02-02 16:32:58] Sent: photo_002_20260202_163258_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 558166 Total Sent: 5
[2026-02-02 16:32:59] Sent: photo_003_20260202_163259_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 600601 Total Sent: 6
[2026-02-02 16:33:00] Sent: photo_004_20260202_163300_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 600806 Total Sent: 7
--- Session Started: 2026-02-02 16:34:57 ---
--- Session Started: 2026-02-02 16:36:29 ---
[2026-02-02 16:39:13] Sent: photo_000_20260202_163913_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 500250 Total Sent: 1
[2026-02-02 16:39:14] Sent: photo_001_20260202_163914_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 499410 Total Sent: 2
[2026-02-02 16:39:15] Sent: photo_002_20260202_163915_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 499874 Total Sent: 3
[2026-02-02 16:39:16] Sent: photo_003_20260202_163916_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 499487 Total Sent: 4
[2026-02-02 16:39:17] Sent: photo_004_20260202_163917_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 498681 Total Sent: 5
[2026-02-02 16:39:52] Sent: photo_005_20260202_163952_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 603995 Total Sent: 6
[2026-02-02 16:39:54] Sent: photo_006_20260202_163954_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 595479 Total Sent: 7
[2026-02-02 16:39:56] Sent: photo_007_20260202_163956_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 643671 Total Sent: 8
[2026-02-02 16:39:58] Sent: photo_008_20260202_163958_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 623669 Total Sent: 9
[2026-02-02 16:40:00] Sent: photo_009_20260202_164000_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 601373 Total Sent: 10
[2026-02-02 16:40:17] Sent: photo_010_20260202_164017_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 630691 Total Sent: 11
[2026-02-02 16:40:19] Sent: photo_011_20260202_164019_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 687730 Total Sent: 12
[2026-02-02 16:40:21] Sent: photo_012_20260202_164021_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 653137 Total Sent: 13
[2026-02-02 16:40:23] Sent: photo_013_20260202_164023_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 628483 Total Sent: 14
--- Session Started: 2026-02-02 16:53:03 ---
[2026-02-02 16:53:30] Sent: photo_000_20260202_165330_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 473529 Total Sent: 1
[2026-02-02 16:53:32] Sent: photo_001_20260202_165332_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 473852 Total Sent: 2
[2026-02-02 16:53:34] Sent: photo_002_20260202_165334_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 473945 Total Sent: 3
[2026-02-02 16:53:36] Sent: photo_003_20260202_165336_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 606768 Total Sent: 4
[2026-02-02 16:54:24] Sent: photo_004_20260202_165424_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 583563 Total Sent: 5
[2026-02-02 16:54:26] Sent: photo_005_20260202_165426_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 602259 Total Sent: 6
[2026-02-02 16:54:28] Sent: photo_006_20260202_165428_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 597611 Total Sent: 7
[2026-02-02 16:54:30] Sent: photo_007_20260202_165430_GPS_lat0.000000_lon0.000000_alt0.0.jpg Size: 583412 Total Sent: 8

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(skylink_bridge LANGUAGES CXX)
project(skylink_bridge LANGUAGES C CXX)
# ============================================================================
# 编译标准和优化选项
@@ -25,6 +25,7 @@ find_package(geometry_msgs REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(image_transport REQUIRED)
find_package(mavros_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
# OpenCV
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
@@ -43,6 +44,15 @@ include_directories(
include
)
# ============================================================================
# 生成服务接口
# ============================================================================
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/TriggerCapture.srv"
DEPENDENCIES
)
# ============================================================================
# 源文件
# ============================================================================
@@ -56,6 +66,8 @@ set(SOURCES
set(HEADERS
include/skylink_bridge/udp_sender_node.h
include/skylink_bridge/device_info_sender_node.h
include/skylink_bridge/camera_driver_node.h
include/skylink_bridge/httplib.h
include/protocol.h
)
@@ -64,7 +76,7 @@ set(HEADERS
# ============================================================================
add_executable(udp_sender_node src/udp_sender_node.cpp ${HEADERS})
add_executable(camera_driver_node src/camera_driver_node.cpp)
add_executable(camera_driver_node src/camera_driver_node.cpp ${HEADERS})
add_executable(device_info_sender_node src/device_info_sender_node.cpp)
# 链接库 - UDP Sender
@@ -84,9 +96,14 @@ target_link_libraries(udp_sender_node
target_link_libraries(camera_driver_node
${rclcpp_LIBRARIES}
${sensor_msgs_LIBRARIES}
${OpenCV_LIBRARIES}
Threads::Threads
)
# 链接服务接口库
rosidl_target_interfaces(camera_driver_node
${PROJECT_NAME} "rosidl_typesupport_cpp")
target_link_libraries(device_info_sender_node
${rclcpp_LIBRARIES}
Threads::Threads
@@ -107,6 +124,11 @@ ament_target_dependencies(camera_driver_node
sensor_msgs
)
# 链接服务接口库
rosidl_get_typesupport_target(cpp_typesupport_target
${PROJECT_NAME} "rosidl_typesupport_cpp")
target_link_libraries(camera_driver_node "${cpp_typesupport_target}")
ament_target_dependencies(device_info_sender_node
rclcpp
)
@@ -139,6 +161,11 @@ install(DIRECTORY include/
DESTINATION include
)
# 安装服务定义文件
install(DIRECTORY srv/
DESTINATION share/${PROJECT_NAME}/srv
)
# ============================================================================
# Ament 导出
# ============================================================================
@@ -151,6 +178,7 @@ ament_export_dependencies(
image_transport
mavros_msgs
OpenCV
rosidl_default_runtime
)
ament_package()

View File

@@ -9,14 +9,28 @@
/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
video_device: "/dev/video2" # OpenCV 摄像头索引,例如 0 代表 /dev/video02 代表 /dev/video2
image_width: 3840
image_height: 2160
# 调试参数
frame_rate: 30.0 # 帧率
fourcc: "MJPG" # 视频流格式: MJPG, YUYV 等
auto_exposure: true # 自动曝光开关: true=自动, false=手动
exposure_value: -5.0 # 曝光值 (仅在 auto_exposure=false 时生效, 具体范围取决于设备,通常是负数或很小的正数)
# 拍照功能配置
save_dir: "./camera_data"
interval: 5.0 # 周期
photo_count: 5 # 周期内拍照数量
gps_topic: "/gps_info"
capture_enabled: false # 是否启用周期拍照功能
# HTTP REST API 配置
http_port: 8080 # HTTP API 服务器端口
http_enabled: true # 是否启用 HTTP 服务器
/udp_sender:
ros__parameters:
# 网络与发送配置(对应 udp_sender 节点)
@@ -26,10 +40,11 @@
sender_thread_name: "skylink_sender"
# 相机相关udp_sender 使用的 topic / 图像格式)
camera_topic: "/camera/image_raw/compressed"
camera_frame: "bgr8"
# camera_topic: "/camera/image_raw" # 已移除,改为文件监控
save_dir: "./camera_data" # 监控目录,与 camera_driver 的 save_dir 一致
log_file: "./sent_log.txt" # 发送日志文件
# JPEG 压缩配置
# JPEG 压缩配置 (对于文件模式,这些如果不压缩可能忽略,但保持原样)
jpeg_quality: 80
adaptive_quality: false # 禁用自适应,使用固定质量以获得稳定帧率
quality_min: 30
@@ -50,17 +65,3 @@
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,260 @@
/**
* @file camera_driver_node.h
* @brief V4L2 相机驱动节点 - 头文件
*
* 使用 V4L2 直接读取 USB 摄像头 MJPEG 流,支持定时拍照和服务触发拍照
*
* @author flowzl
* @date 2026-01-19
*/
#pragma once
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/nav_sat_fix.hpp>
#include <thread>
#include <atomic>
#include <chrono>
#include <vector>
#include <fstream>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <mutex>
#include <string>
// V4L2 includes
#include <linux/videodev2.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/stat.h>
// 服务定义
#include "skylink_bridge/srv/trigger_capture.hpp"
// OpenCV for thumbnail generation
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
// HTTP server
#include "skylink_bridge/httplib.h"
/**
* @class CameraDriverNode
* @brief V4L2 相机驱动 ROS 2 节点
*/
class CameraDriverNode : public rclcpp::Node {
public:
/**
* @brief 构造函数
*/
CameraDriverNode();
/**
* @brief 析构函数
*/
~CameraDriverNode();
// 设备信息查询接口
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_; }
uint32_t total_photos() const { return total_photos_saved_.load(); }
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_;
bool enable_fps_stats_;
/** GPS 订阅 */
std::string gps_topic_;
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr gps_subscription_;
sensor_msgs::msg::NavSatFix latest_gps_fix_;
std::mutex gps_mutex_;
bool has_gps_fix_ = false;
/** 服务接口 */
rclcpp::Service<skylink_bridge::srv::TriggerCapture>::SharedPtr trigger_service_;
/** HTTP 服务器相关 */
std::thread http_server_thread_;
std::atomic<bool> http_server_running_{false};
int http_port_ = 8080;
bool http_enabled_ = true;
/** 捕获线程 */
std::thread capture_thread_;
std::atomic<bool> capturing_{false};
/** 定时拍照相关 */
rclcpp::TimerBase::SharedPtr capture_timer_;
bool capture_enabled_; // 是否启用定时拍照
double capture_interval_; // 拍照间隔(秒)
int photo_count_per_batch_; // 每批拍照数量
int current_photo_index_; // 当前批次中的照片索引
std::atomic<bool> is_capturing_{false}; // 是否正在拍照
std::mutex capture_mutex_; // 拍照互斥锁
/** 照片保存 */
std::string save_dir_;
std::atomic<uint32_t> total_photos_saved_{0}; // 总照片数
/** 传输模式 */
enum class TransmissionMode {
ORIGINAL, // 原图
THUMBNAIL // 缩略图
};
TransmissionMode transmission_mode_ = TransmissionMode::ORIGINAL;
int thumbnail_width_ = 640; // 缩略图宽度
int thumbnail_height_ = 480; // 缩略图高度
/** 统计信息 */
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;
/** 最新帧数据缓存(用于拍照) */
std::vector<uint8_t> latest_frame_data_;
std::mutex frame_data_mutex_;
std::atomic<bool> has_frame_data_{false};
// ========================================================================
// 私有方法
// ========================================================================
// xioctl helper
static int xioctl(int fh, int request, void *arg);
/**
* @brief 关闭设备
*/
void close_device();
/**
* @brief 释放内存映射
*/
void uninit_device();
/**
* @brief 停止流
*/
void stop_capturing();
/**
* @brief 初始化摄像头
*/
bool init_camera();
/**
* @brief 捕获工作线程(持续获取帧数据并缓存)
*/
void capture_worker();
/**
* @brief 拍照并保存
* @param mode 传输模式(原图或缩略图)
*/
void capture_photo(TransmissionMode mode = TransmissionMode::ORIGINAL);
/**
* @brief 生成缩略图
* @param original_data 原始JPEG数据
* @return 缩略图JPEG数据
*/
std::vector<uint8_t> generate_thumbnail(const std::vector<uint8_t>& original_data);
/**
* @brief 启动批量拍照
* @param duration 总时间(秒)
* @param count 拍照数量
* @param interval 时间间隔(秒)
* @param mode 传输模式
*/
void start_batch_capture(double duration, int count, double interval, TransmissionMode mode);
/**
* @brief 服务回调函数
*/
void service_callback(
const std::shared_ptr<skylink_bridge::srv::TriggerCapture::Request> request,
std::shared_ptr<skylink_bridge::srv::TriggerCapture::Response> response);
/**
* @brief 报告统计信息 (Legacy)
*/
void report_stats();
// ========================================================================
// HTTP 服务器方法
// ========================================================================
/**
* @brief 启动 HTTP 服务器
*/
void start_http_server();
/**
* @brief 停止 HTTP 服务器
*/
void stop_http_server();
/**
* @brief HTTP 服务器工作线程
*/
void http_server_worker();
/**
* @brief 处理触发拍照 API 请求
* @param req HTTP 请求
* @param res HTTP 响应
*/
void handle_trigger_capture_api(const httplib::Request& req, httplib::Response& res);
/**
* @brief 处理状态查询 API 请求
* @param req HTTP 请求
* @param res HTTP 响应
*/
void handle_status_api(const httplib::Request& req, httplib::Response& res);
/**
* @brief 简单的 JSON 解析辅助函数
* @param json_str JSON 字符串
* @param key 键名
* @param default_value 默认值
* @return 解析的值或默认值
*/
template<typename T>
T parse_json_value(const std::string& json_str, const std::string& key, T default_value) {
// 默认实现返回默认值特化版本在cpp中实现
return default_value;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,9 @@
#include <thread>
#include <atomic>
#include <cstring>
#include <fstream>
#include <filesystem>
#include <set>
// 协议定义
#include "protocol.h"
@@ -33,10 +36,10 @@
* @brief ROS 2 UDP 发送节点
*
* 负责:
* 1. 订阅相机和 GPS 数据
* 2. 编码图像为 JPEG
* 3. 分片打包为 UDP 数据包
* 4. 通过网络发送数据包
* 1. 监控指定目录下的 JPEG 文件
* 2. 读取文件并分片打包为 UDP 数据包
* 3. 通过网络发送数据包
* 4. 记录发送日志
*/
class UdpSenderNode : public rclcpp::Node {
public:
@@ -55,35 +58,42 @@ private:
// ROS 2 订阅和发布
// ========================================================================
/** 相机图像订阅 */
rclcpp::Subscription<sensor_msgs::msg::CompressedImage>::SharedPtr camera_subscriber_;
/** GPS 全球位置订阅 */
/** GPS 订阅 */
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr gps_subscriber_;
/** 文件扫描定时器 */
rclcpp::TimerBase::SharedPtr file_scan_timer_;
// ========================================================================
// 参数
// 成员变量
// ========================================================================
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_; ///< 丢弃帧统计
// 网络相关
std::string target_ip_;
uint16_t target_port_;
int udp_socket_;
// ========================================================================
// 套接字和网络
// ========================================================================
// GPS 数据
std::string gps_topic_;
bool enable_gps_;
int udp_socket_; ///< UDP 套接字文件描述符
uint32_t frame_counter_; ///< 帧计数器
// 配置参数
uint16_t max_packet_size_;
uint32_t frame_timeout_ms_;
bool enable_fps_stats_;
bool enable_drop_stats_;
// 图像源配置
std::string save_dir_; // 图片保存目录
std::string log_file_path_; // 日志文件路径
std::set<std::string> sent_files_; // 已发送文件记录
std::filesystem::file_time_type last_check_time_; // 上次检查时间
// 日志流
std::ofstream log_file_stream_;
// 统计相关
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}; ///< 丢弃的帧数
@@ -104,26 +114,8 @@ private:
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; ///< 批量发送大小 (增加以减少锁竞争)
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 数据缓存
// ========================================================================
@@ -149,89 +141,30 @@ private:
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
*/
// ---- 核心工作流程 ----
void sender_worker();
void scan_and_send_files();
void process_file(const std::string& filepath, const std::string& filename);
// ---- 队列管理 ----
void enqueue_packets(std::vector<std::vector<uint8_t>>& packets);
int send_udp_packet(const uint8_t* data, uint16_t len);
// ========================================================================
// 图像处理函数
// ========================================================================
// ---- 辅助功能 ----
void log_sent_file(const std::string& filename, size_t size);
/**
* @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);
// ---- 回调函数 ----
void gps_callback(const sensor_msgs::msg::NavSatFix::SharedPtr msg);
/**
* @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

@@ -31,6 +31,11 @@
<!-- 可选MAVROS GPS 支持 -->
<depend>mavros_msgs</depend>
<!-- 服务接口生成 -->
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<!-- 可选USB 相机支持 -->
<exec_depend>usb_cam</exec_depend>

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,8 @@
#include <sstream>
#include <sensor_msgs/msg/nav_sat_fix.hpp>
#include <thread>
#include <regex>
#include <sstream>
/**
* @brief 构造函数
@@ -31,32 +33,27 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
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>("max_packet_size", 1400); // Default to safer MTU
this->declare_parameter<int>("frame_timeout_ms", 5000);
this->declare_parameter<bool>("enable_fps_stats", true);
this->declare_parameter<bool>("enable_drop_stats", true);
this->declare_parameter<std::string>("save_dir", "./camera_data"); // 监控目录
this->declare_parameter<std::string>("log_file", "./sent_log.txt"); // 日志文件
// 读取参数
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();
save_dir_ = this->get_parameter("save_dir").as_string();
log_file_path_ = this->get_parameter("log_file").as_string();
// ========================================================================
// 打印初始化信息
@@ -66,10 +63,24 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
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(), "监控目录: %s", save_dir_.c_str());
RCLCPP_INFO(this->get_logger(), "日志文件: %s", log_file_path_.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_);
// 创建目录
std::filesystem::create_directories(save_dir_);
// 打开日志文件
log_file_stream_.open(log_file_path_, std::ios::out | std::ios::app);
if (!log_file_stream_.is_open()) {
RCLCPP_ERROR(this->get_logger(), "无法打开日志文件: %s", log_file_path_.c_str());
} else {
// 写入头部
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
log_file_stream_ << "--- Session Started: " << std::put_time(std::localtime(&now_c), "%F %T") << " ---" << std::endl;
}
// ========================================================================
// 初始化 UDP 套接字
@@ -81,26 +92,19 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
return;
}
RCLCPP_INFO(this->get_logger(), " UDP 套接字初始化成功");
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());
// 创建文件扫描定时器 (1Hz, 即每秒检查一次)
file_scan_timer_ = this->create_wall_timer(
std::chrono::milliseconds(200), // 200ms
std::bind(&UdpSenderNode::scan_and_send_files, this));
RCLCPP_INFO(this->get_logger(), " 文件监控定时器已启动 (200ms)");
if (enable_gps_) {
gps_subscriber_ = this->create_subscription<sensor_msgs::msg::NavSatFix>(
gps_topic_,
@@ -110,7 +114,7 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
}
);
RCLCPP_INFO(this->get_logger(), " 订阅 GPS Topic: %s", gps_topic_.c_str());
RCLCPP_INFO(this->get_logger(), " 订阅 GPS Topic: %s", gps_topic_.c_str());
}
// ========================================================================
@@ -119,7 +123,7 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
sender_running_ = true;
sender_thread_ = std::thread(&UdpSenderNode::sender_worker, this);
RCLCPP_INFO(this->get_logger(), " 后台发送线程已启动");
RCLCPP_INFO(this->get_logger(), " 后台发送线程已启动");
// ========================================================================
// 初始化统计
@@ -127,7 +131,7 @@ UdpSenderNode::UdpSenderNode() : rclcpp::Node("udp_sender_node"), udp_socket_(-1
stats_.last_report_time = std::chrono::system_clock::now();
RCLCPP_INFO(this->get_logger(), " 节点初始化完成,等待数据...");
RCLCPP_INFO(this->get_logger(), " 节点初始化完成,等待数据...");
RCLCPP_INFO(this->get_logger(), "======================================");
}
@@ -140,7 +144,7 @@ UdpSenderNode::~UdpSenderNode() {
send_queue_cv_.notify_one(); // 唤醒等待的线程
if (sender_thread_.joinable()) {
sender_thread_.join();
RCLCPP_INFO(this->get_logger(), " 后台发送线程已停止");
RCLCPP_INFO(this->get_logger(), " 后台发送线程已停止");
}
close_udp_socket();
@@ -282,26 +286,156 @@ void UdpSenderNode::enqueue_packets(std::vector<std::vector<uint8_t>>& packets)
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;
void UdpSenderNode::scan_and_send_files() {
if (!std::filesystem::exists(save_dir_)) return;
std::vector<std::filesystem::directory_entry> new_files;
try {
for (const auto& entry : std::filesystem::directory_iterator(save_dir_)) {
if (entry.is_regular_file() && entry.path().extension() == ".jpg") {
if (sent_files_.find(entry.path().string()) == sent_files_.end()) {
new_files.push_back(entry);
}
}
}
} catch (const std::exception& e) {
RCLCPP_ERROR(this->get_logger(), "扫描目录失败: %s", e.what());
return;
}
// 按修改时间排序(旧的先发)
std::sort(new_files.begin(), new_files.end(),
[](const std::filesystem::directory_entry& a, const std::filesystem::directory_entry& b) {
return std::filesystem::last_write_time(a) < std::filesystem::last_write_time(b);
});
for (const auto& entry : new_files) {
process_file(entry.path().string(), entry.path().filename().string());
PacketQueueItem item;
item.data.assign(data, data + len);
item.enqueue_time = std::chrono::system_clock::now();
send_queue_.push(item);
// 标记为已发送
sent_files_.insert(entry.path().string());
// 为了防止内存无限增长,如果太大了可以清理旧的 (可选)
if (sent_files_.size() > 10000) {
// 简单清理策略:实际生产中可能需要更复杂的逻辑
// 这里暂时不做,假设磁盘空间有限,文件会被外部删除
}
}
}
void UdpSenderNode::process_file(const std::string& filepath, const std::string& filename) {
// 读取文件内容
std::ifstream file(filepath, std::ios::binary);
if (!file) return;
// 获取文件大小
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
if (size == 0) return;
std::vector<uint8_t> buffer(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) return;
frame_counter_++;
// 从文件名提取照片编号
// 文件名格式: photo_YYYYMMDD_HHMMSS_XXX.jpg
uint32_t photo_number = frame_counter_; // 默认使用frame_counter_
try {
// 尝试从文件名中提取编号(最后一个下划线后的数字)
std::regex pattern(R"(photo_\d{8}_\d{6}_(\d+)\.jpg)");
std::smatch match;
if (std::regex_match(filename, match, pattern) && match.size() > 1) {
photo_number = std::stoul(match[1].str());
} else {
// 如果格式不匹配,尝试提取任何数字作为编号
std::regex alt_pattern(R"(_(\d+)\.jpg$)");
if (std::regex_search(filename, match, alt_pattern) && match.size() > 1) {
photo_number = std::stoul(match[1].str());
}
}
} catch (const std::exception& e) {
RCLCPP_WARN(this->get_logger(), "无法从文件名提取照片编号使用frame_counter: %s", e.what());
}
// 通知发送线程有新数据
send_queue_cv_.notify_one();
// 计算需要多少个包
const size_t header_size = PacketHeader::size();
const size_t payload_size = max_packet_size_ - header_size;
const uint16_t total_packets = (size + payload_size - 1) / payload_size;
std::vector<std::vector<uint8_t>> packets;
packets.reserve(total_packets);
// 获取 GPS 信息
double lat = 0.0, lon = 0.0, alt = 0.0;
{
std::lock_guard<std::mutex> gps_lock(current_gps_data_.mutex);
if (enable_gps_) {
lat = current_gps_data_.latitude;
lon = current_gps_data_.longitude;
alt = current_gps_data_.altitude;
}
}
double timestamp = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()
).count();
for (uint16_t i = 0; i < total_packets; ++i) {
size_t offset = i * payload_size;
size_t current_payload_len = std::min(payload_size, size - offset);
// 准备包数据
std::vector<uint8_t> packet_data(header_size + current_payload_len);
// 填充头部
PacketHeader* header = reinterpret_cast<PacketHeader*>(packet_data.data());
header->magic = SKYLINK_MAGIC;
header->frame_id = frame_counter_;
header->total_chunks = total_packets;
header->chunk_index = i;
header->data_len = static_cast<uint16_t>(current_payload_len);
header->timestamp = timestamp;
// GPS
header->lat = lat;
header->lon = lon;
header->alt = alt;
// 其他字段
header->crc16 = 0;
header->reserve = photo_number; // 存储照片编号
// 填充负载
std::memcpy(packet_data.data() + header_size, buffer.data() + offset, current_payload_len);
packets.push_back(std::move(packet_data));
}
// 加入发送队列
enqueue_packets(packets);
// 记录日志
log_sent_file(filename, size);
RCLCPP_INFO(this->get_logger(), "已排队文件: %s (大小: %zu bytes, 包数: %d, 照片编号: %u)",
filename.c_str(), size, total_packets, photo_number);
}
void UdpSenderNode::log_sent_file(const std::string& filename, size_t size) {
if (log_file_stream_.is_open()) {
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
log_file_stream_ << std::put_time(std::localtime(&now_c), "[%F %T]")
<< " Sent: " << filename
<< " Size: " << size
<< " Total Sent: " << sent_files_.size() + 1
<< std::endl;
log_file_stream_.flush(); // 确保写入磁盘
}
}
/**
@@ -352,58 +486,6 @@ void UdpSenderNode::sender_worker() {
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 回调
*/
@@ -414,132 +496,22 @@ void UdpSenderNode::gps_callback(const sensor_msgs::msg::NavSatFix::SharedPtr ms
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() {
// 简单的统计报告实现
if (!enable_fps_stats_) return;
auto now = std::chrono::system_clock::now();
auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(
now - stats_.last_report_time
).count();
auto diff = 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;
if (diff >= 5) { // 每5秒报告一次
std::lock_guard<std::mutex> lock(stats_mutex_);
// 这里只是为了演示,实际可以计算速率
stats_.last_report_time = now;
RCLCPP_INFO(this->get_logger(), "Stats: Sent %u packets, %u bytes. Dropped %u frames.",
packets_sent_.load(), bytes_sent_.load(), frames_dropped_.load());
}
}

View File

@@ -0,0 +1,11 @@
# 请求参数
float64 duration # 总时间0表示使用默认
int32 count # 拍照数量0表示使用默认
float64 interval # 时间间隔0表示自动计算
int8 transmission_mode # 传输模式0=原图1=缩略图
---
# 响应
bool success # 是否成功启动
string message # 状态消息
int32 total_photos # 当前总照片数