Compare commits

..

26 Commits

Author SHA1 Message Date
lgv
f443a7ce53 merge: resolve xtkuang_dev into dev 2026-07-27 17:47:30 +08:00
7c9a37f808 update quic service 2026-07-27 16:07:41 +08:00
lgv
ed231ace2b Merge branch 'lgv_dev_collision' into dev 2026-07-27 15:41:51 +08:00
lgv
4e9bd398f1 feat(collision): add collision primitive generation tools
Add URDF and USD collision primitive generation, MeshCat collision visualization, configuration, documentation, and generated Isaac Sim assets for the dual-arm model.
2026-07-27 15:39:54 +08:00
lgv
0257ac85ca feat(collision): add self-collision monitoring task
Add Pinocchio and Coal based self-collision checking with collision-pair filtering and displacement-based sampling. Integrate a periodic safety task with warning and stop thresholds, plus the simplified collision URDF and runtime configuration.
2026-07-27 15:37:39 +08:00
428ee328de feat: complete QUIC edge integration
Vendor MsQuic with build and install support, add DeviceManager status to configurable heartbeats, and report only enabled devices.

Add the local QUIC gateway, protocol coverage, real MsQuic E2E tests, process smoke tests, and updated integration documentation.
2026-07-24 12:35:04 +08:00
1181b2d541 fix: bound gRPC camera stream latency 2026-07-23 14:22:09 +08:00
d40c92b9bf feat: add QUIC media transport and integrate Hikvision updates 2026-07-23 12:52:30 +08:00
f8961c4e7a update hikvision camera 2026-07-23 10:40:58 +08:00
b585cb6601 add executeJsonCommand 2026-07-23 09:10:39 +08:00
a0fbeccc19 add hikvision camera controlPtz 2026-07-22 16:35:20 +08:00
53f71bb720 update hikvision camera 2026-07-22 15:58:40 +08:00
1fc5179344 update speaker 2026-07-22 13:48:11 +08:00
0ac04290e4 add stream audio service 2026-07-21 17:04:41 +08:00
d956e538bc update hikvision camera config 2026-07-21 14:34:29 +08:00
a6f00ceb6c add hikvision camera 2026-07-21 14:18:44 +08:00
2eefd845f9 add hikvision camera 2026-07-20 17:26:56 +08:00
89b1e1f8f6 update huayan_arm and grpc_service 2026-07-18 14:49:11 +08:00
2fa161f1cc update src1100 module 2026-07-07 15:54:09 +08:00
6664d181cb 增加aubo_arm更多接口实现 2026-07-06 14:11:19 +08:00
1c70819993 删除aubo_arm.cpp中所有的宏 2026-07-03 10:08:12 +08:00
5015077cb5 fix aubo arm connection error 2026-07-03 08:55:30 +08:00
d04b1920d1 delete agv connect and disconnect interfaces 2026-07-02 17:33:22 +08:00
f217ce96c4 adjust src1100 config 2026-07-02 15:26:20 +08:00
5a6b75938d 实现仙工控制器SRC1100的集成,未测试 2026-07-02 12:30:07 +08:00
27445c00f5 Add Aubo arm support 2026-07-02 08:29:19 +08:00
1257 changed files with 580600 additions and 1656 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/cmake-build-debug
/cmake-build-sigma-debuggit
/build
/build-*
/log
/third_party/osqp/
/third_party/OsqpEigen/

View File

@ -1,6 +0,0 @@
{
"recommendations": [
"ms-vscode.cpptools",
"ms-vscode.cmake-tools"
]
}

30
.vscode/launch.json vendored
View File

@ -1,32 +1,30 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++: g++ 构建和调试活动文件",
"name": "Debug cmvr_es (installed output)",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"program": "${workspaceFolder}/output/bin/cmvr_es",
"args": [],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"cwd": "${workspaceFolder}/output/bin",
"environment": [
{
"name": "LD_LIBRARY_PATH",
"value": "${workspaceFolder}/output/lib:${env:LD_LIBRARY_PATH}"
}
],
"externalConsole": false,
"MIMode": "gdb",
"preLaunchTask": "cmake: install",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: g++ 生成活动文件",
"miDebuggerPath": "/usr/bin/gdb"
]
}
],
"version": "2.0.0"
]
}

View File

@ -1,12 +1,11 @@
{
"cmake.configureOnOpen": true,
"cmake.buildDirectory": "${workspaceFolder}/cmake-build-debug",
"cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json",
"cmake.sourceDirectory": "${workspaceFolder}",
"cmake.buildDirectory": "${workspaceFolder}/build/vscode-debug",
"cmake.configureSettings": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
},
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
"C_Cpp.default.compileCommands": "${workspaceFolder}/compile_commands.json",
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/vscode-debug/compile_commands.json",
"C_Cpp.default.cppStandard": "c++17",
"C_Cpp.default.intelliSenseMode": "linux-gcc-x64"
}

70
.vscode/tasks.json vendored
View File

@ -1,25 +1,71 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ 生成活动文件",
"command": "/usr/bin/g++",
"label": "cmake: configure",
"type": "shell",
"command": "cmake",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
"-S",
"${workspaceFolder}",
"-B",
"${workspaceFolder}/build/vscode-debug",
"-DCMAKE_BUILD_TYPE=Debug",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
],
"options": {
"cwd": "${fileDirname}"
"group": "build",
"problemMatcher": []
},
{
"label": "cmake: build",
"type": "shell",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}/build/vscode-debug",
"-j",
"8"
],
"dependsOn": [
"cmake: configure"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "cmake: install",
"type": "shell",
"command": "cmake",
"args": [
"--install",
"${workspaceFolder}/build/vscode-debug",
"--verbose"
],
"dependsOn": [
"cmake: build"
],
"group": "build",
"detail": "调试器生成的任务。"
"problemMatcher": []
},
{
"label": "cmvr_es: run installed",
"type": "shell",
"command": "${workspaceFolder}/output/bin/cmvr_es",
"options": {
"cwd": "${workspaceFolder}/output/bin",
"env": {
"LD_LIBRARY_PATH": "${workspaceFolder}/output/lib:${env:LD_LIBRARY_PATH}"
}
},
"dependsOn": [
"cmake: install"
],
"version": "2.0.0"
"problemMatcher": []
}
]
}

View File

@ -110,7 +110,7 @@ target_link_libraries(cmvr_es PRIVATE
cmvr_es::runtime
cmvr_es::proto
cmvr_es::logging
service
cmvr_es::quic_edge_task
${GLOG_LIBRARIES}
jsoncpp
cmvr_es::service

6
MUJOCO_LOG.TXT Normal file
View File

@ -0,0 +1,6 @@
Fri Jul 24 15:39:05 2026
ERROR: could not create window
Fri Jul 24 15:40:37 2026
ERROR: could not create window

View File

@ -63,6 +63,7 @@ function(setup_external_libs ARCH)
"${FULL_PATH}/lib/*.so"
"${FULL_PATH}/lib/*.so.*"
)
list(FILTER _SO_FILES EXCLUDE REGEX "/libstdc\\+\\+\\.so(\\..*)?$")
if(_SO_FILES)
list(APPEND INSTALL_SO_FILES ${_SO_FILES})
endif()
@ -85,6 +86,7 @@ function(setup_external_libs ARCH)
"${FULL_PATH}/*.so"
"${FULL_PATH}/*.so.*"
)
list(FILTER _SO_FILES2 EXCLUDE REGEX "/libstdc\\+\\+\\.so(\\..*)?$")
if(_SO_FILES2)
list(APPEND INSTALL_SO_FILES ${_SO_FILES2})
endif()
@ -138,6 +140,14 @@ function(setup_external_libs ARCH)
)
install(FILES ${INSTALL_SO_FILES} DESTINATION lib)
install(CODE [[
file(GLOB _bundled_stdlib_files
"${CMAKE_INSTALL_PREFIX}/lib/libstdc++.so"
"${CMAKE_INSTALL_PREFIX}/lib/libstdc++.so.*")
if(_bundled_stdlib_files)
file(REMOVE ${_bundled_stdlib_files})
endif()
]])
# After installing, patch RPATH of installed shared libs to $ORIGIN
install(CODE [[

View File

@ -0,0 +1,16 @@
# MsQuic's QuicTLS custom build asks CMake's ProcessorCount module for a
# nested `make -jN` value. Route that query through the build script's
# --jobs setting so the dependency build cannot silently oversubscribe the
# host.
if(NOT DEFINED CMVR_MSQUIC_PROCESSOR_COUNT OR
NOT CMVR_MSQUIC_PROCESSOR_COUNT MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR
"CMVR_MSQUIC_PROCESSOR_COUNT must be a positive integer")
endif()
function(ProcessorCount result_variable)
set("${result_variable}"
"${CMVR_MSQUIC_PROCESSOR_COUNT}"
PARENT_SCOPE)
endfunction()

View File

@ -7,7 +7,10 @@ add_subdirectory(algorithms)
add_subdirectory(simulate)
add_subdirectory(devices)
add_subdirectory(manager/device_manager)
add_subdirectory(manager/media_source_hub)
add_subdirectory(service/quic_edge)
add_subdirectory(task)
add_subdirectory(task/quic_edge_task)
add_subdirectory(manager/task_manager)
add_subdirectory(service)
add_subdirectory(runtime)

View File

@ -2,3 +2,4 @@ add_subdirectory(motion_planner)
add_subdirectory(kinematics/ik_solver)
add_subdirectory(perception)
add_subdirectory(controllers)
add_subdirectory(collision_detection)

View File

@ -0,0 +1,125 @@
# Algorithms 模块开发指南
`algorithms/` 保存与具体厂商协议无关的运动学、规划、控制和感知算法。算法接受通用类型或显式接口输入,不应直接解析设备报文,也不应承担 gRPC/QUIC 传输职责。
返回[项目总览](../../README.md)。
## 当前结构
| 目录 | 主要能力 | 主要 CMake target |
| --- | --- | --- |
| `kinematics/ik_solver/` | Pinocchio DLS/QP、SRS、LAWBA 逆运动学 | `cmvr_es::ik_solver` |
| `motion_planner/base_motion/` | TOPPRA、S 曲线、笛卡尔速度限制 | `cmvr_es::base_motion` |
| `motion_planner/arm_motion/` | MoveJ、MoveL、SpeedL 机械臂规划 | `cmvr_es::algorithms::arm_motion` |
| `controllers/` | PID、IBVS、笛卡尔速度控制 | `cmvr_es::algorithms::controller`、`cmvr_es::algorithms::arm_control` |
| `perception/` | AprilTag 和视觉定位 | `cmvr_es::perception` |
顶层入口是 [`CMakeLists.txt`](CMakeLists.txt)。
## 依赖边界
- 算法层可以依赖 `common/`、Eigen、Pinocchio、OSQP、TOPPRA、OpenCV、ViSP 等;
- 不包含串口、CAN、HTTP 或厂商 SDK 协议处理;
- 不启动 gRPC/QUIC 服务或管理设备生命周期;
- 不从算法内部读取全局配置文件,构造或 `configure` 时显式传入配置;
- 可复用算法不应主动取得 `DeviceManager` 单例。
当前部分 controller target 仍链接 `device_manager`,这是现有耦合。新增算法应优先通过参数、回调或窄接口注入设备状态,避免继续扩大该依赖。
## 扩展已有算法类别
### 1. 定义或复用抽象接口
常用接口:
- [`IKSolver`](kinematics/ik_solver/common/include/ik_solver.h)
- [`JointMotionPlanner`](motion_planner/arm_motion/joint_motion/joint_motion_planner.h)
- [`CartesianMotionPlanner`](motion_planner/arm_motion/cartesian_motion/cartesian_motion_planner.h)
接口应明确:
- 输入输出单位和坐标系;
- 是否修改内部状态;
- 是否线程安全;
- 失败时输出是否保持不变;
- 是否支持实时循环,以及最大允许耗时。
### 2. 增加配置
在 [`../../protos/README.md`](../../protos/README.md) 指导下:
1. 为算法增加独立配置 message
2. 在所属 `oneof algorithm` 中增加新字段和新 tag
3. 不复用已发布 tag
4. 为迭代次数、容差、速度和加速度设置有效范围;
5. 在默认设备配置中给出显式参数。
### 3. 实现与工厂注册
将实现放在对应类别子目录,并修改实际工厂:
- IK[`ik_solver_factory.h`](kinematics/ik_solver/ik_solver_factory.h)
- MoveJ[`joint_motion_planner_factory.h`](motion_planner/arm_motion/joint_motion/joint_motion_planner_factory.h)
- MoveL / SpeedL[`cartesian_motion_planner_factory.h`](motion_planner/arm_motion/cartesian_motion/cartesian_motion_planner_factory.h)
工厂失败应返回 `nullptr` 并记录清晰原因不能静默回退到另一个算法。MoveL 和 SpeedL 的实现必须保持配置组合一致。
### 4. 更新 CMake
- 将实现 `.cpp` 加入对应 library
- 使用项目已有 alias target
- 通过 `target_include_directories` 暴露公共头;
- 将依赖放入使用它的最小 target
- 测试源文件不能加入生产共享库;
- 新增三方依赖时同步根依赖发现逻辑和 `request.txt`
## 数值与机器人语义
算法扩展至少需要明确:
- 关节位置单位为 rad速度为 rad/s
- 笛卡尔平移为 m旋转和角速度为 rad
- base、tool、world、user frame 的转换方向;
- URDF base frame、tip frame 和关节顺序;
- 位置、速度、加速度和 jerk 限制;
- 奇异点、不可达目标和求解超时行为;
- measured state 与算法内部 seed 的更新时机。
IK 在求解前应使用真实关节角更新 seed。MoveL 连续求解时,应使用上一步解更新下一步状态,不能一直使用初始状态。
## 测试要求
每个新算法至少覆盖:
1. 正常输入;
2. 空输入、自由度不匹配和 NaN/Inf
3. 关节限位与速度限制;
4. 不可达目标和不收敛;
5. 坐标系转换;
6. 确定性和重复调用;
7. 若用于实时控制,统计最坏执行时间;
8. 与一个已知模型或离线参考结果对比。
当前不少算法测试只通过 `add_executable()` 构建,没有登记到 CTest。新增无设备测试应放在 `BUILD_TESTING` 条件内,并使用 `add_test()`需要图形界面、RealSense 或 MuJoCo 的测试应明确标为集成测试,不得阻塞默认无设备测试。
## 新增算法类别
如果现有类别无法承载:
1. 在 `algorithms/<category>/` 新建目录;
2. 定义协议无关抽象接口;
3. 定义配置 Proto 和工厂;
4. 提供单独 CMake library 与 `cmvr_es::...` alias
5. 在 [`algorithms/CMakeLists.txt`](CMakeLists.txt) 添加子目录;
6. 由设备或任务层注入使用,不让算法反向控制服务层;
7. 添加无设备单元测试和真实设备/仿真集成测试。
## 提交检查
- [ ] 厂商协议没有进入算法接口
- [ ] 单位、坐标系和关节顺序明确
- [ ] 工厂已注册且配置组合经过校验
- [ ] 不可达、超时和数值异常可观测
- [ ] 测试没有被编入生产共享库
- [ ] 无设备测试已登记到 CTest
- [ ] 实时路径没有日志洪泛和无界内存分配

View File

@ -0,0 +1,48 @@
add_library(self_collision_checker SHARED
self_collision/src/self_collision_checker.cpp
self_collision/src/distance_sampling_policy.cpp
)
target_include_directories(self_collision_checker PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_compile_definitions(self_collision_checker PRIVATE
PINOCCHIO_ENABLE_TEMPLATE_INSTANTIATION
PINOCCHIO_WITH_HPP_FCL
COAL_DISABLE_HPP_FCL_WARNINGS
)
target_link_libraries(self_collision_checker PUBLIC
pinocchio_default
pinocchio_parsers
pinocchio_collision
coal
)
add_library(cmvr_es::self_collision_checker ALIAS self_collision_checker)
add_executable(self_collision_checker_test
self_collision/test/self_collision_checker_test.cpp
)
target_link_libraries(self_collision_checker_test PRIVATE
cmvr_es::self_collision_checker
gtest
gtest_main
pthread
)
target_compile_definitions(self_collision_checker_test PRIVATE
CMVR_ES_SOURCE_DIR="${PROJECT_SOURCE_DIR}"
)
add_executable(self_collision_benchmark
self_collision/benchmark/self_collision_benchmark.cpp
)
target_link_libraries(self_collision_benchmark PRIVATE
cmvr_es::self_collision_checker
)
target_compile_definitions(self_collision_benchmark PRIVATE
CMVR_ES_SOURCE_DIR="${PROJECT_SOURCE_DIR}"
)
install(TARGETS self_collision_checker LIBRARY DESTINATION lib)

View File

@ -0,0 +1,53 @@
#include <algorithm>
#include <chrono>
#include <iostream>
#include <string>
#include <vector>
#include "algorithms/collision_detection/self_collision/include/self_collision_checker.h"
int main()
{
const std::string urdf_path = std::string(CMVR_ES_SOURCE_DIR) +
"/model/xiaoyan_description/dual_arm_collision.urdf";
const std::vector<std::string> joint_names{
"R_SHOULDER_P", "R_SHOULDER_R", "R_SHOULDER_Y", "R_ELBOW_R",
"R_WRIST_P", "R_WRIST_Y", "R_WRIST_R",
};
cmvr::SelfCollisionChecker checker;
std::string error;
if (!checker.init(urdf_path, joint_names, {}, &error)) {
std::cerr << "Initialization failed: " << error << '\n';
return 1;
}
constexpr std::size_t kIterations = 2000;
std::vector<double> samples_us;
samples_us.reserve(kIterations);
std::vector<double> q(joint_names.size(), 0.0);
for (std::size_t iteration = 0; iteration < kIterations; ++iteration) {
q[0] = 0.2 * static_cast<double>(iteration % 100) / 100.0;
const auto begin = std::chrono::steady_clock::now();
const auto result = checker.check(q);
const auto end = std::chrono::steady_clock::now();
if (!result.valid) {
std::cerr << "Collision check failed: " << result.error << '\n';
return 1;
}
samples_us.push_back(std::chrono::duration<double, std::micro>(end - begin).count());
}
std::sort(samples_us.begin(), samples_us.end());
double total_us = 0.0;
for (const double sample : samples_us) {
total_us += sample;
}
const std::size_t p99_index = static_cast<std::size_t>(0.99 * (samples_us.size() - 1));
std::cout << "active_pairs=" << checker.activePairCount() << '\n'
<< "iterations=" << samples_us.size() << '\n'
<< "average_us=" << total_us / samples_us.size() << '\n'
<< "p99_us=" << samples_us[p99_index] << '\n'
<< "max_us=" << samples_us.back() << '\n';
return 0;
}

View File

@ -0,0 +1,46 @@
#ifndef CMVR_ES_DISTANCE_SAMPLING_POLICY_H
#define CMVR_ES_DISTANCE_SAMPLING_POLICY_H
#include <chrono>
#include <string>
#include "algorithms/collision_detection/self_collision/include/self_collision_checker.h"
namespace cmvr {
struct DistanceSamplingOptions {
double max_geometry_displacement_m{0.002};
double max_check_period_s{0.01};
};
class DistanceSamplingPolicy {
public:
using Clock = std::chrono::steady_clock;
bool configure(const DistanceSamplingOptions& options,
std::string* error = nullptr);
bool shouldCheck(const CollisionGeometrySnapshot& current,
Clock::time_point now) const;
void markChecked(const CollisionGeometrySnapshot& current,
Clock::time_point now);
void reset();
double displacementSinceLastCheck(
const CollisionGeometrySnapshot& current) const;
bool hasBaseline() const { return has_baseline_; }
private:
DistanceSamplingOptions options_{};
CollisionGeometrySnapshot last_checked_{};
Clock::time_point last_check_time_{};
bool configured_{false};
bool has_baseline_{false};
};
} // namespace cmvr
#endif // CMVR_ES_DISTANCE_SAMPLING_POLICY_H

View File

@ -0,0 +1,82 @@
#ifndef CMVR_ES_SELF_COLLISION_CHECKER_H
#define CMVR_ES_SELF_COLLISION_CHECKER_H
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include <Eigen/Geometry>
namespace cmvr {
struct CollisionPair {
std::string first;
std::string second;
};
struct SelfCollisionOptions {
std::vector<CollisionPair> ignored_pairs;
};
struct CollisionObjectPose {
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
std::size_t geometry_index{0};
Eigen::Vector3d position{Eigen::Vector3d::Zero()};
Eigen::Quaterniond orientation{Eigen::Quaterniond::Identity()};
double bounding_radius_m{0.0};
};
struct CollisionGeometrySnapshot {
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
std::vector<CollisionObjectPose, Eigen::aligned_allocator<CollisionObjectPose>> objects;
};
struct SelfCollisionResult {
bool valid{false};
bool in_collision{false};
double minimum_distance_m{0.0};
std::string first;
std::string second;
std::string error;
};
// Instances cache Pinocchio work data and are not thread-safe.
class SelfCollisionChecker {
public:
SelfCollisionChecker();
~SelfCollisionChecker();
SelfCollisionChecker(SelfCollisionChecker&&) noexcept;
SelfCollisionChecker& operator=(SelfCollisionChecker&&) noexcept;
SelfCollisionChecker(const SelfCollisionChecker&) = delete;
SelfCollisionChecker& operator=(const SelfCollisionChecker&) = delete;
bool init(const std::string& urdf_path,
const std::vector<std::string>& active_joint_names,
const SelfCollisionOptions& options,
std::string* error = nullptr);
bool makeSnapshot(const std::vector<double>& joint_positions,
CollisionGeometrySnapshot* snapshot,
std::string* error = nullptr);
SelfCollisionResult check(const CollisionGeometrySnapshot& snapshot);
SelfCollisionResult check(const std::vector<double>& joint_positions);
bool initialized() const;
std::size_t dof() const;
std::size_t activePairCount() const;
const std::vector<std::string>& jointNames() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace cmvr
#endif // CMVR_ES_SELF_COLLISION_CHECKER_H

View File

@ -0,0 +1,100 @@
#include "algorithms/collision_detection/self_collision/include/distance_sampling_policy.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace cmvr {
namespace {
void setError(std::string* error, const std::string& message)
{
if (error) {
*error = message;
}
}
double rotationAngle(const Eigen::Quaterniond& first,
const Eigen::Quaterniond& second)
{
const double dot = std::clamp(
std::abs(first.normalized().dot(second.normalized())), 0.0, 1.0);
return 2.0 * std::acos(dot);
}
} // namespace
bool DistanceSamplingPolicy::configure(const DistanceSamplingOptions& options,
std::string* error)
{
if (!std::isfinite(options.max_geometry_displacement_m) ||
options.max_geometry_displacement_m <= 0.0) {
setError(error, "max_geometry_displacement_m must be finite and positive");
return false;
}
if (!std::isfinite(options.max_check_period_s) ||
options.max_check_period_s <= 0.0) {
setError(error, "max_check_period_s must be finite and positive");
return false;
}
options_ = options;
configured_ = true;
reset();
if (error) {
error->clear();
}
return true;
}
bool DistanceSamplingPolicy::shouldCheck(const CollisionGeometrySnapshot& current,
const Clock::time_point now) const
{
if (!configured_ || !has_baseline_) {
return true;
}
const double elapsed_s = std::chrono::duration<double>(now - last_check_time_).count();
if (elapsed_s >= options_.max_check_period_s) {
return true;
}
return displacementSinceLastCheck(current) >= options_.max_geometry_displacement_m;
}
void DistanceSamplingPolicy::markChecked(const CollisionGeometrySnapshot& current,
const Clock::time_point now)
{
last_checked_ = current;
last_check_time_ = now;
has_baseline_ = true;
}
void DistanceSamplingPolicy::reset()
{
last_checked_.objects.clear();
last_check_time_ = Clock::time_point{};
has_baseline_ = false;
}
double DistanceSamplingPolicy::displacementSinceLastCheck(
const CollisionGeometrySnapshot& current) const
{
if (!has_baseline_ || current.objects.size() != last_checked_.objects.size()) {
return std::numeric_limits<double>::infinity();
}
double maximum_displacement = 0.0;
for (std::size_t index = 0; index < current.objects.size(); ++index) {
const auto& previous = last_checked_.objects[index];
const auto& now = current.objects[index];
if (previous.geometry_index != now.geometry_index) {
return std::numeric_limits<double>::infinity();
}
const double translation = (now.position - previous.position).norm();
const double radius = std::max(previous.bounding_radius_m, now.bounding_radius_m);
const double swept_distance =
translation + radius * rotationAngle(previous.orientation, now.orientation);
maximum_displacement = std::max(maximum_displacement, swept_distance);
}
return maximum_displacement;
}
} // namespace cmvr

View File

@ -0,0 +1,403 @@
#include "algorithms/collision_detection/self_collision/include/self_collision_checker.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <limits>
#include <set>
#include <sstream>
#include <unordered_set>
#include <utility>
#include <pinocchio/algorithm/geometry.hpp>
#include <pinocchio/algorithm/joint-configuration.hpp>
#include <pinocchio/collision/distance.hpp>
#include <pinocchio/multibody/data.hpp>
#include <pinocchio/multibody/geometry.hpp>
#include <pinocchio/multibody/model.hpp>
#include <pinocchio/parsers/urdf.hpp>
namespace cmvr {
namespace {
using LinkPairKey = std::pair<std::string, std::string>;
LinkPairKey canonicalPair(std::string first, std::string second)
{
if (second < first) {
std::swap(first, second);
}
return {std::move(first), std::move(second)};
}
void setError(std::string* error, const std::string& message)
{
if (error) {
*error = message;
}
}
} // namespace
class SelfCollisionChecker::Impl {
public:
bool init(const std::string& urdf_path,
const std::vector<std::string>& active_joint_names,
const SelfCollisionOptions& options,
std::string* error)
{
reset();
if (urdf_path.empty()) {
setError(error, "URDF path is empty");
return false;
}
if (!std::filesystem::is_regular_file(urdf_path)) {
setError(error, "URDF file does not exist: " + urdf_path);
return false;
}
if (active_joint_names.empty()) {
setError(error, "Active joint list is empty");
return false;
}
try {
pinocchio::urdf::buildModel(urdf_path, model_);
pinocchio::urdf::buildGeom(
model_, urdf_path, pinocchio::COLLISION, geometry_model_);
} catch (const std::exception& exception) {
setError(error, "Failed to load collision URDF: " + std::string(exception.what()));
reset();
return false;
}
if (geometry_model_.ngeoms == 0) {
setError(error, "URDF contains no collision geometry: " + urdf_path);
reset();
return false;
}
std::unordered_set<pinocchio::JointIndex> active_joint_ids;
std::unordered_set<std::string> unique_joint_names;
joint_names_.reserve(active_joint_names.size());
joint_q_indices_.reserve(active_joint_names.size());
for (const auto& joint_name : active_joint_names) {
if (joint_name.empty() || !unique_joint_names.insert(joint_name).second) {
setError(error, "Active joint names must be non-empty and unique");
reset();
return false;
}
if (!model_.existJointName(joint_name)) {
setError(error, "Joint not found in URDF: " + joint_name);
reset();
return false;
}
const pinocchio::JointIndex joint_id = model_.getJointId(joint_name);
const auto& joint = model_.joints[joint_id];
if (joint.nq() != 1) {
setError(error, "Only one-DoF active joints are supported: " + joint_name);
reset();
return false;
}
active_joint_ids.insert(joint_id);
joint_names_.push_back(joint_name);
joint_q_indices_.push_back(joint.idx_q());
}
geometry_link_names_.resize(geometry_model_.ngeoms);
std::unordered_set<std::string> selected_link_names;
for (pinocchio::GeomIndex geometry_id = 0;
geometry_id < geometry_model_.ngeoms;
++geometry_id) {
auto& geometry = geometry_model_.geometryObjects[geometry_id];
const std::string link_name = geometry.parentFrame < model_.frames.size()
? model_.frames[geometry.parentFrame].name
: geometry.name;
geometry_link_names_[geometry_id] = link_name;
const bool is_static = geometry.parentJoint == 0;
const bool belongs_to_active_arm = active_joint_ids.count(geometry.parentJoint) != 0;
if (!is_static && !belongs_to_active_arm) {
continue;
}
if (!geometry.geometry) {
setError(error, "Collision geometry is null for link: " + link_name);
reset();
return false;
}
geometry.geometry->computeLocalAABB();
selected_geometry_indices_.push_back(geometry_id);
selected_link_names.insert(link_name);
}
if (selected_geometry_indices_.size() < 2) {
setError(error, "Fewer than two collision geometries remain after arm filtering");
reset();
return false;
}
std::set<LinkPairKey> ignored_pairs;
for (const auto& pair : options.ignored_pairs) {
if (pair.first.empty() || pair.second.empty() || pair.first == pair.second) {
setError(error, "Ignored collision pairs require two different non-empty links");
reset();
return false;
}
if (!selected_link_names.count(pair.first) || !selected_link_names.count(pair.second)) {
setError(error,
"Ignored collision pair references an inactive or unknown link: " +
pair.first + ", " + pair.second);
reset();
return false;
}
ignored_pairs.insert(canonicalPair(pair.first, pair.second));
}
geometry_model_.removeAllCollisionPairs();
for (std::size_t first_index = 0;
first_index < selected_geometry_indices_.size();
++first_index) {
const auto first_geometry_id = selected_geometry_indices_[first_index];
const auto& first_geometry = geometry_model_.geometryObjects[first_geometry_id];
for (std::size_t second_index = first_index + 1;
second_index < selected_geometry_indices_.size();
++second_index) {
const auto second_geometry_id = selected_geometry_indices_[second_index];
const auto& second_geometry = geometry_model_.geometryObjects[second_geometry_id];
if (first_geometry.parentJoint == second_geometry.parentJoint) {
continue;
}
if (model_.parents[first_geometry.parentJoint] == second_geometry.parentJoint ||
model_.parents[second_geometry.parentJoint] == first_geometry.parentJoint) {
continue;
}
const auto link_pair = canonicalPair(
geometry_link_names_[first_geometry_id],
geometry_link_names_[second_geometry_id]);
if (ignored_pairs.count(link_pair)) {
continue;
}
geometry_model_.addCollisionPair(
pinocchio::CollisionPair(first_geometry_id, second_geometry_id));
}
}
if (geometry_model_.collisionPairs.empty()) {
setError(error, "No active collision pairs remain after filtering");
reset();
return false;
}
data_ = std::make_unique<pinocchio::Data>(model_);
geometry_data_ = std::make_unique<pinocchio::GeometryData>(geometry_model_);
for (auto& request : geometry_data_->distanceRequests) {
request.enable_signed_distance = true;
}
neutral_q_ = pinocchio::neutral(model_);
initialized_ = true;
if (error) {
error->clear();
}
return true;
}
bool makeSnapshot(const std::vector<double>& joint_positions,
CollisionGeometrySnapshot* snapshot,
std::string* error)
{
if (!initialized_) {
setError(error, "SelfCollisionChecker is not initialized");
return false;
}
if (!snapshot) {
setError(error, "Collision snapshot output is null");
return false;
}
if (joint_positions.size() != joint_names_.size()) {
std::ostringstream stream;
stream << "Joint position size mismatch: expected " << joint_names_.size()
<< ", got " << joint_positions.size();
setError(error, stream.str());
return false;
}
Eigen::VectorXd q = neutral_q_;
for (std::size_t index = 0; index < joint_positions.size(); ++index) {
if (!std::isfinite(joint_positions[index])) {
setError(error, "Joint position contains a non-finite value: " + joint_names_[index]);
return false;
}
q[joint_q_indices_[index]] = joint_positions[index];
}
try {
pinocchio::updateGeometryPlacements(
model_, *data_, geometry_model_, *geometry_data_, q);
} catch (const std::exception& exception) {
setError(error, "Failed to update collision geometry: " + std::string(exception.what()));
return false;
}
snapshot->objects.clear();
snapshot->objects.reserve(selected_geometry_indices_.size());
for (const auto geometry_id : selected_geometry_indices_) {
const auto& placement = geometry_data_->oMg[geometry_id];
const auto& geometry = geometry_model_.geometryObjects[geometry_id];
CollisionObjectPose pose;
pose.geometry_index = geometry_id;
pose.position = placement.translation();
pose.orientation = Eigen::Quaterniond(placement.rotation()).normalized();
pose.bounding_radius_m = std::max(0.0, geometry.geometry->aabb_radius);
snapshot->objects.push_back(std::move(pose));
}
if (error) {
error->clear();
}
return true;
}
SelfCollisionResult check(const CollisionGeometrySnapshot& snapshot)
{
SelfCollisionResult result;
if (!initialized_) {
result.error = "SelfCollisionChecker is not initialized";
return result;
}
if (snapshot.objects.size() != selected_geometry_indices_.size()) {
result.error = "Collision snapshot size does not match initialized geometry";
return result;
}
for (std::size_t index = 0; index < snapshot.objects.size(); ++index) {
const auto& pose = snapshot.objects[index];
if (pose.geometry_index != selected_geometry_indices_[index] ||
pose.geometry_index >= geometry_data_->oMg.size()) {
result.error = "Collision snapshot geometry order is invalid";
return result;
}
if (!pose.position.allFinite() || !pose.orientation.coeffs().allFinite() ||
pose.orientation.norm() <= std::numeric_limits<double>::epsilon()) {
result.error = "Collision snapshot contains an invalid pose";
return result;
}
geometry_data_->oMg[pose.geometry_index] = pinocchio::SE3(
pose.orientation.normalized().toRotationMatrix(), pose.position);
}
try {
const std::size_t pair_index =
pinocchio::computeDistances(geometry_model_, *geometry_data_);
if (pair_index >= geometry_model_.collisionPairs.size()) {
result.error = "Collision distance computation returned no active pair";
return result;
}
const auto& pair = geometry_model_.collisionPairs[pair_index];
result.minimum_distance_m = geometry_data_->distanceResults[pair_index].min_distance;
result.first = geometry_link_names_[pair.first];
result.second = geometry_link_names_[pair.second];
result.in_collision = result.minimum_distance_m <= 0.0;
result.valid = std::isfinite(result.minimum_distance_m);
if (!result.valid) {
result.error = "Collision distance is not finite";
}
} catch (const std::exception& exception) {
result.error = "Collision distance computation failed: " + std::string(exception.what());
}
return result;
}
SelfCollisionResult check(const std::vector<double>& joint_positions)
{
CollisionGeometrySnapshot snapshot;
std::string error;
if (!makeSnapshot(joint_positions, &snapshot, &error)) {
SelfCollisionResult result;
result.error = std::move(error);
return result;
}
return check(snapshot);
}
void reset()
{
initialized_ = false;
joint_names_.clear();
joint_q_indices_.clear();
selected_geometry_indices_.clear();
geometry_link_names_.clear();
geometry_data_.reset();
data_.reset();
model_ = pinocchio::Model{};
geometry_model_ = pinocchio::GeometryModel{};
neutral_q_.resize(0);
}
bool initialized_{false};
std::vector<std::string> joint_names_;
std::vector<int> joint_q_indices_;
std::vector<pinocchio::GeomIndex> selected_geometry_indices_;
std::vector<std::string> geometry_link_names_;
pinocchio::Model model_;
pinocchio::GeometryModel geometry_model_;
std::unique_ptr<pinocchio::Data> data_;
std::unique_ptr<pinocchio::GeometryData> geometry_data_;
Eigen::VectorXd neutral_q_;
};
SelfCollisionChecker::SelfCollisionChecker()
: impl_(std::make_unique<Impl>())
{
}
SelfCollisionChecker::~SelfCollisionChecker() = default;
SelfCollisionChecker::SelfCollisionChecker(SelfCollisionChecker&&) noexcept = default;
SelfCollisionChecker& SelfCollisionChecker::operator=(SelfCollisionChecker&&) noexcept = default;
bool SelfCollisionChecker::init(const std::string& urdf_path,
const std::vector<std::string>& active_joint_names,
const SelfCollisionOptions& options,
std::string* error)
{
return impl_->init(urdf_path, active_joint_names, options, error);
}
bool SelfCollisionChecker::makeSnapshot(const std::vector<double>& joint_positions,
CollisionGeometrySnapshot* snapshot,
std::string* error)
{
return impl_->makeSnapshot(joint_positions, snapshot, error);
}
SelfCollisionResult SelfCollisionChecker::check(const CollisionGeometrySnapshot& snapshot)
{
return impl_->check(snapshot);
}
SelfCollisionResult SelfCollisionChecker::check(const std::vector<double>& joint_positions)
{
return impl_->check(joint_positions);
}
bool SelfCollisionChecker::initialized() const
{
return impl_->initialized_;
}
std::size_t SelfCollisionChecker::dof() const
{
return impl_->joint_names_.size();
}
std::size_t SelfCollisionChecker::activePairCount() const
{
return impl_->geometry_model_.collisionPairs.size();
}
const std::vector<std::string>& SelfCollisionChecker::jointNames() const
{
return impl_->joint_names_;
}
} // namespace cmvr

View File

@ -0,0 +1,110 @@
#include <chrono>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include "algorithms/collision_detection/self_collision/include/distance_sampling_policy.h"
#include "algorithms/collision_detection/self_collision/include/self_collision_checker.h"
namespace cmvr {
namespace {
const std::vector<std::string> kRightArmJoints{
"R_SHOULDER_P",
"R_SHOULDER_R",
"R_SHOULDER_Y",
"R_ELBOW_R",
"R_WRIST_P",
"R_WRIST_Y",
"R_WRIST_R",
};
std::string collisionUrdfPath()
{
return std::string(CMVR_ES_SOURCE_DIR) +
"/model/xiaoyan_description/dual_arm_collision.urdf";
}
CollisionGeometrySnapshot singleObjectSnapshot(double x,
double angle,
double radius)
{
CollisionGeometrySnapshot snapshot;
CollisionObjectPose pose;
pose.geometry_index = 1;
pose.position = Eigen::Vector3d(x, 0.0, 0.0);
pose.orientation = Eigen::AngleAxisd(angle, Eigen::Vector3d::UnitZ());
pose.bounding_radius_m = radius;
snapshot.objects.push_back(pose);
return snapshot;
}
TEST(SelfCollisionCheckerTest, LoadsRightArmFromDualArmUrdf)
{
SelfCollisionChecker checker;
std::string error;
ASSERT_TRUE(checker.init(collisionUrdfPath(), kRightArmJoints, {}, &error)) << error;
EXPECT_EQ(checker.dof(), 7U);
EXPECT_GT(checker.activePairCount(), 0U);
CollisionGeometrySnapshot snapshot;
ASSERT_TRUE(checker.makeSnapshot(std::vector<double>(7, 0.0), &snapshot, &error)) << error;
EXPECT_EQ(snapshot.objects.size(), 11U);
const SelfCollisionResult result = checker.check(snapshot);
ASSERT_TRUE(result.valid) << result.error;
EXPECT_TRUE(result.first.rfind("L_", 0) != 0);
EXPECT_TRUE(result.second.rfind("L_", 0) != 0);
}
TEST(SelfCollisionCheckerTest, RejectsWrongJointVectorSize)
{
SelfCollisionChecker checker;
std::string error;
ASSERT_TRUE(checker.init(collisionUrdfPath(), kRightArmJoints, {}, &error)) << error;
CollisionGeometrySnapshot snapshot;
EXPECT_FALSE(checker.makeSnapshot(std::vector<double>(6, 0.0), &snapshot, &error));
EXPECT_NE(error.find("size mismatch"), std::string::npos);
}
TEST(SelfCollisionCheckerTest, RemovesConfiguredIgnoredPair)
{
SelfCollisionChecker baseline;
SelfCollisionChecker filtered;
std::string error;
ASSERT_TRUE(baseline.init(collisionUrdfPath(), kRightArmJoints, {}, &error)) << error;
SelfCollisionOptions options;
options.ignored_pairs.push_back({"base_link", "R_ELBOW_R_S"});
ASSERT_TRUE(filtered.init(collisionUrdfPath(), kRightArmJoints, options, &error)) << error;
EXPECT_EQ(filtered.activePairCount() + 1, baseline.activePairCount());
}
TEST(DistanceSamplingPolicyTest, SamplesByAccumulatedGeometryDisplacement)
{
DistanceSamplingPolicy policy;
DistanceSamplingOptions options;
options.max_geometry_displacement_m = 0.002;
options.max_check_period_s = 0.01;
std::string error;
ASSERT_TRUE(policy.configure(options, &error)) << error;
const auto start = DistanceSamplingPolicy::Clock::now();
const auto initial = singleObjectSnapshot(0.0, 0.0, 0.2);
EXPECT_TRUE(policy.shouldCheck(initial, start));
policy.markChecked(initial, start);
EXPECT_FALSE(policy.shouldCheck(
singleObjectSnapshot(0.001, 0.0, 0.2), start + std::chrono::milliseconds(1)));
EXPECT_TRUE(policy.shouldCheck(
singleObjectSnapshot(0.0021, 0.0, 0.2), start + std::chrono::milliseconds(2)));
EXPECT_TRUE(policy.shouldCheck(
singleObjectSnapshot(0.0, 0.011, 0.2), start + std::chrono::milliseconds(2)));
EXPECT_TRUE(policy.shouldCheck(
initial, start + std::chrono::milliseconds(10)));
}
} // namespace
} // namespace cmvr

111
cmvr-es/common/README.md Normal file
View File

@ -0,0 +1,111 @@
# Common 模块开发指南
`common/` 保存可被设备、算法、管理器和协议层复用的基础能力。这里适合放稳定、协议无关、厂商无关的类型与工具,不适合放设备连接、业务服务或任务调度逻辑。
返回[项目总览](../../README.md)。
## 目录职责
| 目录 | 职责 |
| --- | --- |
| `base/` | 日志、基础常量、gRPC 辅助函数和线程安全缓冲区 |
| `config/` | 配置根目录解析和 Proto Text 配置加载 |
| `io/` | Protobuf 二进制与 TextFormat 文件读写 |
| `math/` | 坐标变换、关节限制、QP 和运动数学 |
| `media/` | 协议无关媒体模型以及 FFmpeg 采集、编码、写文件能力 |
| `types/` | 跨后端共享的领域类型,例如 AGV、机械臂和几何类型 |
| `vision/` | 图像显示、投影等视觉辅助代码 |
## 依赖边界
新增公共组件时应遵守:
- 不依赖 `service/`、`task/` 或具体厂商设备实现;
- 不保存 gRPC/QUIC 连接、session 或客户端状态;
- 通用类型不包含厂商报文字段、端口号和私有错误码;
- 需要调用设备的逻辑应放在 manager adapter、service 或 task
- 需要第三方库的 `.cpp` 组件应通过明确的 CMake target 暴露依赖;
- 避免在公共头文件中使用全局 `using namespace` 或引入大体量实现头。
当前 `common` 共享库目标是 `cmvr_es::common`,日志是独立目标 `cmvr_es::logging`。新增 `.cpp` 文件时,需要更新 [`CMakeLists.txt`](CMakeLists.txt) 或对应子目录 CMake纯头文件不需要加入 `add_library` 源文件列表。
## 新增共享类型
1. 选择 `types/<domain>/` 或已有领域文件;
2. 类型使用明确单位,例如米、弧度、秒、纳秒;
3. 为容器长度、自由度和数值范围提供校验函数;
4. 保持控制器无关,将厂商字段转换为通用枚举或结果;
5. 确认不会迫使所有调用方引入设备 SDK
6. 增加边界值和错误输入测试。
AGV 通用类型应参考 [`types/agv/agv_types.h`](types/agv/agv_types.h),机械臂通用类型应参考 [`types/arm/arm_types.h`](types/arm/arm_types.h)。不要为了一个具体控制器把协议结构塞回 `abstract_*.h`
## 媒体模型
[`media/media_frame.h`](media/media_frame.h) 中的 `TrackDescriptor`、`MediaFrame` 及 payload 在构造后不可变,可被多个协议消费者共享。
扩展媒体字段时需要保持:
- `TrackDescriptor::generation` 非零,编码参数变化时创建新 descriptor
- PTS、DTS 和 duration 使用 descriptor 的 `time_base`
- `capture_time_ns` 使用单调时钟,供节奏控制和延迟统计;
- `capture_utc_ns` 只作为可选墙上时间,不能用于计算持续时间;
- H.264/H.265 明确 `ANNEX_B``AVCC`
- AAC、Opus、PCM 明确 payload format、采样率和声道数
- 不把 QUIC、gRPC 或浏览器专有字段加入通用帧。
设备媒体接入流程见 [`../manager/README.md`](../manager/README.md) 的 MediaSourceHub 章节。
## 环形队列选择
[`base/ring_buffer.h`](base/ring_buffer.h) 当前包含三类缓冲区:
| 类型 | 使用场景 | 重要约束 |
| --- | --- | --- |
| `RingBuffer<T>` | 只需要保存最近 N 项并批量读取 | 覆盖最旧项,没有阻塞读取 |
| `SPMCRingBuffer<T>` | 历史单生产者场景 | 独立 `reader_tail` 只能由一个线程拥有 |
| `BroadcastFrameRing<T>` | 新的媒体或广播式多消费者场景 | 每个消费者使用独立 Cursor保存不可变共享对象 |
新的实时多消费者模块优先使用 `BroadcastFrameRing<T>`
- capacity 必须大于零;
- 同一 Cursor 不得被多个线程同时读取或移动;
- 慢消费者落后时会跳到最旧保留项,并得到精确 dropped count
- `reset()` 开启新 generation旧 Cursor 在下一次成功读取时看到变化;
- `close()` 唤醒等待者,关闭后不能继续发布;
- 不要先读取 head 再无锁读取槽位,应使用队列提供的原子读取接口。
## 配置和文件路径
[`config/config_files.h`](config/config_files.h) 提供:
- `resolveConfigFile()`:相对根配置目录解析业务配置;
- `resolveResourceFile()`:在配置根及其父目录中查找模型等资源;
- `loadConfigFile()` / `saveConfigFile()`:读写 Proto Text 配置。
进程启动后配置根由 `main.cpp` 设置。公共组件不应自行使用当前工作目录拼接配置路径。
[`io/proto_file_io.h`](io/proto_file_io.h) 写出的 TextFormat 文件权限为 `0600`。保存运行时配置前,应确认目标目录存在,并避免把生产密钥写入仓库。
## 新增公共组件
1. 确认能力确实会被两个及以上模块复用;
2. 定义最小 API 和所有权、线程安全、错误语义;
3. 将头文件放入合适子目录,将实现放入相邻 `.cpp`
4. 更新 CMake target 和 `target_link_libraries`
5. 不使用未声明的传递依赖;
6. 增加无设备单元测试;
7. 对并发组件增加关闭、超时、覆盖、取消和析构测试;
8. 使用 ASan/TSan 时检查生命周期和数据竞争。
推荐测试目标放在组件相邻的 `tests/`,并在 `BUILD_TESTING` 下通过 `add_test()` 登记。仅创建 `_test` 可执行文件不会自动进入 CTest。
## 提交检查
- [ ] API 不依赖具体设备或传输协议
- [ ] 公共类型有明确单位和有效性规则
- [ ] 所有权及线程安全写入注释
- [ ] 新增 `.cpp` 和依赖已经加入 CMake
- [ ] 缓冲区关闭能够唤醒等待线程
- [ ] 不记录密码、私钥或大块媒体 payload
- [ ] 无设备测试可以在开发主机运行

View File

@ -6,16 +6,25 @@
#define CMVR_ES_RING_BUFFER_H
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <vector>
#include <atomic>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
template<typename T>
class RingBuffer {
public:
explicit RingBuffer(size_t capacity) : capacity_(capacity) {}
explicit RingBuffer(size_t capacity) : capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("RingBuffer capacity must be greater than zero");
}
}
void push(const T& item) {
std::lock_guard<std::mutex> lock(mutex_);
@ -50,47 +59,49 @@ template<typename T>
class SPMCRingBuffer {
public:
explicit SPMCRingBuffer(size_t capacity)
: buffer_(capacity), capacity_(capacity),
head_(0), tail_(0) {}
: buffer_(capacity), capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("SPMCRingBuffer capacity must be greater than zero");
}
}
// 写入操作(仅支持单个生产者)
void push(const T& item) {
size_t head = head_.load(std::memory_order_relaxed);
size_t tail = tail_.load(std::memory_order_acquire);
buffer_[head % capacity_] = item;
head = head + 1;
head_.store(head, std::memory_order_release);
if (head - tail >= capacity_) {
{
std::lock_guard<std::mutex> lock(mutex_);
buffer_[head_ % capacity_] = item;
++head_;
if (head_ - tail_ > capacity_) {
// 队列满,覆盖最旧的数据
tail_.store(tail + 1, std::memory_order_release);
tail_ = head_ - capacity_;
}
}
condition_.notify_all();
}
// 单消费者使用(内部 tail_
std::optional<T> pop() {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t head = head_.load(std::memory_order_acquire);
if (tail >= head) return std::nullopt;
T value = buffer_[tail % capacity_];
tail_.store(tail + 1, std::memory_order_release);
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) return std::nullopt;
T value = buffer_[tail_ % capacity_];
++tail_;
return value;
}
std::optional<T> getLast() {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t head = head_.load(std::memory_order_acquire);
if (tail >= head) return std::nullopt;
T value = buffer_[head_ % capacity_];
return value;
std::optional<T> getLast() const {
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) return std::nullopt;
return buffer_[(head_ - 1) % capacity_];
}
// 多消费者使用(每个读者独立维护 reader_tail
// 多消费者使用(每个读者独立维护 reader_tail。同一个 reader_tail 只能由
// 一个消费线程拥有,且不要把该游标与无参 pop() 的共享 tail_ 混合作为同一路读取。
std::optional<T> pop(size_t& reader_tail) const {
size_t head = head_.load(std::memory_order_acquire);
if (reader_tail >= head) return std::nullopt;
if (head > reader_tail + capacity_) {
std::lock_guard<std::mutex> lock(mutex_);
if (reader_tail >= head_) return std::nullopt;
if (reader_tail < tail_) {
// 数据已被覆盖,跳过无效读取区间
reader_tail = head - capacity_;
reader_tail = tail_;
return std::nullopt;
}
T value = buffer_[reader_tail % capacity_];
@ -98,16 +109,52 @@ public:
return value;
}
// 在同一次加锁中把独立读游标跳到当前最新元素并读取,避免先 getHead()
// 再 pop() 时被高速覆盖造成的检查/读取竞态。
std::optional<T> getLatest(size_t& reader_tail) const {
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) {
return std::nullopt;
}
reader_tail = head_ - 1;
T value = buffer_[reader_tail % capacity_];
++reader_tail;
return value;
}
template<class Rep, class Period>
std::optional<T> waitPop(
size_t& reader_tail,
const std::chrono::duration<Rep, Period>& timeout) const {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait_for(lock, timeout, [&] { return reader_tail < head_; });
if (reader_tail >= head_) {
return std::nullopt;
}
if (reader_tail < tail_) {
reader_tail = tail_;
}
if (reader_tail >= head_) {
return std::nullopt;
}
T value = buffer_[reader_tail % capacity_];
++reader_tail;
return value;
}
size_t size() const {
return head_.load(std::memory_order_acquire) - tail_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return head_ - tail_;
}
size_t getHead() const {
return head_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return head_;
}
size_t getTail() const {
return tail_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return tail_;
}
bool empty() const {
@ -119,16 +166,278 @@ public:
}
void clear() {
head_.store(0, std::memory_order_release);
tail_.store(0, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(mutex_);
// Keep sequence numbers monotonic so cursors created before clear()
// cannot alias newly published slots after the reset.
tail_ = head_;
}
condition_.notify_all();
}
private:
std::vector<T> buffer_;
mutable std::vector<T> buffer_;
const size_t capacity_;
mutable std::mutex mutex_;
mutable std::condition_variable condition_;
size_t head_{0}; // 单调写序号clear() 仅推进 tail_避免旧游标 ABA。
size_t tail_{0}; // 当前仍保留的最旧序号,同时也是 pop() 的共享读指针。
};
std::atomic<size_t> head_; // 共享写指针
std::atomic<size_t> tail_; // 共享读指针(仅用于 SPSC 模式)
// 线程安全的多消费者广播缓冲区。缓冲区只保存不可变共享对象,消费者通过各自
// 的 Cursor 独立前进;慢消费者被覆盖的数据会累计到 Cursor::dropped_count。
// Cursor 是单线程所有权对象,不可由多个线程同时读写;每个消费者应创建自己的 Cursor。
template<typename T>
class BroadcastFrameRing {
public:
using ValuePtr = std::shared_ptr<const T>;
enum class StartPosition {
NEXT_PUBLISHED,
OLDEST_AVAILABLE,
LATEST_AVAILABLE
};
struct Cursor {
uint64_t generation{0};
uint64_t next_sequence{0};
uint64_t dropped_count{0};
StartPosition start_position{StartPosition::NEXT_PUBLISHED};
private:
bool generation_changed{false};
uint64_t reported_dropped_count{0};
friend class BroadcastFrameRing<T>;
};
struct ReadResult {
ValuePtr value;
uint64_t generation{0};
uint64_t sequence{0};
uint64_t dropped_count{0};
uint64_t dropped_since_last_read{0};
bool generation_changed{false};
};
struct Stats {
size_t capacity{0};
size_t size{0};
uint64_t generation{0};
uint64_t next_sequence{0};
uint64_t dropped_count{0};
bool closed{false};
};
explicit BroadcastFrameRing(const size_t capacity)
: capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("BroadcastFrameRing capacity must be greater than zero");
}
}
BroadcastFrameRing(const BroadcastFrameRing&) = delete;
BroadcastFrameRing& operator=(const BroadcastFrameRing&) = delete;
Cursor makeCursor(const StartPosition start_position = StartPosition::NEXT_PUBLISHED) const {
std::lock_guard<std::mutex> lock(mutex_);
Cursor cursor;
cursor.generation = generation_;
cursor.start_position = start_position;
cursor.next_sequence = startSequenceLocked_(start_position);
return cursor;
}
std::optional<uint64_t> publish(ValuePtr value) {
if (!value) {
return std::nullopt;
}
std::optional<uint64_t> published_sequence;
{
std::lock_guard<std::mutex> lock(mutex_);
if (closed_) {
return std::nullopt;
}
const uint64_t sequence = next_sequence_++;
if (entries_.size() == capacity_) {
entries_.pop_front();
++dropped_count_;
}
entries_.push_back(Entry{generation_, sequence, std::move(value)});
published_sequence = sequence;
}
condition_.notify_all();
return published_sequence;
}
std::optional<ReadResult> tryRead(Cursor& cursor) const {
std::lock_guard<std::mutex> lock(mutex_);
return tryReadLocked_(cursor);
}
// Low-latency consumers can use this before reading to abandon an excessive
// backlog atomically. When the number of currently readable entries exceeds
// maximum_pending_frames, every pending entry is discarded and the cursor is
// advanced to the next sequence that will be published. Frames already
// overwritten by the ring and frames actively discarded here are both
// reflected in Cursor::dropped_count; the next successful read reports their
// sum through ReadResult::dropped_since_last_read.
uint64_t discardPendingIfExceeds(
Cursor& cursor,
const size_t maximum_pending_frames) const {
std::lock_guard<std::mutex> lock(mutex_);
synchronizeCursorGenerationLocked_(cursor);
if (!entries_.empty()) {
const uint64_t oldest_sequence = entries_.front().sequence;
if (cursor.next_sequence < oldest_sequence) {
cursor.dropped_count += oldest_sequence - cursor.next_sequence;
cursor.next_sequence = oldest_sequence;
}
}
const uint64_t pending =
cursor.next_sequence < next_sequence_
? next_sequence_ - cursor.next_sequence
: 0;
if (pending <= static_cast<uint64_t>(maximum_pending_frames)) {
return 0;
}
cursor.next_sequence = next_sequence_;
cursor.dropped_count += pending;
return pending;
}
template<class Rep, class Period>
std::optional<ReadResult> waitRead(
Cursor& cursor,
const std::chrono::duration<Rep, Period>& timeout) const {
const auto deadline = std::chrono::steady_clock::now() + timeout;
std::unique_lock<std::mutex> lock(mutex_);
while (true) {
if (auto result = tryReadLocked_(cursor)) {
return result;
}
if (closed_) {
return std::nullopt;
}
if (condition_.wait_until(lock, deadline) == std::cv_status::timeout) {
return tryReadLocked_(cursor);
}
}
}
// 开始一个新的发布代次。旧 Cursor 会在下一次成功读取时收到
// generation_changed=true序号从 0 重新开始。
uint64_t reset() {
uint64_t generation = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
entries_.clear();
++generation_;
next_sequence_ = 0;
closed_ = false;
generation = generation_;
}
condition_.notify_all();
return generation;
}
void close() {
{
std::lock_guard<std::mutex> lock(mutex_);
closed_ = true;
}
condition_.notify_all();
}
bool closed() const {
std::lock_guard<std::mutex> lock(mutex_);
return closed_;
}
Stats stats() const {
std::lock_guard<std::mutex> lock(mutex_);
return Stats{capacity_, entries_.size(), generation_, next_sequence_, dropped_count_, closed_};
}
private:
struct Entry {
uint64_t generation;
uint64_t sequence;
ValuePtr value;
};
uint64_t startSequenceLocked_(const StartPosition start_position) const {
if (entries_.empty()) {
return next_sequence_;
}
switch (start_position) {
case StartPosition::OLDEST_AVAILABLE:
return entries_.front().sequence;
case StartPosition::LATEST_AVAILABLE:
return entries_.back().sequence;
case StartPosition::NEXT_PUBLISHED:
default:
return next_sequence_;
}
}
void synchronizeCursorGenerationLocked_(Cursor& cursor) const {
if (cursor.generation == generation_) {
return;
}
cursor.generation = generation_;
cursor.next_sequence = startSequenceLocked_(cursor.start_position);
cursor.generation_changed = true;
}
std::optional<ReadResult> tryReadLocked_(Cursor& cursor) const {
synchronizeCursorGenerationLocked_(cursor);
if (entries_.empty()) {
return std::nullopt;
}
const uint64_t oldest_sequence = entries_.front().sequence;
if (cursor.next_sequence < oldest_sequence) {
cursor.dropped_count += oldest_sequence - cursor.next_sequence;
cursor.next_sequence = oldest_sequence;
}
if (cursor.next_sequence >= next_sequence_) {
return std::nullopt;
}
const size_t index = static_cast<size_t>(cursor.next_sequence - oldest_sequence);
if (index >= entries_.size()) {
return std::nullopt;
}
const Entry& entry = entries_[index];
++cursor.next_sequence;
const uint64_t dropped_since_last = cursor.dropped_count - cursor.reported_dropped_count;
cursor.reported_dropped_count = cursor.dropped_count;
ReadResult result;
result.value = entry.value;
result.generation = entry.generation;
result.sequence = entry.sequence;
result.dropped_count = cursor.dropped_count;
result.dropped_since_last_read = dropped_since_last;
result.generation_changed = cursor.generation_changed;
cursor.generation_changed = false;
return result;
}
const size_t capacity_;
mutable std::mutex mutex_;
mutable std::condition_variable condition_;
std::deque<Entry> entries_;
uint64_t generation_{1};
uint64_t next_sequence_{0};
uint64_t dropped_count_{0};
bool closed_{false};
};

View File

@ -0,0 +1,244 @@
#ifndef CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H
#define CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace cmvr::media {
enum class MediaKind : uint8_t {
UNKNOWN = 0,
VIDEO = 1,
AUDIO = 2
};
enum class Codec : uint8_t {
UNKNOWN = 0,
H264 = 1,
H265 = 2,
OPUS = 3,
PCM_S16LE = 4,
AAC = 5
};
enum class PayloadFormat : uint8_t {
UNKNOWN = 0,
ANNEX_B = 1,
AVCC = 2,
RAW = 3,
OPUS_PACKET = 4,
AAC_ADTS = 5
};
struct Rational {
int32_t numerator{0};
int32_t denominator{1};
constexpr bool valid() const noexcept {
return numerator > 0 && denominator > 0;
}
};
constexpr bool operator==(const Rational lhs, const Rational rhs) noexcept {
return lhs.numerator == rhs.numerator && lhs.denominator == rhs.denominator;
}
constexpr bool operator!=(const Rational lhs, const Rational rhs) noexcept {
return !(lhs == rhs);
}
// A TrackDescriptor is immutable after construction. Reconfiguration is represented by
// publishing a new descriptor with a larger generation and attaching it to later frames.
class TrackDescriptor final {
public:
struct Config {
std::string id;
std::string source_id;
MediaKind kind{MediaKind::UNKNOWN};
Codec codec{Codec::UNKNOWN};
PayloadFormat payload_format{PayloadFormat::UNKNOWN};
Rational time_base{};
uint32_t width{0};
uint32_t height{0};
uint32_t sample_rate{0};
uint32_t channels{0};
uint32_t nominal_rate{0};
float fx{0.0F};
float fy{0.0F};
float cx{0.0F};
float cy{0.0F};
std::vector<float> distortion;
uint64_t generation{1};
std::vector<uint8_t> codec_config;
};
explicit TrackDescriptor(Config config)
: id(std::move(config.id)),
source_id(std::move(config.source_id)),
kind(config.kind),
codec(config.codec),
payload_format(config.payload_format),
time_base(config.time_base),
width(config.width),
height(config.height),
sample_rate(config.sample_rate),
channels(config.channels),
nominal_rate(config.nominal_rate),
fx(config.fx),
fy(config.fy),
cx(config.cx),
cy(config.cy),
distortion(std::move(config.distortion)),
generation(config.generation),
codec_config(std::move(config.codec_config)) {
if (id.empty()) {
throw std::invalid_argument("TrackDescriptor id must not be empty");
}
if (source_id.empty()) {
throw std::invalid_argument("TrackDescriptor source_id must not be empty");
}
if (kind == MediaKind::UNKNOWN) {
throw std::invalid_argument("TrackDescriptor kind must not be UNKNOWN");
}
if (!time_base.valid()) {
throw std::invalid_argument("TrackDescriptor time_base is invalid");
}
if (generation == 0) {
throw std::invalid_argument("TrackDescriptor generation must be greater than zero");
}
}
const std::string id;
const std::string source_id;
const MediaKind kind;
const Codec codec;
const PayloadFormat payload_format;
const Rational time_base;
const uint32_t width;
const uint32_t height;
const uint32_t sample_rate;
const uint32_t channels;
const uint32_t nominal_rate;
const float fx;
const float fy;
const float cx;
const float cy;
const std::vector<float> distortion;
const uint64_t generation;
const std::vector<uint8_t> codec_config;
};
using TrackDescriptorPtr = std::shared_ptr<const TrackDescriptor>;
inline bool equivalentTrackDescriptor(
const TrackDescriptor& lhs,
const TrackDescriptor& rhs) noexcept {
return lhs.id == rhs.id &&
lhs.source_id == rhs.source_id &&
lhs.kind == rhs.kind &&
lhs.codec == rhs.codec &&
lhs.payload_format == rhs.payload_format &&
lhs.time_base == rhs.time_base &&
lhs.width == rhs.width &&
lhs.height == rhs.height &&
lhs.sample_rate == rhs.sample_rate &&
lhs.channels == rhs.channels &&
lhs.nominal_rate == rhs.nominal_rate &&
lhs.fx == rhs.fx &&
lhs.fy == rhs.fy &&
lhs.cx == rhs.cx &&
lhs.cy == rhs.cy &&
lhs.distortion == rhs.distortion &&
lhs.generation == rhs.generation &&
lhs.codec_config == rhs.codec_config;
}
// MediaFrame and its payload are immutable and therefore safe to share across all protocol
// adapters and consumers without copying. PTS/DTS use TrackDescriptor::time_base;
// capture_time_ns is monotonic for pacing, while capture_utc_ns is optional wall time.
class MediaFrame final {
public:
using Payload = std::vector<uint8_t>;
using PayloadPtr = std::shared_ptr<const Payload>;
struct Config {
TrackDescriptorPtr descriptor;
Payload payload;
uint64_t sequence{0};
// Opaque producer-native timing/counter values. Their units and epoch
// are source-defined; zero means that the source did not provide them.
uint64_t source_timestamp{0};
uint64_t source_frame_number{0};
int64_t pts{0};
int64_t dts{0};
int64_t duration{0};
uint64_t capture_time_ns{0};
int64_t capture_utc_ns{0};
bool key_frame{false};
bool discontinuity{false};
};
explicit MediaFrame(Config config)
: descriptor(std::move(config.descriptor)),
payload(std::make_shared<const Payload>(std::move(config.payload))),
sequence(config.sequence),
source_timestamp(config.source_timestamp),
source_frame_number(config.source_frame_number),
pts(config.pts),
dts(config.dts),
duration(config.duration),
capture_time_ns(config.capture_time_ns),
capture_utc_ns(config.capture_utc_ns),
key_frame(config.key_frame),
discontinuity(config.discontinuity) {
if (!descriptor) {
throw std::invalid_argument("MediaFrame descriptor must not be null");
}
}
const uint8_t* data() const noexcept {
return payload->empty() ? nullptr : payload->data();
}
size_t size() const noexcept {
return payload->size();
}
bool empty() const noexcept {
return payload->empty();
}
const TrackDescriptorPtr descriptor;
const PayloadPtr payload;
const uint64_t sequence;
const uint64_t source_timestamp;
const uint64_t source_frame_number;
const int64_t pts;
const int64_t dts;
const int64_t duration;
const uint64_t capture_time_ns;
const int64_t capture_utc_ns;
const bool key_frame;
const bool discontinuity;
};
using MediaFramePtr = std::shared_ptr<const MediaFrame>;
inline TrackDescriptorPtr makeTrackDescriptor(TrackDescriptor::Config config) {
return std::make_shared<const TrackDescriptor>(std::move(config));
}
inline MediaFramePtr makeMediaFrame(MediaFrame::Config config) {
return std::make_shared<const MediaFrame>(std::move(config));
}
} // namespace cmvr::media
#endif // CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H

View File

@ -0,0 +1,420 @@
#ifndef CMVR_ES_AGV_TYPES_H
#define CMVR_ES_AGV_TYPES_H
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/types/geometry_types.h"
namespace cmvr::device {
/**
* @brief AGV /
*
*
* AGV /
*/
enum class AgvErrorCode {
OK = 0,
NotConnected,
AlreadyConnected,
ConnectionFailed,
Timeout,
InvalidArgument,
LocalizationLost,
MapNotLoaded,
TaskRejected,
TaskFailed,
TaskCanceled,
CommandFailed,
EmergencyStopped,
Fault,
UnsupportedCommand,
UnknownError
};
/**
* @brief AGV
*/
struct AgvResult {
AgvErrorCode code{AgvErrorCode::OK};
std::string message{"OK"};
bool ok() const { return code == AgvErrorCode::OK; }
static AgvResult success() { return {AgvErrorCode::OK, "OK"}; }
static AgvResult failure(AgvErrorCode c, const std::string& msg) { return {c, msg}; }
};
/**
* @brief AGV
*/
enum class AgvMode {
Unknown = 0,
Disconnected,
Idle,
Manual,
Auto,
Charging,
Paused,
Stopped,
Fault,
EmergencyStop
};
/**
* @brief
*/
enum class AgvTaskState {
None = 0,
Waiting,
Running,
Paused,
Completed,
Failed,
Canceled
};
/**
* @brief
*/
enum class AgvTaskType {
None = 0,
NavigateToPose,
NavigateToStation,
FollowPath,
Dock,
Charge,
Custom
};
/**
* @brief AGV
*
* 线//
*/
struct AgvVelocity {
double vx{0.0};
double vy{0.0};
double wz{0.0};
};
/**
* @brief
*
* 0 使
*/
struct AgvMotionOptions {
double max_speed{0.0};
double max_angular_speed{0.0};
double max_acceleration{0.0};
double max_angular_acceleration{0.0};
double reach_distance{0.0};
double reach_angle{0.0};
double speed_ratio{1.0};
bool asynchronous{true};
};
/**
* @brief AGV
*
* AGV
*
*/
struct AgvAdapterParams {
std::unordered_map<std::string, std::string> values;
bool empty() const { return values.empty(); }
std::optional<std::string> getString(const std::string& key) const
{
const auto it = values.find(key);
if (it == values.end()) {
return std::nullopt;
}
return it->second;
}
std::optional<double> getDouble(const std::string& key) const
{
const auto value = getString(key);
if (!value) {
return std::nullopt;
}
try {
return std::stod(*value);
} catch (...) {
return std::nullopt;
}
}
std::optional<bool> getBool(const std::string& key) const
{
const auto value = getString(key);
if (!value) {
return std::nullopt;
}
if (*value == "1" || *value == "true" || *value == "yes" || *value == "on") {
return true;
}
if (*value == "0" || *value == "false" || *value == "no" || *value == "off") {
return false;
}
return std::nullopt;
}
};
/**
* @brief AgvRuntimeState
*/
struct AgvBatteryState {
double percentage{0.0};
double voltage{0.0};
double current{0.0};
double temperature{0.0};
bool charging{false};
};
/**
* @brief AGV
*
* AGV 便
* getter
*/
struct AgvRuntimeState {
double timestamp{0.0};
AgvMode mode{AgvMode::Unknown};
bool connected{false};
bool localized{false};
bool moving{false};
bool fault{false};
bool emergency_stopped{false};
math::Pose2d pose{};
AgvVelocity velocity{};
AgvBatteryState battery{};
std::string current_map;
std::string current_station;
std::string last_error;
};
/**
* @brief AGV /
*/
struct AgvStation {
std::string id;
std::string type;
math::Pose2d pose{};
std::string description;
};
/**
* @brief
*/
struct AgvPathSegment {
std::string source_station;
std::string target_station;
};
/**
* @brief AGV
*
* content SRC1100 rawmap zip
*/
struct AgvMappingDataFile {
std::string name;
std::string content;
};
/**
* @brief
*/
struct AgvMappingData {
int start_index{0};
int next_index{0};
std::vector<AgvMappingDataFile> files;
};
/**
* @brief
*
* 2D3D
* AGV
*/
enum class AgvMapDimension {
Unspecified = 0,
Map2D,
Map3D,
Map2DAnd3D
};
/**
* @brief
*/
enum class AgvMapUpdateType {
Unspecified = 0,
Snapshot,
Incremental,
Reset
};
/**
* @brief
*/
enum class AgvMapObjectType {
Unspecified = 0,
Station,
Line,
Area,
QrTag,
Reflector,
BinLocation,
ExternalDevice
};
/**
* @brief
*/
struct AgvMapPoint3D {
double x{0.0};
double y{0.0};
double z{0.0};
};
/**
* @brief 使
*/
struct AgvMapObject {
std::string id;
AgvMapObjectType type{AgvMapObjectType::Unspecified};
std::vector<AgvMapPoint3D> points;
double heading{0.0};
std::unordered_map<std::string, std::string> properties;
};
/**
* @brief 2D
*
* data -1 0 100
* /data objects
*/
struct AgvUnifiedMap2D {
std::string frame_id{"map"};
double timestamp{0.0};
double resolution{0.0};
std::uint32_t width{0};
std::uint32_t height{0};
math::Pose2d origin{};
std::vector<std::int32_t> data;
std::vector<AgvMapObject> objects;
};
/**
* @brief 3D
*/
struct AgvMapPointSample3D {
double x{0.0};
double y{0.0};
double z{0.0};
float intensity{0.0F};
std::uint32_t ring{0};
double time_offset{0.0};
};
/**
* @brief 3D
*/
struct AgvMapVoxel3D {
std::int32_t x{0};
std::int32_t y{0};
std::int32_t z{0};
float probability{-1.0F};
};
/**
* @brief 3D
*/
struct AgvMapPlane3D {
AgvMapPoint3D center;
AgvMapPoint3D normal;
double d{0.0};
double radius{0.0};
};
/**
* @brief 3D
*/
struct AgvUnifiedMap3D {
std::string frame_id{"map"};
double timestamp{0.0};
double voxel_resolution{0.0};
std::vector<AgvMapPointSample3D> points;
std::vector<AgvMapVoxel3D> voxels;
std::vector<AgvMapPlane3D> planes;
std::vector<AgvMapObject> objects;
};
/**
* @brief
*/
struct AgvMapStreamOptions {
AgvMapDimension dimension{AgvMapDimension::Unspecified};
std::string map_name;
std::string resume_token;
bool snapshot{true};
bool incremental{false};
int max_chunk_bytes{0};
int wait_timeout_ms{1000};
};
/**
* @brief /
*/
struct AgvMappingOptions {
AgvMapDimension dimension{AgvMapDimension::Unspecified};
std::string map_name;
bool real_time{false};
};
/**
* @brief
*/
struct AgvUnifiedMapUpdate {
std::string map_id;
std::string session_id;
std::uint64_t sequence{0};
std::string resume_token;
AgvMapDimension dimension{AgvMapDimension::Unspecified};
AgvMapUpdateType update_type{AgvMapUpdateType::Unspecified};
std::string frame_id{"map"};
double timestamp{0.0};
bool snapshot_begin{false};
bool snapshot_end{false};
std::uint32_t chunk_index{0};
std::uint32_t chunk_count{0};
std::optional<AgvUnifiedMap2D> map_2d;
std::optional<AgvUnifiedMap3D> map_3d;
};
/**
* @brief
*
* AgvRuntimeState
*
*/
struct AgvNavigationStatus {
AgvTaskState state{AgvTaskState::None};
AgvTaskType type{AgvTaskType::None};
double progress{0.0};
std::string message;
};
/**
* @brief AGV API
*/
using AGVState = AgvRuntimeState;
} // namespace cmvr::device
#endif // CMVR_ES_AGV_TYPES_H

View File

@ -49,7 +49,7 @@ namespace cmvr::math {
typedef struct {
double x; //* unit: m
double y;
double theta;
double theta; //* unit: rad
} Pose2d;
}

130
cmvr-es/config/README.md Normal file
View File

@ -0,0 +1,130 @@
# Config 模块开发指南
`config/` 保存 CMVR-ES 的默认运行配置。配置格式是 Protobuf TextFormatSchema 位于 [`../../protos/cmvr/config/`](../../protos/cmvr/config/)。
返回[项目总览](../../README.md)。
## 配置树
```text
cmvr_es.pb.txt
├── logger/logger.pb.txt
├── manager/device_manager.pb.txt
│ └── devices/<category>/*.pb.txt
└── manager/task_manager.pb.txt
└── tasks/<task>/*.pb.txt
```
入口文件:
- [`cmvr_es.pb.txt`](cmvr_es.pb.txt)
- [`manager/device_manager.pb.txt`](manager/device_manager.pb.txt)
- [`manager/task_manager.pb.txt`](manager/task_manager.pb.txt)
## 路径规则
无参数运行时,程序读取:
```text
<cmvr_es 可执行文件所在目录>/config/cmvr_es.pb.txt
```
安装后的 `output/bin/cmvr_es` 因此会读取 `output/bin/config/cmvr_es.pb.txt`;直接运行 `build/cmvr_es` 则会查找 `build/config/cmvr_es.pb.txt`,不会自动跳到安装目录。传入显式根配置时:
```bash
./output/bin/cmvr_es /etc/cmvr-es/cmvr_es.pb.txt
```
设备、任务和证书等相对配置路径均以根配置文件所在目录解析。模型等资源通过 `ConfigHelper::resolveResourceFile()` 在配置根及父目录中查找;生产部署仍建议使用明确绝对路径。
日志配置中的相对 `directory` 以可执行文件目录解析,不以配置根解析。
## 新增设备配置
增加同类设备后端时:
1. 在 `protos/cmvr/config/<category>_config/` 增加后端 message
2. 在类别设备 message 的 `oneof backend` 中增加字段;
3. 在 `devices/<category>/``.pb.txt` 中增加实例;
4. 实例外层 `id` 必须唯一;
5. 在 [`manager/device_manager.pb.txt`](manager/device_manager.pb.txt) 增加相同 `id`、正确 `type` 和配置路径;
6. 开发默认保持 `enable: false`
7. 同步类别 factory 和 CMake
8. 在无硬件环境验证关闭状态,在真机环境单独开启。
设备集合中的 ID 与 DeviceManager 条目 ID 不一致时,工厂会拒绝创建。
## 新增任务配置
1. 在 `protos/cmvr/config/` 增加任务配置和 root message
2. 在 `tasks/<task_name>/` 增加默认 `.pb.txt`
3. 在 `task_manager.pb.txt` 增加唯一任务 ID
4. 配置正确的 `TaskType``TaskRunMode`
5. 周期任务设置大于零的 `control_period_s`
6. 服务任务使用 `TASK_RUN_MODE_BLOCKING_SERVICE`
7. 默认关闭依赖网络、证书或硬件的新任务。
任务实现流程见 [`../task/README.md`](../task/README.md)。
## 默认值与校验
- 不依赖 proto3 数值零值表达危险的生产默认值;
- timeout、队列大小、帧大小和周期应在代码中校验
- 新增 loader 对不认识的 enum 和未设置的 oneof 必须明确失败;当前个别历史路径仍有退化默认行为,不应复制;
- 设备端口、坐标系、速度和单位写入注释;
- `enable` 应由 manager 层控制,后端内部的 enable 字段不能替代 manager 开关;
- QUIC 需要 TaskManager 与 `QuicEdgeConfig.enable` 同时开启;
- QUIC 零媒体轨道是合法配置。
### gRPC 相机实时流
[`tasks/grpc_server_task/grpc_server_task.pb.txt`](tasks/grpc_server_task/grpc_server_task.pb.txt)
中的两个低延迟参数仅作用于 gRPC RGB 编码流不改变机械臂、AGV 等控制 RPC
- `camera_stream_max_pending_frames`:单个客户端允许的待发送帧数,超过后清空该
客户端积压;默认 2
- `camera_stream_max_frame_age_ms`:从设备回调进入边缘系统起计算的最大帧龄,
超过后不再发送;默认 250 ms。
两个字段填 0 或旧配置未包含字段时使用默认值。丢弃 H.264/H.265 帧后服务会请求
IDR 并等待关键帧恢复。如果现场采集、编码本身稳定超过 250 ms应根据日志中的
`age_ms` 调高帧龄阈值,而不是增大环形队列。
新二进制可以读取未包含这两个字段的旧配置;旧二进制不能解析包含新字段的
TextFormat。部署时必须同步更新程序与配置不能只把新版
`grpc_server_task.pb.txt` 复制给旧的 `output/bin/cmvr_es`
## 配置验证
构建后可以用 `protoc --encode` 对单个 TextFormat 文件做语法和字段验证。例如:
```bash
output/bin/protoc \
-I protos \
--encode=cmvr.config.QuicEdgeRootConfig \
protos/cmvr/config/quic_edge_config/quic_edge_config.proto \
< cmvr-es/config/tasks/quic_edge_task/quic_edge_task.pb.txt \
> /tmp/quic_edge_config.pb
```
该命令只验证 Proto Text 解析,不验证文件、设备、证书、网络和跨字段语义。最终仍需运行组件测试和进程烟雾测试。
## 生产配置
`cmake --install` 会重建 `output/bin/config/`。生产配置应复制到 `/etc/cmvr-es/` 等外部目录并显式传入。
- 不提交真实设备密码、token、私钥和生产地址
- 证书与私钥放在独立 `certs/`,使用最小读取权限;
- 为不同站点维护独立配置根,不在运行时修改仓库样例;
- 发布前检查所有 `enable`、IP、端口和设备 ID
- 变更配置 Schema 时同步 Proto 兼容性文档和平台生成代码。
## 提交检查
- [ ] TextFormat 可以被对应 root message 解析
- [ ] ID、类别和引用路径完全一致
- [ ] 新硬件和新网络任务默认关闭
- [ ] 参数单位、范围和安全默认值明确
- [ ] 没有生产凭据
- [ ] 安装覆盖不会丢失现场配置
- [ ] 无设备启动仍然成功

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKzCCApOgAwIBAgIUTtZCyKM8INYxZKkfwbKdpHKLjn4wDQYJKoZIhvcNAQEL
BQAwHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENBMB4XDTI2MDcyNDA2NDQy
MFoXDTM2MDcyMTA2NDQyMFowHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENB
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmq2rHldOobaemqNfWggS
OVj3inKy6AYjfgtcXUfKs48DDbpZ9gyEd/YPJXA8C2jGPXpxzgnc7a4UCUVQZ8ah
ddoJtFcC+Q6BgjeMVqUdUubu5Y9HpkfU3lvnp4KhzvOeFnkKtrCzYIPa2nK3zLc7
uCiuLlB+91KQSRXPFbc6N7H/EAfGmUHIwlZGysAkRN7b2TAoR4C7E96JLVtuUQsS
VtlEGpunSfuefFzeeZCMS6avLbB+a8Q6yUzLt6pqnheNsDB+jCCXodlJs5XS1AOB
W2GOpGFMj7dLoTD+eBIlAlrhWFcwKjzFmtp6LGl/Jy0O+E99X4TL72oNSOdTrO2O
iavXABz9IvWR2BrAyo5AKlTJqO6tmZw77iVti8jYi+HsXIVGQKMYwvv5k0jdHeIZ
FaDToUbFPP/zj0m8ZraMy+8eNAhScnx6Zs56fcncBuDti6pT+zKisjV1rH/sFvZY
wO1UvJlOEbvXrfYLPp58Aqe/toG2nV45a2Q4+x6AETV1AgMBAAGjYzBhMB0GA1Ud
DgQWBBRdHz+g2vVmqghDZi2kEHtZfoyHZzAfBgNVHSMEGDAWgBRdHz+g2vVmqghD
Zi2kEHtZfoyHZzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAYEASuEoMFiVcg4iTMxO2kshFTJ6LIrqqGXBn+1j+yQ1ennG
mqPMo5fBOe/Kp3YWCnREQWu0+EEPEC9qWgIDOIm3v7ch4mZiW31GUOqae6bjprBe
er7ySElKGZ5GefKAq++we19A6WHnNxtNAT9BE1VSKUmkxEsnIkuwd+QMmQ9eaIRm
8RvzshWdUcyiBJg07sI3rPzPD/YpnfcYlAa2a0+oXJ3o3zlhdbs2S+9fIl+Y/zFN
wBaUV6ZNk0RFzOCA+jUq2jU5Y1pODcot3Mp2jCHzp4uTFyVqsoqc5USRB2TUILCP
acSbEjD9GnKaNU4miTcftnC85fnBAiZ5btZkXIs72ij+J3VW7QnbcFQG8bElngoR
mWizh6ByPQrsvyT0evJjdjpuOzILsVTAn64m0cYpnlR4g5orcEkIJHv4DzMhpXc2
CvpcWwk2vJrUuws/LGxDATTAcqtIUIxAkxffqUsdB/6j8fdykNCvXFwNh/uPcawd
4kzh6dr+RFzHSMQ6oN16
-----END CERTIFICATE-----

View File

@ -0,0 +1,40 @@
-----BEGIN PRIVATE KEY-----
MIIG/wIBADANBgkqhkiG9w0BAQEFAASCBukwggblAgEAAoIBgQCaraseV06htp6a
o19aCBI5WPeKcrLoBiN+C1xdR8qzjwMNuln2DIR39g8lcDwLaMY9enHOCdztrhQJ
RVBnxqF12gm0VwL5DoGCN4xWpR1S5u7lj0emR9TeW+engqHO854WeQq2sLNgg9ra
crfMtzu4KK4uUH73UpBJFc8Vtzo3sf8QB8aZQcjCVkbKwCRE3tvZMChHgLsT3okt
W25RCxJW2UQam6dJ+558XN55kIxLpq8tsH5rxDrJTMu3qmqeF42wMH6MIJeh2Umz
ldLUA4FbYY6kYUyPt0uhMP54EiUCWuFYVzAqPMWa2nosaX8nLQ74T31fhMvvag1I
51Os7Y6Jq9cAHP0i9ZHYGsDKjkAqVMmo7q2ZnDvuJW2LyNiL4exchUZAoxjC+/mT
SN0d4hkVoNOhRsU8//OPSbxmtozL7x40CFJyfHpmznp9ydwG4O2LqlP7MqKyNXWs
f+wW9ljA7VS8mU4Ru9et9gs+nnwCp7+2gbadXjlrZDj7HoARNXUCAwEAAQKCAYAi
e49/pNqWghoTItM9xLldWAhdcMsSH1Y3wgweDoRxqbL2U0I9cFZy0OPZBo+YQowZ
RgwLcRbz1MBKPc3aSMWTep95uQEkaVe1YjFS2p3yLqH5AstoFjDuPmJjLWPpuVVX
sLXS+wsOO+7lDriLdpjlagpEsHTRqbIZXPeM4YtkwbV5SyZ64ZfCPU4sYo/jW6R6
43nDUP9Dw3Nk7XJnNlbpDigY33T4sRPIqUJ+qtsf/WGlx6gzWax6Vni+8gqxQlIe
80XG6dHCM3UGFEdGF64PB2FuprsPWBSSau7L7I/gagiIuQbhs9VYXOanz1MQizMU
ZdK8cqmG/bpP+uY4VQLJ5Lxu87boEllGFHBOYTCjWq0zatzGMt1/DeggLnjgfC90
5kOzlxl403j+4K8nqiHoKpAbOatwTxFz1up6XSecsarE1RKsCq8sIH+FjjCfO1Db
hBfv2a7/XemdcxHtl9Q/EOSDaAFOUQfFMDAc3rCH7/vonTzaaZq5ucWmjlBsv2EC
gcEAz+cH+3x09pXVOnM7zlWrGAQk69Fj/0BGs7g3GFf8ZvI/kkbez+9J1QGItxj+
7Dm4Ay0kgxYi3DxFWbPLHTbFpVg2CyL08IQZSXJzokwz2V/HTd1a+OgzgB/dBNDc
V+XnxJI89+SI3VhOQZJG625l//lhHWCy2u0ULE7ZlK3ao95L86bSXQHI4dZ2nWXl
0f4ISJmm4qkEJAY+DkQeF3UZLumd1NRxy0LBdbYVM/wZIN+sGsRdQqQ9f+ik6+aX
YrjhAoHBAL52dJzIPIN1Z/Y9qxUrVLPE7K8Mm8KLaFIl/ztl3TFqaUvErHi66nx3
MTwEnrQIBI31OKxB6Or2JLxYMvDTSVRV1K2tY53+K6Q8giWU4x3zFc/svyl35gkd
U1bLdfxXqKMLrvlmVWj6/RMoH7vd/aTcidYVvwOZgtL+6RahS4LfZ9qXAyREFAJa
VSI0HW5LLiOBLTegztT1CBVe7xb78bWFMiUkxre4XsLZYwFPzXzVPh6HloDcACDL
FPwLrfJrFQKBwQCvGDl90UzEnF4v0vsshLQLDvp1bS1VvTGOjPhB1WBq510o+e0P
nM1Gyvr0keWo19elPTDCAjOr3kreCHFpEkcVQRyK9o7pvad6Vx0SNDF6wpKdfm7u
sMknAC7prmnU0XkH8c3NTTkDiiqmSObXw2u+UK48ysL3ZLIXuvS+pkk8t6yp8Pa8
hBNGOJQ/baFH4TXixx1pScWF/YfoBfB9+w4Rl4loxN9tu7QpSgfDd29GY3qUNIsC
5EYzYqD7WIJpD6ECgcEAkdIfdenYas14yw5r7ck/EGO00lDU8B3LwRlWUCOtNihC
dcAeTFDPNnwLNehTmYKJ+iXFPh04Nqw9c/YTCk651dfg/RfDLTNsNlIdUqirOkLi
cE7SDO2/MTtCkzEzI//5HNvVGx0+RyHioMgXg75yc8ZlwYLku9zMTL7dtnXHWmux
F6qGvT1iFGsUwxsjbU4iBQzhkbWMpX70sWf9pZs/c7qGqel+OyrtYkENi/ONYAXj
iXxFvmKxtmnFpzNJ+lABAoHBAJjs8jfEXJnJP80t93Bxg6NmfKhxUEm4ovcIeR8U
lEfb8Ind1ICrBWGVGOAkirTFZgFAAlEjne+lT1FYgf19oC+XbxFdl/e02M8KfflS
01+0/t6lEpefiU0dfW0dWtXOPaAVvpNgMkeMgBzB2Yz7ZNUrMypfCGk289qveWxB
8UbO+QLjTI+cFvPsmR95Eti/nAjd2fXGeqbR+z+DdQnz4BvXLSCQCw4on2+T+mpw
BprrEQmcdeZA8LE1zgcKtkZR1w==
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
2C945D70B02014891B6E09D57E377CEFB6D18498

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDuzCCAiOgAwIBAgIULJRdcLAgFIkbbgnVfjd877bRhJgwDQYJKoZIhvcNAQEL
BQAwHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENBMB4XDTI2MDcyNDA2NDQz
NloXDTI4MTAyNjA2NDQzNlowGDEWMBQGA1UEAwwNMTkyLjE2OC4wLjIyMjCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdM3i1FYFKqNWJzhfhsD9nRUAuK
pzilz5uqCKAt8lKYYC9WnLHOYdiEjcHGnGr02yd6sWFH/LBbxNhzx8M7h4S4izuO
bhSlG1EIhkMiojzVD1e3P7YzXdEoVxTCfmMgBZQJG63GNOfzRawFYtEeGv7ndFVw
kitCYlyTza5KlBNlWpiNOPmmx4dLTdGLUk8a5TUm0zJ+b/LyzVsWUtr9sxKmKeG8
0/77AeiL0hQE3xUt5QROTZjRTVhNHowv410dFMJyIfY4sab8ndc4SIwE9PCKg068
4705vbFInBS3eTvQur5VLSZLPatnXGKzCXjci1lIQX2p/QIqADMGFWIN9OECAwEA
AaN4MHYwDwYDVR0RBAgwBocEwKgA3jAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww
CgYIKwYBBQUHAwEwHQYDVR0OBBYEFLmoEtsglxm3mXh4l8OF31bt2ZhnMB8GA1Ud
IwQYMBaAFF0fP6Da9WaqCENmLaQQe1l+jIdnMA0GCSqGSIb3DQEBCwUAA4IBgQAR
eK1mD9rJkzHe4OusimQfcuDQW+0J32e4T/34RHlW+lIj7botFaElXIzO9S80tDwq
4d4ozNPKysqgJN9hv/BBMzJpZLwP2XozPaGLTNl1jRTCc9UhFUPrUeu0LbpQGBfC
6Ghq42V94zPAw4lnMujnkq8botk21hclbJORQ9kblXP31IdWCgiKSFLy1NTBmQmc
IxmR+SldMWYrWGWv/0I85AeMu6HR3+NKHmzDblm1HUHFekyC1f7sypNG+D1r8ab2
GSijoCMKHSEOm81Vl/j5bgWQygnnIOhsLOUf2DZO6jC+VZmTKpMEJNHgwk23WBlu
FYz/X9p5z9ZucL8aBxegj7G1fI5Ik2O05+LLeqJMfspAm6ZcnRDxCVZWQ+K/dAle
fz+gTASzhHTsjEBeiX46LP0L2PVyBiPvNtD66e52LoM5ZigechZq91niLO+ipWdh
xq7ryJzmaAEQKPuYaoswriWzJam07ywX9yupGHzBA6vVgGy0jPAsnGCuZXTfDyA=
-----END CERTIFICATE-----

View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICpDCCAYwCAQAwGDEWMBQGA1UEAwwNMTkyLjE2OC4wLjIyMjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAKdM3i1FYFKqNWJzhfhsD9nRUAuKpzilz5uq
CKAt8lKYYC9WnLHOYdiEjcHGnGr02yd6sWFH/LBbxNhzx8M7h4S4izuObhSlG1EI
hkMiojzVD1e3P7YzXdEoVxTCfmMgBZQJG63GNOfzRawFYtEeGv7ndFVwkitCYlyT
za5KlBNlWpiNOPmmx4dLTdGLUk8a5TUm0zJ+b/LyzVsWUtr9sxKmKeG80/77AeiL
0hQE3xUt5QROTZjRTVhNHowv410dFMJyIfY4sab8ndc4SIwE9PCKg0684705vbFI
nBS3eTvQur5VLSZLPatnXGKzCXjci1lIQX2p/QIqADMGFWIN9OECAwEAAaBHMEUG
CSqGSIb3DQEJDjE4MDYwDwYDVR0RBAgwBocEwKgA3jAOBgNVHQ8BAf8EBAMCBaAw
EwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAGIcLeCE344z
PENI1/oONVHBzMMt5VN0P8jbkJOFgZ3a6AUhfqAmDNBr+8SBym+cX2Y9Q2BsaWAu
TKOBN+fs+fh5/NqF0hTNvmXzp89NFK5SlsTjoC21HJvK1HTNuNW8drOxNfWgFW3/
gSCutcsWS9hVtYrV2FHQzOvVXvXKfmuTkZE8g92P3BCkvRm+ORxI0QfVS81/Ibnv
Yl+t/o2QCphdP+1OWzU6+Pccqi2xCyIc6jqCmh0b01zg265PTTOfICO6WQNtYcOn
308avDDpTneGl5vWRVdmmiXPqkc+dCesoguIEDUlerFpDKKY81P2GTpfjki93K7Z
2DJPs4P1cU0=
-----END CERTIFICATE REQUEST-----

View File

@ -1,9 +0,0 @@
agv {
agvs {
id: "agv_1"
my_agv {
ip: "127.0.0.1"
port: 8080
}
}
}

View File

@ -0,0 +1,45 @@
agv {
agvs {
id: "agv_1"
my_agv {
ip: "127.0.0.1"
port: 8080
}
}
agvs {
id: "src1100"
src1100_agv {
ip: "192.168.192.5"
port_status: 19204
port_control: 19205
port_nav: 19206
port_config: 19207
port_other: 19210
port_push: 19301
recv_timeout_ms: 1000
enable_state_push: true
state_push_interval_ms: 200
state_push_included_fields: "x"
state_push_included_fields: "y"
state_push_included_fields: "angle"
state_push_included_fields: "vx"
state_push_included_fields: "vy"
state_push_included_fields: "w"
state_push_included_fields: "battery_level"
state_push_included_fields: "battery_temp"
state_push_included_fields: "charging"
state_push_included_fields: "voltage"
state_push_included_fields: "current"
state_push_included_fields: "current_map"
state_push_included_fields: "current_station"
state_push_included_fields: "confidence"
state_push_included_fields: "emergency"
state_push_included_fields: "fatals"
state_push_included_fields: "errors"
enable_map_update: true
map_update_interval_ms: 1000
map_update_history_size: 8
}
}
}

View File

@ -4,7 +4,7 @@ arm {
vendor {
brand: VENDOR_ROBOT_ARM_BRAND_AUBO_ARM
ip: "192.168.1.100"
ip: "192.168.192.18"
port: 30004
dof: 6
joint_names: "joint_1"

View File

@ -146,4 +146,43 @@ camera {
sync: false
}
}
cameras {
id: "hikvision_cam"
hikvision {
ip: "192.168.192.64"
port: 8000
username: "admin"
password: "okwy1688"
channel: 1
stream_type: 0
link_mode: 0
width: 1920
height: 1080
fps: 25
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
buffer_size: 30
}
}
cameras {
id: "hikvision_thermal_cam"
hikvision {
ip: "192.168.192.65"
port: 8000
username: "admin"
password: "okwy1688"
channel: 1
stream_type: 0
link_mode: 0
width: 384
height: 288
fps: 50
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
buffer_size: 30
}
}
}

View File

@ -3,7 +3,7 @@ microphone {
id: "mic1"
ffmpeg {
channels: 2
sampleRate: 44100
sampleRate: 48000
volume: 100
input_device: "default"
}

View File

@ -100,7 +100,7 @@ device_manager {
id: "huayan_arm"
type: DEVICE_TYPE_ROBOT_ARM
config_file: "devices/arm/huayan_arm.pb.txt"
enable: true
enable: false
}
devices {
@ -111,9 +111,40 @@ device_manager {
}
devices {
id: "agv_1"
id: "src1100"
type: DEVICE_TYPE_AGV
config_file: "devices/agv/agv.pb.txt"
config_file: "devices/agv/src1100.pb.txt"
enable: false
}
devices {
id: "hikvision_cam"
type: DEVICE_TYPE_CAMERA
config_file: "devices/camera/camera.pb.txt"
# Host-development default: keep physical cameras disabled.
enable: false
}
devices {
id: "hikvision_thermal_cam"
type: DEVICE_TYPE_CAMERA
config_file: "devices/camera/camera.pb.txt"
# Host-development default: keep physical cameras disabled.
enable: false
}
devices {
id: "mic1"
type: DEVICE_TYPE_MICROPHONE
config_file: "devices/microphone/microphone.pb.txt"
# Host-development default: keep physical audio devices disabled.
enable: false
}
devices {
id: "spk1"
type: DEVICE_TYPE_SPEAKER
config_file: "devices/speaker/speaker.pb.txt"
# Host-development default: keep physical audio devices disabled.
enable: false
}
}

View File

@ -14,4 +14,20 @@ task_manager {
config_file: "tasks/grpc_server_task/grpc_server_task.pb.txt"
enable: true
}
tasks {
id: "right_arm_self_collision"
type: TASK_TYPE_SELF_COLLISION
run_mode: TASK_RUN_MODE_PERIODIC_STEP
control_period_s: 0.002
config_file: "tasks/self_collision_task/self_collision_task.pb.txt"
enable: false
}
tasks {
id: "quic_edge"
type: TASK_TYPE_QUIC_EDGE
run_mode: TASK_RUN_MODE_BLOCKING_SERVICE
config_file: "tasks/quic_edge_task/quic_edge_task.pb.txt"
# Host-development default: no QUIC Gateway or physical media devices.
enable: true
}
}

View File

@ -3,4 +3,6 @@ grpc_server {
host: "0.0.0.0"
port: "50052"
enable_reflection: true
camera_stream_max_pending_frames: 2
camera_stream_max_frame_age_ms: 250
}

View File

@ -0,0 +1,65 @@
quic_edge {
id: "quic_edge"
# Task enablement is controlled by manager/task_manager.pb.txt. Configure a
# reachable QUIC Gateway and TLS policy before enabling the task there.
server_host: "quic-gateway.example.com"
server_port: 4433
alpn: "cmvr-quic-edge/1"
node_id: "cmvr-edge"
software_version: "0.1"
# The existing cmvr-es gRPC server remains the robot-control endpoint. "auto"
# selects a usable address from the interface snapshot sent at registration
# and on every heartbeat.
grpc_endpoint_host: "auto"
grpc_endpoint_port: 50052
grpc_endpoint_tls: false
include_loopback_interfaces: false
# Local heartbeat period. The Gateway keeps this value when its registration
# response returns heartbeat_interval_ms=0; a non-zero response overrides it.
heartbeat_interval_ms: 5000
control_response_timeout_ms: 1000
tls {
ca_file: "certs/quic_gateway_ca.pem"
certificate_file: "certs/cmvr_edge_cert.pem"
private_key_file: "certs/cmvr_edge_key.pem"
server_name: "quic-gateway.example.com"
allow_insecure: false
}
reconnect {
initial_delay_ms: 500
maximum_delay_ms: 30000
multiplier: 2.0
jitter_percent: 20
connect_timeout_ms: 5000
}
maximum_datagram_bytes: 1200
maximum_control_frame_bytes: 1048576
# With 1200-byte DATAGRAMs and a 512-entry queue, 524288 stays below
# the atomic batch capacity while reserving slots for control messages.
maximum_frame_bytes: 524288
datagram_send_queue_depth: 512
media_poll_interval_ms: 2
# Zero media tracks is valid and keeps registration, IP reporting and
# heartbeat active. Add tracks only for devices enabled in DeviceManager.
# tracks {
# track_id: 1
# source_kind: SOURCE_KIND_CAMERA
# device_id: "right_hand_cam"
# source_track_id: "right_hand_cam/video/color"
# enable: true
# }
# tracks {
# track_id: 2
# source_kind: SOURCE_KIND_MICROPHONE
# device_id: "mic1"
# source_track_id: "mic1/audio/main"
# enable: true
# }
}

View File

@ -0,0 +1,24 @@
self_collision_task {
id: "right_arm_self_collision"
arm_id: "mujoco_right_arm"
checker {
urdf_path: "model/xiaoyan_description/dual_arm_collision.urdf"
# Simplified compact-wrist bodies overlap in the normal assembled pose.
ignored_pairs {
first: "R_WRIST_P_S"
second: "R_WRIST_R_S"
}
}
sampling {
max_geometry_displacement_m: 0.002
max_check_period_s: 0.01
}
safety {
warning_distance_m: 0.02
stop_distance_m: 0.005
}
}

369
cmvr-es/devices/README.md Normal file
View File

@ -0,0 +1,369 @@
# Devices 模块开发指南
`devices/` 屏蔽厂商 SDK、通信总线和硬件型号差异对 manager、service、task 和算法层提供稳定的设备能力接口。
仓库真实目录名是 `cmvr-es/devices/`。设备层使用复数 `devices`,不是 `cmvr_es/device`
返回[项目总览](../../README.md)。
## 创建链路
```text
config/cmvr_es.pb.txt
-> config/manager/device_manager.pb.txt
-> DeviceManager
-> DeviceFactory
按 DeviceConfigEntry::DeviceType 选择设备大类
-> CameraFactory / AGVFactory / RobotArmFactory / ...
按设备配置 oneof backend 选择厂商实现
-> 具体设备类
-> AbstractDevice 和设备大类抽象接口
```
关键文件:
- [`abstract_device.h`](abstract_device.h)
- [`device_types.h`](device_types.h)
- [`../manager/device_manager/include/device_manager.h`](../manager/device_manager/include/device_manager.h)
- [`../manager/device_manager/src/device_factory.cpp`](../manager/device_manager/src/device_factory.cpp)
- [`../../protos/cmvr/config/device_manager_config/device_manager_config.proto`](../../protos/cmvr/config/device_manager_config/device_manager_config.proto)
- [`../config/manager/device_manager.pb.txt`](../config/manager/device_manager.pb.txt)
协议层和业务任务不应直接依赖厂商 SDK 类型。厂商错误码、报文和连接细节由具体后端转换为抽象接口的通用语义。
## 当前可由配置创建的设备
| 大类 | 抽象接口 | 类别工厂 | 当前可选后端 |
| --- | --- | --- | --- |
| Camera | [`camera/abstract_camera.h`](camera/abstract_camera.h) | [`camera/camera_factory.h`](camera/camera_factory.h) | UVC、RealSense、Hikvision |
| AGV | [`agv/abstract_agv.h`](agv/abstract_agv.h) | [`agv/agv_factory.h`](agv/agv_factory.h) | MyAgv、SRC1100 |
| RobotArm | [`arm/robot_arm.h`](arm/robot_arm.h) | [`arm/robot_arm_factory.h`](arm/robot_arm_factory.h) | MotorRobotArm、AUBO、Huayan |
| DexHand | [`dexhand/abstract_dexhand.h`](dexhand/abstract_dexhand.h) | [`dexhand/dexhand_factory.h`](dexhand/dexhand_factory.h) | RH56DFTP、PX6AXGen3 |
| Microphone | [`microphone/abstract_microphone.h`](microphone/abstract_microphone.h) | [`microphone/microphone_factory.h`](microphone/microphone_factory.h) | FFmpeg |
| Speaker | [`speaker/abstract_speaker.h`](speaker/abstract_speaker.h) | [`speaker/speaker_factory.h`](speaker/speaker_factory.h) | FFmpeg |
| BioHead | [`biohead/abstract_biohead.h`](biohead/abstract_biohead.h) | DeviceFactory 直接创建 | BioHeadRobot |
| MotorSystem | `motor/motor_system/` | DeviceFactory 直接创建 | CAN/MuJoCo motor group |
代码目录存在不等于已经接入配置创建链:
- MechMind Proto 和实现仍存在,但当前 CameraFactory 明确拒绝创建;
- MujocoCamera 有实现并参与部分构建,但当前没有 CameraFactory 分支;
- Battery、Gripper、Robot、CanBus 等抽象或实现不一定已注册到 DeviceFactory
- 所有已注册设备共用一个全局 ID 命名空间。
新增能力前先确认“已有代码”“可被 CMake 构建”“可被 Factory 创建”“可被 DeviceManager 配置启用”四个状态,不要混为一谈。
## 新增同类厂商后端
以新增 Camera 后端为例。
### 1. 扩展配置 Proto
修改:
```text
protos/cmvr/config/camera_config/camera_config.proto
```
新增厂商 config并加入 `CameraDeviceConfig.oneof backend`。只使用新的字段 tag不复用 reserved 或已发布 tag。
### 2. 新建后端目录
```text
camera/vendor_camera/
├── CMakeLists.txt
├── include/
│ └── vendor_camera.h
├── src/
│ └── vendor_camera.cpp
└── tests/
└── vendor_camera_test.cpp
```
### 3. 实现抽象接口
至少实现:
- `typeName()`
- `init()`
- 实际支持的 `start()` / `stop()`
- 状态查询和该类别核心能力;
- 若能提供运行时健康信息,实现无阻塞的 `healthSnapshot()`
- 若支持实时媒体,完整实现流接口和并发停止。
不要为了厂商特例向抽象类加入 SDK handle、私有报文或厂商专有结构。只有多个后端都需要的稳定语义才进入抽象接口或 `common/types/`
### 4. 类别工厂注册
在 [`camera/camera_factory.h`](camera/camera_factory.h) 的 `backend_case()` 增加创建分支。
AGV、DexHand、Microphone、Speaker 等遵循相同模式。新增同类后端通常不需要修改全局 DeviceFactory。
### 5. CMake 聚合
1. 在 `camera/CMakeLists.txt` 增加 `add_subdirectory(vendor_camera)`
2. 让类别 target 链接新后端 target
3. 安装需要随应用分发的共享库;
4. 厂商 SDK 路径使用 `dependency/${ARCH}/third_party/...`,不能硬编码 x86
5. 需要特殊 RPATH 时参考 Hikvision、Huayan 等现有实现。
推荐 target 形式:
```cmake
add_library(vendor_camera SHARED
src/vendor_camera.cpp
)
target_include_directories(vendor_camera
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(vendor_camera
PUBLIC cmvr_es::proto
PRIVATE vendor_sdk
)
add_library(cmvr_es::device::vendor_camera ALIAS vendor_camera)
install(TARGETS vendor_camera LIBRARY DESTINATION lib)
```
### 6. 默认配置
在类别集合配置增加实例,并在 DeviceManager 配置增加同 ID 条目:
```protobuf
devices {
id: "camera_front"
type: DEVICE_TYPE_CAMERA
config_file: "devices/camera/camera.pb.txt"
enable: false
}
```
新硬件默认 `enable: false`,确保无设备开发主机仍可启动。
## 新增全新设备大类
当现有抽象无法表达新设备能力时,还需要:
1. 在 [`device_types.h`](device_types.h) 增加 `DeviceKind``toString()`
2. 新增类别抽象接口;
3. 将稳定公共数据放入 `../common/types/<category>/`
4. 新增配置 Proto
5. 在 `DeviceConfigEntry::DeviceType` 使用新的 enum 数值;
6. 在 `DeviceFactory::DeviceFactory()` 注册 creator
7. 更新 DeviceManager 的 type-to-string 日志映射;
8. 为 `getDevice<AbstractNewDevice>()` 增加显式模板实例化;
9. 更新 `devices/CMakeLists.txt`、类别 CMake 和 device_manager target 链接;
10. 若平台需要访问,独立增加 API Proto 和 gRPC service。
不得复用 `device_manager_config.proto` 中已 reserved 的 enum 数值或名称。新增设备类别不会自动生成平台 RPC。
## 配置和 ID
集合类型设备必须满足:
```text
DeviceConfigEntry.id
==
CameraDeviceConfig / AGVDeviceConfig / ... 的外层 id
```
厂商 backend 内部 ID 可以为空,由类别工厂补齐;若填写,也必须与外层 ID 相同。
其他约束:
- 相对配置路径以根 `cmvr_es.pb.txt` 所在目录解析;
- 生产密码、token 和证书不得提交到样例配置;
- 单个设备 init 失败时不会进入可用对象表,进程仍可能继续启动;失败条目会保留
在 DeviceManager 状态快照中,其中已启用的失败设备会通过 QUIC heartbeat
上报,禁用设备不会上报;
- 有初始化依赖的设备按配置顺序排列,例如 MotorSystem 在依赖它的 RobotArm 前;
- DeviceManager stop 遍历 unordered_map不能依赖跨设备停止顺序
- DeviceManager 支持并发查询、状态快照和动态注册,但动态设备不会自动补执行
已经发生的 `start()`,当前也没有设备移除或完整热插拔生命周期。
配置细节见 [`../config/README.md`](../config/README.md)。
## 生命周期约定
| 接口 | 当前语义 |
| --- | --- |
| 构造函数 | 保存配置和轻量校验,不启动长期工作线程 |
| `init()` | DeviceManager 构造期间对启用设备调用 |
| `start()` | 当前 main 不统一调用,由 RPC、媒体或其他 owner 显式触发 |
| `startStreaming()` | 获取一个实时流生产租约 |
| `stopStreaming()` | 释放租约,最后一份租约停止生产者 |
| `stop()` | 完整停止设备,必须幂等 |
| `update()` | 当前没有统一 scheduler 自动调用 |
| 析构函数 | 回收线程、SDK callback、socket、fd 和 handle |
实现要求:
- init/start/stop 的重复调用有明确结果;
- stop 和最后一次 stopStreaming 返回前停止所有发布;
- 不持有 worker 退出所需的锁执行 join
- SDK callback 不获取停止路径长期持有的控制锁;
- getState 使用与状态写入相同的锁;
- 含 `std::string`、vector 等状态不能无锁复制;
- `healthSnapshot()` 只能读取已经缓存的内存状态,必须线程安全,不能同步访问
SDK、网络、串口或设备总线
- 无法提供可信健康状态时返回 `UNKNOWN`,不能用“没有观察到错误”冒充健康;
- 析构函数调用安全停止路径;
- callback 捕获对象前保证 owner 生命周期。
并发停止可参考:
- [`camera/uvc_camera/`](camera/uvc_camera/)
- [`camera/hikvision_camera/`](camera/hikvision_camera/)
- [`camera/hikvision_camera/tests/`](camera/hikvision_camera/tests/)
当前 main 收到退出信号只停止 TaskManager没有显式调用 DeviceManager stop设备析构仍必须可靠。
## 摄像头与麦克风实时流
设备实现抽象流接口后,由 [`../manager/media_source_hub/`](../manager/media_source_hub/) 适配给 gRPC 和 QUIC不应在设备后端实现两套协议代码。
当前 Hub 轨道:
```text
<camera-id>/video/color
<microphone-id>/audio/main
```
当前 adapter 只把 `StreamFrameData.rgbFrame` 注册为彩色视频轨道depthFrame 尚未注册为 Hub 深度轨道。
### Camera 完整编码帧
每个 access unit 应正确填写:
- `rgbFrame`
- `codec`H.264 或 H.265
- `width`、`height`、`fps`
- `bKey`
- `stream_epoch`
- `sequence`
- `capture_monotonic_ns`
- `capture_utc_ns`
- `source_timestamp`:设备 SDK 提供的原始时间戳;单位和时钟域由设备定义,未知
时保持 0不能直接当成 Unix 时间;
- `source_frame_number`:设备 SDK 提供的原始帧号,未知时保持 0
- `pts`、`dts`
- `time_base_num`、`time_base_den`
- `duration`
- `discontinuity`
- `codec_config_generation`
- `codec_config`
H.264/H.265 后端必须识别关键帧,并尽量实现 `requestKeyFrame()`
Hikvision 后端优先采用 SDK 回调中的有效帧率,并保留 SDK 的 64 位原始时间戳和帧号;
SDK 帧率无效时才回退到配置的 `fps`。这些字段用于跨层诊断,设备层不应在不了解
SDK 时钟语义时擅自换算。
### Microphone 完整音频包
应正确填写:
- `data`
- `sample_rate`
- `channels`
- `format``codec`
- `nb_samples`
- sequence、时间戳、time base、duration
- stream epoch、discontinuity 和 codec generation
### 生产者重启
采集/编码生产者真正停止并重新启动时:
1. 增加 `stream_epoch`
2. 将 source sequence 重置为 0
3. 增加 `codec_config_generation`
4. 确保后续帧携带完整的新编码元数据;
5. 将第一帧标记为 discontinuity
6. 视频从关键帧恢复输出。
设备后端不直接发布 `TrackDescriptor`。`device_media_source_adapter.cpp` 会根据帧元数据生成或更新 descriptor。
### 运行期编码配置变化
编码器没有重启、只在同一 stream epoch 内改变分辨率、codec config 等参数时:
1. 保持 `stream_epoch` 不变;
2. 保持 sequence 连续递增;
3. 增加 `codec_config_generation`,或让其他描述字段反映变化;
4. 在后续帧中携带新元数据;
5. 标记 discontinuity
6. H.264/H.265 从新的关键帧恢复。
adapter 检测到描述变化后创建新 descriptor设备后端不要自行维护协议侧 descriptor 状态。
## 设备侧环形队列安全
现有设备后端多使用 `SPMCRingBuffer<T>`
- 只允许一个逻辑生产者;
- 每个消费者独立持有读游标;
- 同一读游标不能跨线程并发访问;
- 不对同一路读取混用无参 `pop()` 和带游标 `pop(index)`
- 需要最新帧时使用 `getLatest(index)`
- 不要先取 head 再分两步读取,避免检查/读取竞态;
- 满队列覆盖旧数据是实时媒体的预期行为;
- `waitEncodedFrame()` 必须有有限 timeout不能永久阻塞。
MediaSourceHub Subscription 同样是单消费者对象,不同协议或客户端必须各自订阅。
发布后的 `MediaFrame`、`TrackDescriptor` 和 payload 不可再修改。
## 测试要求
至少覆盖:
- 配置缺失、非法参数和 ID 不一致;
- Factory 选择正确后端;
- init/start/stop 重复执行;
- init 失败后无残留线程和句柄;
- stop 与 SDK callback 并发;
- 最后一个流租约释放后不再发布;
- 多消费者使用独立游标;
- 环形队列覆盖和丢帧;
- epoch、sequence、关键帧和 codec generation
- 设备断开、超时和重连。
无硬件参考测试:
- [`camera/hikvision_camera/tests/hikvision_camera_callback_test.cpp`](camera/hikvision_camera/tests/hikvision_camera_callback_test.cpp)
- [`../manager/media_source_hub/tests/media_source_hub_test.cpp`](../manager/media_source_hub/tests/media_source_hub_test.cpp)
```bash
cmake -S . -B build \
-DCMVR_ARCH=x86 \
-DBUILD_TESTING=ON \
-DCMVR_MEDIA_SOURCE_HUB_BUILD_TESTS=ON
cmake --build build -j"$(nproc)"
ctest \
--test-dir build \
-R 'hikvision_camera_callback_test|media_source_hub_test' \
--output-on-failure
```
新测试必须在 `BUILD_TESTING` 下使用 `add_test()` 登记。只创建 executable 不会自动被 CTest 执行。
## 提交检查
- [ ] 抽象接口没有厂商 SDK 类型
- [ ] manager、外层配置和 backend ID 一致
- [ ] 新硬件配置默认关闭
- [ ] 生命周期支持重复调用
- [ ] stop 是同步发布屏障
- [ ] 状态读写使用同一把锁
- [ ] 每个实时流只有一个生产者
- [ ] 每个消费者使用独立游标
- [ ] 时间戳、sequence 和编码元数据完整
- [ ] Factory 和类别 CMake 均已接入
- [ ] 厂商运行库有安装规则
- [ ] 有无真实硬件的自动测试
- [ ] 平台能力变化已评估 Proto 和 service

View File

@ -34,6 +34,23 @@ namespace cmvr::device {
virtual bool start() { return true; }
virtual bool stop() { return true; }
virtual bool update() { return true; }
virtual bool executeJsonCommand(const std::string& request_json, std::string& response_json) {
response_json = R"({"success":false,"error_message":"JSON command unsupported"})";
return false;
}
// This hook is sampled by DeviceManager while building heartbeats. It
// must be thread-safe and complete in bounded time while only copying
// in-memory state through atomics or a dedicated short-held state
// lock. Implementations must not perform device I/O, network requests,
// or wait on a lifecycle lock held across such I/O.
//
// The method is intentionally non-const because several legacy device
// categories expose non-const state getters. The returned object is a
// value and does not expose the device lifetime to callers.
virtual DeviceHealthSnapshot healthSnapshot() {
return {};
}
protected:
std::string id_; // 设备名称

View File

@ -1,4 +1,5 @@
add_subdirectory(my_agv)
add_subdirectory(src1100)
add_library(agv INTERFACE)
@ -7,6 +8,7 @@ target_include_directories(agv INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(agv
INTERFACE
cmvr_es::device::my_agv
cmvr_es::device::src1100_agv
cmvr_es::proto
)

View File

@ -6,32 +6,235 @@
#define CMVR_ES_ABSTRACT_AGV_H
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "common/types/agv/agv_types.h"
#include "devices/abstract_device.h"
namespace cmvr::device {
/**
* @brief AGV/
*
* AGV /
* API AgvAdapterParams
*/
class AbstractAGV : public AbstractDevice {
public:
AbstractAGV() = default;
~AbstractAGV() override = default;
DeviceKind kind() const noexcept override { return DeviceKind::AGV; }
virtual bool getState(AGVState &state) { return true; }
// navigation
virtual bool eStop() { return true; }
virtual bool goHome() { return true; }
virtual bool moveto(math::Pose2d &location, double speed_ratio) { return true; }
virtual bool setVelocity(math::Vec3 linear, math::Vec3 angular) { return true; }
/**
* @brief AGV
*/
virtual AgvRuntimeState runtimeState() const { return {}; }
// map
virtual bool initMap(float resolution, int width, int height) { return true; }
virtual bool updateMap() { return true; }
virtual bool saveMap(const std::string& file_path) { return true; }
virtual bool loadMap(const std::string& file_path) { return true; }
/**
* @brief
*/
virtual AgvNavigationStatus navigationStatus() const { return {}; }
protected:
AGVState state_;
};
/**
* @brief AGV
*/
virtual AgvResult emergencyStop()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "emergencyStop not implemented");
}
/**
* @brief AGV
*/
virtual AgvResult clearFault()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "clearFault not implemented");
}
/**
* @brief /姿
*/
virtual AgvResult navigateToPose(
const math::Pose2d& pose,
const AgvMotionOptions& options = {},
const AgvAdapterParams& adapter_params = AgvAdapterParams{})
{
(void)pose;
(void)options;
(void)adapter_params;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "navigateToPose not implemented");
}
/**
* @brief
*/
virtual AgvResult navigateToStation(
const std::string& station_id,
const AgvMotionOptions& options = {},
const AgvAdapterParams& adapter_params = AgvAdapterParams{})
{
(void)station_id;
(void)options;
(void)adapter_params;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "navigateToStation not implemented");
}
/**
* @brief
*/
virtual AgvResult followPath(const std::vector<AgvPathSegment>& path)
{
(void)path;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "followPath not implemented");
}
/**
* @brief
*/
virtual AgvResult pauseNavigation()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "pauseNavigation not implemented");
}
/**
* @brief
*/
virtual AgvResult resumeNavigation()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "resumeNavigation not implemented");
}
/**
* @brief
*/
virtual AgvResult cancelNavigation()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "cancelNavigation not implemented");
}
/**
* @brief AGV
*
*
*
*/
virtual AgvResult setVelocity(const AgvVelocity& velocity)
{
(void)velocity;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "setVelocity not implemented");
}
/**
* @brief
*
* 使
* cancelNavigation()
*/
virtual AgvResult stopVelocityControl()
{
return setVelocity(AgvVelocity{});
}
/**
* @brief AGV
*/
virtual AgvResult listMaps(std::vector<std::string>& maps) const
{
(void)maps;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "listMaps not implemented");
}
/**
* @brief
*/
virtual AgvResult listStations(std::vector<AgvStation>& stations) const
{
(void)stations;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "listStations not implemented");
}
/**
* @brief
*/
virtual AgvResult switchMap(const std::string& map_name)
{
(void)map_name;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "switchMap not implemented");
}
/**
* @brief
*/
virtual AgvResult uploadMap(const std::string& map_name, const std::string& content)
{
(void)map_name;
(void)content;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "uploadMap not implemented");
}
/**
* @brief
*/
virtual AgvResult downloadMap(const std::string& map_name, std::string& content) const
{
(void)map_name;
(void)content;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "downloadMap not implemented");
}
/**
* @brief /
*/
virtual AgvResult startMapping(const AgvMappingOptions& options = {})
{
(void)options;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "startMapping not implemented");
}
/**
* @brief
*
* 使 gRPC 使
* getUnifiedMapUpdate()
*/
virtual AgvResult getMappingData(int start_index, AgvMappingData& data) const
{
(void)start_index;
(void)data;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "getMappingData not implemented");
}
/**
* @brief
*
* after_sequence 0 0
*
* options.wait_timeout_ms 线
*/
virtual AgvResult getUnifiedMapUpdate(
std::uint64_t after_sequence,
const AgvMapStreamOptions& options,
AgvUnifiedMapUpdate& update) const
{
(void)after_sequence;
(void)options;
(void)update;
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "getUnifiedMapUpdate not implemented");
}
/**
* @brief /
*/
virtual AgvResult stopMapping()
{
return AgvResult::failure(AgvErrorCode::UnsupportedCommand, "stopMapping not implemented");
}
};
} // namespace cmvr::device
#endif // CMVR_ES_ABSTRACT_AGV_H

View File

@ -7,6 +7,7 @@
#include "common/base/logging/logger.h"
#include "devices/agv/abstract_agv.h"
#include "devices/agv/my_agv/include/my_agv.h"
#include "devices/agv/src1100/include/src1100_agv.h"
namespace cmvr::device {
@ -30,6 +31,16 @@ public:
backend.set_id(cfg.id());
return std::make_shared<MyAgv>(backend);
}
case config::AGVDeviceConfig::kSrc1100Agv:
{
if (!cfg.src1100_agv().id().empty() && cfg.src1100_agv().id() != cfg.id()) {
CMVR_LOG(ERROR) << "[AGVFactory]: AGV id does not match backend id: " << cfg.id();
return nullptr;
}
auto backend = cfg.src1100_agv();
backend.set_id(cfg.id());
return std::make_shared<Src1100Agv>(backend);
}
case config::AGVDeviceConfig::BACKEND_NOT_SET:
default:

View File

@ -20,15 +20,13 @@ public:
bool stop() override;
bool update() override;
bool getState(AGVState& state) override;
bool eStop() override;
bool goHome() override;
bool moveto(math::Pose2d& location, double speed_ratio) override;
bool setVelocity(math::Vec3 linear, math::Vec3 angular) override;
bool initMap(float resolution, int width, int height) override;
bool updateMap() override;
bool saveMap(const std::string& file_path) override;
bool loadMap(const std::string& file_path) override;
AgvRuntimeState runtimeState() const override;
AgvResult emergencyStop() override;
AgvResult navigateToPose(
const math::Pose2d& pose,
const AgvMotionOptions& options = {},
const AgvAdapterParams& adapter_params = AgvAdapterParams{}) override;
AgvResult setVelocity(const AgvVelocity& velocity) override;
private:
config::MyAgvConfig config_;

View File

@ -27,49 +27,27 @@ bool MyAgv::update()
return true;
}
bool MyAgv::getState(AGVState&)
AgvRuntimeState MyAgv::runtimeState() const
{
return true;
return {};
}
bool MyAgv::eStop()
AgvResult MyAgv::emergencyStop()
{
return true;
return AgvResult::success();
}
bool MyAgv::goHome()
AgvResult MyAgv::navigateToPose(
const math::Pose2d&,
const AgvMotionOptions&,
const AgvAdapterParams&)
{
return true;
return AgvResult::success();
}
bool MyAgv::moveto(math::Pose2d&, double)
AgvResult MyAgv::setVelocity(const AgvVelocity&)
{
return true;
}
bool MyAgv::setVelocity(math::Vec3, math::Vec3)
{
return true;
}
bool MyAgv::initMap(float, int, int)
{
return true;
}
bool MyAgv::updateMap()
{
return true;
}
bool MyAgv::saveMap(const std::string&)
{
return true;
}
bool MyAgv::loadMap(const std::string&)
{
return true;
return AgvResult::success();
}
} // namespace cmvr::device

View File

@ -0,0 +1,12 @@
add_library(src1100_agv SHARED src/src1100_agv.cpp)
target_include_directories(src1100_agv PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(src1100_agv
PUBLIC
cmvr_es::proto
jsoncpp
)
add_library(cmvr_es::device::src1100_agv ALIAS src1100_agv)
install(TARGETS src1100_agv LIBRARY DESTINATION lib)

View File

@ -0,0 +1,179 @@
#ifndef CMVR_ES_SRC1100_AGV_H
#define CMVR_ES_SRC1100_AGV_H
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <json/json.h>
#include "cmvr/config/agv_config/agv_config.pb.h"
#include "devices/agv/abstract_agv.h"
namespace cmvr::device {
class Src1100Agv final : public AbstractAGV {
public:
explicit Src1100Agv(const config::Src1100AgvConfig& cfg);
~Src1100Agv() override;
std::string typeName() const override { return "Src1100Agv"; }
bool init() override;
bool start() override;
bool stop() override;
bool update() override;
AgvRuntimeState runtimeState() const override;
AgvNavigationStatus navigationStatus() const override;
AgvResult emergencyStop() override;
AgvResult clearFault() override;
AgvResult navigateToPose(
const math::Pose2d& pose,
const AgvMotionOptions& options = {},
const AgvAdapterParams& adapter_params = AgvAdapterParams{}) override;
AgvResult navigateToStation(
const std::string& station_id,
const AgvMotionOptions& options = {},
const AgvAdapterParams& adapter_params = AgvAdapterParams{}) override;
AgvResult followPath(const std::vector<AgvPathSegment>& path) override;
AgvResult pauseNavigation() override;
AgvResult resumeNavigation() override;
AgvResult cancelNavigation() override;
AgvResult setVelocity(const AgvVelocity& velocity) override;
AgvResult listMaps(std::vector<std::string>& maps) const override;
AgvResult listStations(std::vector<AgvStation>& stations) const override;
AgvResult switchMap(const std::string& map_name) override;
AgvResult uploadMap(const std::string& map_name, const std::string& content) override;
AgvResult downloadMap(const std::string& map_name, std::string& content) const override;
AgvResult startMapping(const AgvMappingOptions& options = {}) override;
AgvResult getMappingData(int start_index, AgvMappingData& data) const override;
AgvResult getUnifiedMapUpdate(
std::uint64_t after_sequence,
const AgvMapStreamOptions& options,
AgvUnifiedMapUpdate& update) const override;
AgvResult stopMapping() override;
private:
struct Ports {
int status{19204};
int control{19205};
int navigation{19206};
int config{19207};
int other{19210};
int push{19301};
};
AgvResult connect_();
AgvResult disconnect_();
AgvResult connectSocket_(int& sock, int port);
AgvResult ensureOtherSocket_();
void closeSocket_(int& sock) const;
bool connected_() const;
AgvResult sendCommand_(int sock,
std::uint16_t command,
const Json::Value& payload,
Json::Value* response) const;
AgvResult sendCommandRaw_(int sock,
std::uint16_t command,
const Json::Value& payload,
std::string* response_payload) const;
AgvResult sendCommandNoResponse_(int sock, std::uint16_t command, const Json::Value& payload) const;
AgvResult configurePush_();
void startPushThread_();
void stopPushThread_();
void pushLoop_();
AgvRuntimeState queryRuntimeState_() const;
void updateCachedRuntimeState_(const Json::Value& payload);
void startMapUpdateThread_();
void stopMapUpdateThread_();
void mapUpdateLoop_();
AgvResult refreshMapCacheOnce_(const AgvMapStreamOptions& options) const;
AgvResult parseMapFileToUpdates_(
const std::string& file_name,
const std::string& content,
const AgvMapStreamOptions& options,
std::vector<AgvUnifiedMapUpdate>& updates) const;
AgvResult parseSrc1100MapArchive_(
const std::string& file_name,
const std::string& content,
const AgvMapStreamOptions& options,
std::vector<AgvUnifiedMapUpdate>& updates) const;
AgvResult parseSrc1100Map2D_(
const std::string& file_name,
const std::string& content,
const AgvMapStreamOptions& options,
AgvUnifiedMapUpdate& update) const;
AgvResult parseSrc1100Map3D_(
const std::string& file_name,
const std::string& content,
const AgvMapStreamOptions& options,
AgvUnifiedMapUpdate& update) const;
void cacheMapUpdates_(std::vector<AgvUnifiedMapUpdate> updates) const;
bool findCachedMapUpdate_(
std::uint64_t after_sequence,
const AgvMapStreamOptions& options,
AgvUnifiedMapUpdate& update) const;
bool mapUpdateMatches_(
const AgvUnifiedMapUpdate& update,
const AgvMapStreamOptions& options) const;
static std::vector<std::uint8_t> buildFrame_(std::uint16_t command, const std::string& payload);
static std::string toJsonString_(const Json::Value& value);
static bool parseJson_(const std::string& input, Json::Value& output, std::string& error);
static std::string extractJson_(const std::string& raw);
static AgvResult receiveFrame_(int sock, std::uint16_t& command, std::string& payload);
static int optionalInt_(const AgvAdapterParams& params, const std::string& key, int fallback);
static double optionalDouble_(const AgvAdapterParams& params, const std::string& key, double fallback);
static void applyMotionOptions_(Json::Value& payload, const AgvMotionOptions& options);
static void applyAdapterParams_(Json::Value& payload, const AgvAdapterParams& params);
static AgvResult resultFromResponse_(const Json::Value& response);
config::Src1100AgvConfig config_;
std::string ip_;
int recv_timeout_ms_{1000};
Ports ports_;
bool state_push_enabled_{false};
bool map_update_enabled_{false};
int map_update_interval_ms_{1000};
std::size_t map_update_history_size_{8};
mutable std::mutex mutex_;
int sock_status_{-1};
int sock_control_{-1};
int sock_navigation_{-1};
int sock_config_{-1};
int sock_other_{-1};
int sock_push_{-1};
std::string last_error_;
std::atomic<bool> push_running_{false};
std::thread push_thread_;
mutable std::mutex runtime_state_mutex_;
AgvRuntimeState cached_runtime_state_;
bool cached_runtime_state_valid_{false};
mutable std::atomic<bool> map_update_running_{false};
mutable std::thread map_update_thread_;
mutable std::mutex map_update_mutex_;
mutable std::condition_variable map_update_cv_;
mutable std::deque<AgvUnifiedMapUpdate> cached_map_updates_;
mutable std::uint64_t map_sequence_{0};
mutable int next_mapping_index_{0};
mutable std::size_t last_map_content_hash_{0};
mutable std::string map_session_id_;
};
} // namespace cmvr::device
#endif // CMVR_ES_SRC1100_AGV_H

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,36 @@
add_library(aubo_arm SHARED
src/aubo_arm.cpp
aubo_arm.cpp
)
target_include_directories(aubo_arm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set(AUBO_SDK_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/AuboSdk/linux/include)
set(AUBO_SDK_LIB_DIR ${CMAKE_SOURCE_DIR}/third_party/AuboSdk/linux/lib)
set(AUBO_SDK_ROOT ${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/aubo_sdk/v0.27.1)
set(AUBO_SDK_INCLUDE_DIR ${AUBO_SDK_ROOT}/include)
set(AUBO_SDK_LIB_DIR ${AUBO_SDK_ROOT}/lib)
if (EXISTS "${AUBO_SDK_INCLUDE_DIR}/aubo_sdk/rpc.h")
if (EXISTS "${AUBO_SDK_LIB_DIR}/cmake/aubo_sdk/aubo_sdkConfig.cmake")
list(APPEND CMAKE_PREFIX_PATH "${AUBO_SDK_LIB_DIR}/cmake")
find_package(Qt5Core QUIET)
if (NOT Qt5Core_FOUND AND NOT TARGET Qt5::Core)
find_library(QT5_CORE_LIBRARY
NAMES Qt5Core libQt5Core.so.5
PATHS /lib /usr/lib /usr/local/lib /lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu
)
if (QT5_CORE_LIBRARY)
add_library(Qt5::Core UNKNOWN IMPORTED)
set_target_properties(Qt5::Core PROPERTIES
IMPORTED_LOCATION "${QT5_CORE_LIBRARY}"
)
endif()
endif()
find_package(aubo_sdk REQUIRED CONFIG PATHS "${AUBO_SDK_LIB_DIR}/cmake/aubo_sdk" NO_DEFAULT_PATH)
set_target_properties(aubo_sdk::aubo_sdk aubo_sdk::robot_proxy PROPERTIES
MAP_IMPORTED_CONFIG_DEBUG Release
)
target_compile_definitions(aubo_arm PRIVATE CMVR_HAS_AUBO_SDK)
target_include_directories(aubo_arm PRIVATE ${AUBO_SDK_INCLUDE_DIR})
target_link_libraries(aubo_arm PRIVATE aubo_sdk::aubo_sdk)
elseif (EXISTS "${AUBO_SDK_INCLUDE_DIR}/aubo_sdk/rpc.h")
target_compile_definitions(aubo_arm PRIVATE CMVR_HAS_AUBO_SDK)
target_include_directories(aubo_arm PRIVATE ${AUBO_SDK_INCLUDE_DIR})
if (EXISTS "${AUBO_SDK_LIB_DIR}")

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ public:
CartesianPose getTcpPose(FrameType frame = FrameType::Base) const override;
RobotMode getRobotMode() const override;
SafetyMode getSafetyMode() const override { return SafetyMode::Normal; }
ControlMode getControlMode() const override { return ControlMode::Position; }
ControlMode getControlMode() const override { return servo_mode_.load() ? ControlMode::Servo : ControlMode::Position; }
Result torqueOn() override;
Result torqueOff() override;
@ -98,8 +98,10 @@ private:
std::string username_;
std::string password_;
double speed_scaling_{1.0};
ServoOptions servo_options_;
std::atomic<bool> connected_{false};
std::atomic<bool> busy_{false};
std::atomic<bool> servo_mode_{false};
bool emergency_stopped_{false};
mutable std::mutex mutex_;

View File

@ -1,575 +0,0 @@
#include "devices/arm/aubo_arm/include/aubo_arm.h"
#include <algorithm>
#include <chrono>
#include <exception>
#include <thread>
#include "common/base/logging/logger.h"
#if defined(CMVR_HAS_AUBO_SDK)
#include "aubo_sdk/rpc.h"
#endif
namespace cmvr::device {
namespace {
struct BusyGuard {
std::atomic<bool>& busy;
~BusyGuard() { busy.store(false); }
};
std::vector<std::string> defaultJointNames(const std::size_t dof)
{
std::vector<std::string> names;
names.reserve(dof);
for (std::size_t i = 0; i < dof; ++i) {
names.push_back("joint_" + std::to_string(i + 1));
}
return names;
}
std::string vendorBrandName(const config::VendorRobotArmBrand brand)
{
switch (brand) {
case config::VENDOR_ROBOT_ARM_BRAND_AUBO_ARM:
return "AuboARM";
case config::VENDOR_ROBOT_ARM_BRAND_UNKNOWN:
default:
return "Unknown";
}
}
} // namespace
#if defined(CMVR_HAS_AUBO_SDK)
struct AuboArm::SdkState {
std::shared_ptr<arcs::aubo_sdk::RpcClient> rpc_client;
};
#endif
AuboArm::AuboArm(const config::RobotArmConfig& cfg)
: cfg_(cfg)
{
id_ = cfg.id();
if (cfg.has_vendor()) {
vendor_cfg_ = cfg.vendor();
}
ip_ = vendor_cfg_.ip();
port_ = vendor_cfg_.port() > 0 ? vendor_cfg_.port() : 30004;
username_ = vendor_cfg_.username().empty() ? "aubo" : vendor_cfg_.username();
password_ = vendor_cfg_.password().empty() ? "123456" : vendor_cfg_.password();
const auto dof = vendor_cfg_.dof() > 0 ? static_cast<std::size_t>(vendor_cfg_.dof()) : 6U;
model_.name = vendor_cfg_.model().empty() ? "AuboARM" : vendor_cfg_.model();
model_.manufacturer = vendorBrandName(vendor_cfg_.brand());
model_.dof = dof;
model_.joint_names.assign(vendor_cfg_.joint_names().begin(), vendor_cfg_.joint_names().end());
if (model_.joint_names.empty()) {
model_.joint_names = defaultJointNames(dof);
}
if (model_.joint_names.size() != dof) {
CMVR_LOG(ERROR) << "[AuboArm] joint_names size mismatch, id=" << id_;
model_.joint_names = defaultJointNames(dof);
}
}
AuboArm::~AuboArm()
{
(void)disconnect();
}
bool AuboArm::init()
{
if (ip_.empty()) {
CMVR_LOG(ERROR) << "[AuboArm] ip is empty, id=" << id_;
return false;
}
const auto result = connect(ip_, port_);
if (!result.ok()) {
CMVR_LOG(ERROR) << "[AuboArm] init failed: " << result.message;
return false;
}
return true;
}
bool AuboArm::stop()
{
return stopMotion().ok();
}
ArmState AuboArm::getRobotState() const
{
ArmState state;
state.connected = connected_.load();
state.powered_on = state.connected;
state.brake_released = state.connected;
state.moving = busy_.load();
state.robot_mode = getRobotMode();
state.safety_mode = getSafetyMode();
state.control_mode = getControlMode();
state.emergency_stopped = emergency_stopped_;
state.speed_scaling = speed_scaling_;
state.actual_joint_state = getJointState();
state.target_joint_state = state.actual_joint_state;
return state;
}
JointGroupState AuboArm::getJointState() const
{
JointGroupState state;
state.position.assign(model_.dof, 0.0);
state.velocity.assign(model_.dof, 0.0);
state.effort.assign(model_.dof, 0.0);
#if defined(CMVR_HAS_AUBO_SDK)
if (!connected_.load() || !sdk_ || !sdk_->rpc_client) {
return state;
}
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
CMVR_LOG(ERROR) << "[AuboArm] getJointState failed: robot name list is empty";
return state;
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (!robot_interface) {
CMVR_LOG(ERROR) << "[AuboArm] getJointState failed: robot interface is null";
return state;
}
const auto robot_state = robot_interface->getRobotState();
const auto positions = robot_state->getJointPositions();
const auto velocities = robot_state->getJointSpeeds();
const auto n = std::min<std::size_t>(model_.dof, positions.size());
for (std::size_t i = 0; i < n; ++i) {
state.position[i] = positions[i];
}
const auto vn = std::min<std::size_t>(model_.dof, velocities.size());
for (std::size_t i = 0; i < vn; ++i) {
state.velocity[i] = velocities[i];
}
} catch (const std::exception& e) {
CMVR_LOG(ERROR) << "[AuboArm] getJointState failed: " << e.what();
}
#endif
return state;
}
CartesianPose AuboArm::getTcpPose(FrameType frame) const
{
(void)frame;
return {};
}
RobotMode AuboArm::getRobotMode() const
{
if (!connected_.load()) {
return RobotMode::Disconnected;
}
if (emergency_stopped_) {
return RobotMode::Stopped;
}
return busy_.load() ? RobotMode::Running : RobotMode::Idle;
}
Result AuboArm::torqueOn()
{
const auto ready = ensureConnected_("torqueOn");
if (!ready.ok()) {
return ready;
}
#if defined(CMVR_HAS_AUBO_SDK)
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot name list is empty");
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (!robot_interface) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot interface is null");
}
double mass = 0.0;
std::vector<double> cog(3, 0.0);
std::vector<double> aom(3, 0.0);
std::vector<double> inertia(6, 0.0);
robot_interface->getRobotConfig()->setPayload(mass, cog, aom, inertia);
if (robot_interface->getRobotState()->getRobotModeType() !=
arcs::common_interface::RobotModeType::Running) {
robot_interface->getRobotManage()->poweron();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
robot_interface->getRobotManage()->startup();
}
emergency_stopped_ = false;
return Result::success();
} catch (const std::exception& e) {
return Result::failure(ArmErrorCode::CommandFailed, std::string("[AuboArm] torqueOn failed: ") + e.what());
}
#else
return unsupported_("torqueOn");
#endif
}
Result AuboArm::torqueOff()
{
const auto ready = ensureConnected_("torqueOff");
if (!ready.ok()) {
return ready;
}
#if defined(CMVR_HAS_AUBO_SDK)
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot name list is empty");
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (!robot_interface) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot interface is null");
}
robot_interface->getRobotManage()->poweroff();
return Result::success();
} catch (const std::exception& e) {
return Result::failure(ArmErrorCode::CommandFailed, std::string("[AuboArm] torqueOff failed: ") + e.what());
}
#else
return unsupported_("torqueOff");
#endif
}
Result AuboArm::calibrateZeroQ(const std::string& joint_name)
{
(void)joint_name;
return unsupported_("calibrateZeroQ");
}
Result AuboArm::emergencyStop()
{
emergency_stopped_ = true;
return stopMotion();
}
Result AuboArm::setSpeedScaling(const double scaling)
{
if (scaling < 0.0 || scaling > 1.0) {
return Result::failure(ArmErrorCode::InvalidArgument, "speed scaling must be in [0, 1]");
}
speed_scaling_ = scaling;
return Result::success();
}
Result AuboArm::moveJ(const JointPositionCommand& target, const MotionOptions& options)
{
std::string error;
if (!validDof_(target.position.size(), error)) {
return Result::failure(ArmErrorCode::InvalidDof, error);
}
const auto ready = ensureConnected_("moveJ");
if (!ready.ok()) {
return ready;
}
if (busy_.exchange(true)) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] arm is busy: " + id_);
}
BusyGuard busy_guard{busy_};
#if defined(CMVR_HAS_AUBO_SDK)
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot name list is empty");
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (!robot_interface) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot interface is null");
}
robot_interface->getMotionControl()->setSpeedFraction(speed_scaling_);
robot_interface->getMotionControl()->moveJoint(
target.position,
options.acceleration > 0.0 ? options.acceleration : 0.5,
options.velocity > 0.0 ? options.velocity : 0.5,
options.blend_radius,
0);
return Result::success();
} catch (const std::exception& e) {
return Result::failure(ArmErrorCode::CommandFailed, std::string("[AuboArm] moveJ failed: ") + e.what());
}
#else
return unsupported_("moveJ");
#endif
}
Result AuboArm::speedJ(const JointVelocityCommand& velocity, double acceleration, double duration)
{
(void)velocity;
(void)acceleration;
(void)duration;
return unsupported_("speedJ");
}
Result AuboArm::stopJ(double acceleration)
{
(void)acceleration;
return stopMotion();
}
Result AuboArm::moveL(const CartesianPose& target, const MotionOptions& options, FrameType frame)
{
(void)frame;
const auto ready = ensureConnected_("moveL");
if (!ready.ok()) {
return ready;
}
if (busy_.exchange(true)) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] arm is busy: " + id_);
}
BusyGuard busy_guard{busy_};
#if defined(CMVR_HAS_AUBO_SDK)
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot name list is empty");
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (!robot_interface) {
return Result::failure(ArmErrorCode::RobotNotReady, "[AuboArm] robot interface is null");
}
robot_interface->getMotionControl()->setSpeedFraction(speed_scaling_);
std::vector<double> tcp_offset(6, 0.0);
robot_interface->getRobotConfig()->setTcpOffset(tcp_offset);
std::vector<double> pose{target.x, target.y, target.z, target.rx, target.ry, target.rz};
robot_interface->getMotionControl()->moveLine(
pose,
options.acceleration > 0.0 ? options.acceleration : 0.5,
options.velocity > 0.0 ? options.velocity : 0.25,
options.blend_radius,
0);
return Result::success();
} catch (const std::exception& e) {
return Result::failure(ArmErrorCode::CommandFailed, std::string("[AuboArm] moveL failed: ") + e.what());
}
#else
return unsupported_("moveL");
#endif
}
Result AuboArm::speedL(const CartesianVelocity& velocity, double acceleration, double duration, FrameType frame)
{
(void)velocity;
(void)acceleration;
(void)duration;
(void)frame;
return unsupported_("speedL");
}
Result AuboArm::stopL(std::optional<double> acceleration)
{
(void)acceleration;
return stopMotion();
}
Result AuboArm::stopMotion()
{
#if defined(CMVR_HAS_AUBO_SDK)
if (!connected_.load() || !sdk_ || !sdk_->rpc_client) {
return Result::success();
}
try {
const auto robot_names = sdk_->rpc_client->getRobotNames();
if (robot_names.empty()) {
return Result::success();
}
auto robot_interface = sdk_->rpc_client->getRobotInterface(robot_names.front());
if (robot_interface) {
robot_interface->getMotionControl()->stopMove();
}
busy_.store(false);
return Result::success();
} catch (const std::exception& e) {
return Result::failure(ArmErrorCode::CommandFailed, std::string("[AuboArm] stopMotion failed: ") + e.what());
}
#else
busy_.store(false);
return Result::success();
#endif
}
Result AuboArm::startServoMode(const ServoOptions& options)
{
(void)options;
return unsupported_("startServoMode");
}
Result AuboArm::servoJ(const JointPositionCommand& target)
{
(void)target;
return unsupported_("servoJ");
}
Result AuboArm::servoL(const CartesianPose& target, FrameType frame)
{
(void)target;
(void)frame;
return unsupported_("servoL");
}
Result AuboArm::servoSpeedJ(const JointVelocityCommand& velocity)
{
(void)velocity;
return unsupported_("servoSpeedJ");
}
Result AuboArm::servoSpeedL(const CartesianVelocity& velocity, FrameType frame)
{
(void)velocity;
(void)frame;
return unsupported_("servoSpeedL");
}
Result AuboArm::stopServoMode()
{
return Result::success();
}
Result AuboArm::connect(const std::string& ip, const int port)
{
if (connected_.load()) {
return Result::success();
}
if (ip.empty()) {
return Result::failure(ArmErrorCode::InvalidArgument, "[AuboArm] ip is empty");
}
#if defined(CMVR_HAS_AUBO_SDK)
try {
sdk_ = std::make_unique<SdkState>();
sdk_->rpc_client = std::make_shared<arcs::aubo_sdk::RpcClient>();
sdk_->rpc_client->setRequestTimeout(1000);
sdk_->rpc_client->connect(ip, port > 0 ? port : 30004);
sdk_->rpc_client->login(username_, password_);
ip_ = ip;
port_ = port > 0 ? port : 30004;
connected_.store(true);
return Result::success();
} catch (const std::exception& e) {
sdk_.reset();
connected_.store(false);
return Result::failure(ArmErrorCode::ConnectionFailed,
std::string("[AuboArm] connect failed: ") + e.what());
}
#else
(void)port;
return Result::failure(ArmErrorCode::UnsupportedCommand,
"[AuboArm] Aubo SDK is not available in this build");
#endif
}
Result AuboArm::disconnect()
{
#if defined(CMVR_HAS_AUBO_SDK)
try {
if (sdk_ && sdk_->rpc_client) {
sdk_->rpc_client->logout();
sdk_->rpc_client->disconnect();
}
} catch (const std::exception& e) {
CMVR_LOG(ERROR) << "[AuboArm] disconnect failed: " << e.what();
}
sdk_.reset();
#endif
connected_.store(false);
busy_.store(false);
return Result::success();
}
Result AuboArm::shutdown()
{
(void)stopMotion();
return disconnect();
}
Result AuboArm::loadProgram(const std::string& program_name)
{
(void)program_name;
return unsupported_("loadProgram");
}
Result AuboArm::playProgram()
{
return unsupported_("playProgram");
}
Result AuboArm::pauseProgram()
{
return unsupported_("pauseProgram");
}
Result AuboArm::stopProgram()
{
return unsupported_("stopProgram");
}
std::vector<double> AuboArm::ik(const std::string& base_link,
const std::string& ee_link,
const CartesianPose& pose)
{
(void)base_link;
(void)ee_link;
(void)pose;
CMVR_LOG(ERROR) << "[AuboArm] ik is not implemented";
return {};
}
CartesianPose AuboArm::fk(const std::string& base_link, const std::string& ee_link)
{
(void)base_link;
(void)ee_link;
CMVR_LOG(ERROR) << "[AuboArm] fk(base,ee) is not implemented";
return {};
}
CartesianPose AuboArm::fk(bool is_tcp)
{
(void)is_tcp;
CMVR_LOG(ERROR) << "[AuboArm] fk is not implemented";
return {};
}
Result AuboArm::unsupported_(const std::string& name) const
{
const std::string message = "[AuboArm] " + name + " is not implemented";
CMVR_LOG(ERROR) << message;
return Result::failure(ArmErrorCode::UnsupportedCommand, message);
}
bool AuboArm::validDof_(const std::size_t size, std::string& error) const
{
if (size != model_.dof) {
error = "[AuboArm] command dof mismatch, expected=" + std::to_string(model_.dof) +
", actual=" + std::to_string(size);
CMVR_LOG(ERROR) << error;
return false;
}
return true;
}
Result AuboArm::ensureConnected_(const std::string& context) const
{
if (!connected_.load()) {
return Result::failure(ArmErrorCode::NotConnected,
"[AuboArm] " + context + " failed: arm is not connected");
}
#if defined(CMVR_HAS_AUBO_SDK)
if (!sdk_ || !sdk_->rpc_client) {
return Result::failure(ArmErrorCode::NotConnected,
"[AuboArm] " + context + " failed: SDK client is null");
}
#endif
return Result::success();
}
} // namespace cmvr::device

View File

@ -117,6 +117,7 @@ bool HuayanRobot::init()
CMVR_LOG(ERROR) << "[HuayanRobot] init failed: " << result.message;
return false;
}
setSpeedScaling(1);
return true;
}

View File

@ -6,7 +6,7 @@
#include "cmvr/config/arm_config/arm_config.pb.h"
#include "common/base/logging/logger.h"
#include "devices/arm/aubo_arm/include/aubo_arm.h"
#include "devices/arm/aubo_arm/aubo_arm.h"
#include "devices/arm/huayan_arm/huayan_arm.h"
#include "devices/arm/motor_robot_arm/include/motor_robot_arm.h"

View File

@ -3,7 +3,7 @@ add_subdirectory(common)
add_subdirectory(uvc_camera)
add_subdirectory(realsense_camera)
add_subdirectory(mujoco_camera)
add_subdirectory(hikvision_camera)
add_library(camera INTERFACE)
target_include_directories(camera INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
@ -14,6 +14,7 @@ target_link_libraries(camera
cmvr_es::device::realsense_camera
cmvr_es::device::mujoco_camera
cmvr_es::device::camera_stream_encoder
cmvr_es::device::hikvision_camera
cmvr_es::proto
)

View File

@ -2,6 +2,11 @@
#define CMVR_ES_ABSTRACT_CAMERA_H
#pragma once
#include <cstdint>
#include <chrono>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include "../abstract_device.h"
#include <Eigen/Core>
@ -31,13 +36,47 @@ namespace cmvr::device {
std::vector<uint8_t> depthFrame;
//编码格式
std::string codec = ".h264";
Rs2Intrinsics intrinsics;
int width;
int height;
int fps;
bool bKey;
bool depthKey;
Rs2Intrinsics intrinsics{};
int width = 0;
int height = 0;
int fps = 0;
bool bKey = false;
bool depthKey = false;
// Protocol-neutral real-time metadata. The producer fills these values
// when a complete encoded access unit is published.
uint64_t stream_epoch = 0;
uint64_t sequence = 0;
// Opaque producer-native timing/counter values. Their units and epoch
// are source-defined; zero means that the source did not provide them.
uint64_t source_timestamp = 0;
uint64_t source_frame_number = 0;
int64_t capture_monotonic_ns = 0;
int64_t capture_utc_ns = 0;
int64_t pts = 0;
int64_t dts = 0;
int32_t time_base_num = 1;
int32_t time_base_den = 1;
int64_t duration = 0;
bool discontinuity = false;
uint32_t codec_config_generation = 0;
std::vector<uint8_t> codec_config;
};
enum class PtzCommand {
TiltUp,
TiltDown,
PanLeft,
PanRight,
UpLeft,
UpRight,
DownLeft,
DownRight,
ZoomIn,
ZoomOut,
PanAuto
};
class AbstractCamera : public AbstractDevice {
public:
// 录制状态
@ -51,7 +90,25 @@ namespace cmvr::device {
~AbstractCamera() override = default;
DeviceKind kind() const noexcept override { return DeviceKind::Camera; }
inline void getState(CameraState &state) {state = state_;}
// Every implementation must take the same lock used by its state_
// writers; CameraState contains std::string and cannot be snapshotted
// safely while another thread mutates it.
virtual void getState(CameraState &state) = 0;
DeviceHealthSnapshot healthSnapshot() override {
CameraState state{};
getState(state);
DeviceHealthSnapshot health;
health.error_message = state.error_message;
if (state.is_error) {
health.state = DeviceHealthState::Fault;
} else if (!state.error_message.empty()) {
health.state = DeviceHealthState::Degraded;
} else if (state.is_initialized) {
health.state = DeviceHealthState::Healthy;
}
return health;
}
virtual void getRGBImage(cv::Mat &color, Rs2Intrinsics& intrinsics) {}
virtual void getDepthImage(cv::Mat &depth, Rs2Intrinsics& intrinsics) {}
virtual void getRGBDImages(cv::Mat &color, cv::Mat &depth, Rs2Intrinsics& intrinsics) {}
@ -60,12 +117,27 @@ namespace cmvr::device {
virtual void pauseRecording() {}
virtual void resumeRecording() {}
virtual void getEncodedFrame(StreamFrameData& frame_data, size_t& index) {}
virtual bool waitEncodedFrame(
StreamFrameData& frame_data,
size_t& index,
std::chrono::milliseconds timeout) {
(void)timeout;
getEncodedFrame(frame_data, index);
return !frame_data.rgbFrame.empty() || !frame_data.depthFrame.empty();
}
virtual bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) {
return false;
}
virtual bool startStreaming() {return true;}
virtual void stopStreaming() {}
virtual bool controlPtz(PtzCommand command, bool stop, int speed) {
(void)command;
(void)stop;
(void)speed;
return false;
}
virtual bool requestKeyFrame() { return false; }
virtual Eigen::Vector3f get3DPointFromPixel(int u, int v) {return {0,0,0};}
protected:
CameraState state_{};

View File

@ -9,6 +9,7 @@
#include "common/base/logging/logger.h"
#include "devices/camera/abstract_camera.h"
#include "devices/camera/mujoco_camera/include/mujoco_camera.h"
#include "devices/camera/hikvision_camera/include/hikvision_camera.h"
#include "devices/camera/realsense_camera/include/realsense_camera.h"
#include "devices/camera/uvc_camera/include/uvc_camera.h"
@ -41,6 +42,10 @@ public:
return nullptr;
}
case config::CameraDeviceConfig::kHikvision:
return std::make_shared<HikvisionCamera>(
backendWithId_(cfg.id(), cfg.hikvision()));
case config::CameraDeviceConfig::kMujoco:
return std::make_shared<MujocoCamera>(
backendWithId_(cfg.id(), cfg.mujoco()));

View File

@ -0,0 +1,122 @@
add_library(hikvision_camera SHARED src/hikvision_camera.cpp)
target_include_directories(hikvision_camera PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set(HIKVISION_SDK_ROOT "${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/hikvision_sdk/v6.1.11.5")
set(HIKVISION_SDK_LIB_DIR "${HIKVISION_SDK_ROOT}/lib")
set(HIKVISION_SDK_COM_DIR "${HIKVISION_SDK_LIB_DIR}/HCNetSDKCom")
file(GLOB HIKVISION_SDK_LIBS CONFIGURE_DEPENDS
"${HIKVISION_SDK_LIB_DIR}/*.so"
"${HIKVISION_SDK_LIB_DIR}/*.so.*"
)
file(GLOB HIKVISION_SDK_COM_LIBS CONFIGURE_DEPENDS
"${HIKVISION_SDK_COM_DIR}/*.so"
"${HIKVISION_SDK_COM_DIR}/*.so.*"
)
set(HIKVISION_HCNETSDK_LIB "${HIKVISION_SDK_LIB_DIR}/libhcnetsdk.so")
if(NOT HIKVISION_SDK_LIBS)
message(FATAL_ERROR "Hikvision SDK libraries not found: ${HIKVISION_SDK_LIB_DIR}")
endif()
if(NOT HIKVISION_SDK_COM_LIBS)
message(FATAL_ERROR "Hikvision SDK component libraries not found: ${HIKVISION_SDK_COM_DIR}")
endif()
if(NOT EXISTS "${HIKVISION_HCNETSDK_LIB}")
message(FATAL_ERROR "Hikvision HCNetSDK library not found: ${HIKVISION_HCNETSDK_LIB}")
endif()
target_include_directories(hikvision_camera PRIVATE "${HIKVISION_SDK_ROOT}/include")
target_link_directories(hikvision_camera PRIVATE "${HIKVISION_SDK_LIB_DIR}" "${HIKVISION_SDK_COM_DIR}")
set(HIKVISION_JSONCPP_LINK jsoncpp)
if(TARGET jsoncpp)
get_target_property(HIKVISION_JSONCPP_IMPORTED_LOCATION jsoncpp IMPORTED_LOCATION)
get_target_property(HIKVISION_JSONCPP_IMPORTED_LOCATION_RELEASE jsoncpp IMPORTED_LOCATION_RELEASE)
get_target_property(HIKVISION_JSONCPP_INTERFACE_INCLUDES jsoncpp INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "[hikvision_camera] jsoncpp target: jsoncpp")
message(STATUS "[hikvision_camera] jsoncpp IMPORTED_LOCATION: ${HIKVISION_JSONCPP_IMPORTED_LOCATION}")
message(STATUS "[hikvision_camera] jsoncpp IMPORTED_LOCATION_RELEASE: ${HIKVISION_JSONCPP_IMPORTED_LOCATION_RELEASE}")
message(STATUS "[hikvision_camera] jsoncpp INTERFACE_INCLUDE_DIRECTORIES: ${HIKVISION_JSONCPP_INTERFACE_INCLUDES}")
else()
find_library(HIKVISION_JSONCPP_LIBRARY NAMES jsoncpp)
if(HIKVISION_JSONCPP_LIBRARY)
set(HIKVISION_JSONCPP_LINK "${HIKVISION_JSONCPP_LIBRARY}")
message(STATUS "[hikvision_camera] jsoncpp library: ${HIKVISION_JSONCPP_LIBRARY}")
else()
message(WARNING "[hikvision_camera] jsoncpp library not found by CMake; linker will resolve -ljsoncpp")
endif()
endif()
find_path(HIKVISION_JSONCPP_INCLUDE_DIR NAMES json/json.h PATH_SUFFIXES jsoncpp)
message(STATUS "[hikvision_camera] jsoncpp include dir: ${HIKVISION_JSONCPP_INCLUDE_DIR}")
if(HIKVISION_JSONCPP_INCLUDE_DIR)
target_include_directories(hikvision_camera PRIVATE "${HIKVISION_JSONCPP_INCLUDE_DIR}")
endif()
target_link_libraries(hikvision_camera
PUBLIC
glog
opencv_core
cmvr_es::proto
PRIVATE
"${HIKVISION_HCNETSDK_LIB}"
${HIKVISION_JSONCPP_LINK}
pthread
)
set_target_properties(hikvision_camera PROPERTIES
BUILD_RPATH "${HIKVISION_SDK_LIB_DIR};${HIKVISION_SDK_COM_DIR}"
INSTALL_RPATH "$ORIGIN;$ORIGIN/HCNetSDKCom"
)
add_library(cmvr_es::device::hikvision_camera ALIAS hikvision_camera)
install(TARGETS hikvision_camera LIBRARY DESTINATION lib)
install(FILES ${HIKVISION_SDK_LIBS} DESTINATION lib)
install(DIRECTORY "${HIKVISION_SDK_COM_DIR}" DESTINATION lib)
if(BUILD_TESTING)
add_executable(hikvision_camera_callback_test
tests/hikvision_camera_callback_test.cpp
src/hikvision_camera.cpp
)
target_include_directories(hikvision_camera_callback_test
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
"${HIKVISION_SDK_ROOT}/include"
)
if(HIKVISION_JSONCPP_INCLUDE_DIR)
target_include_directories(
hikvision_camera_callback_test
PRIVATE
"${HIKVISION_JSONCPP_INCLUDE_DIR}"
)
endif()
target_compile_definitions(hikvision_camera_callback_test
PRIVATE
HIKVISION_SDK_LIB_DIR="${HIKVISION_SDK_LIB_DIR}/"
)
# The test supplies a small in-process HCNetSDK fake, so it exercises the
# callback/stop ordering without requiring a camera or loading hcnetsdk.
target_link_libraries(hikvision_camera_callback_test
PRIVATE
glog
opencv_core
cmvr_es::proto
${HIKVISION_JSONCPP_LINK}
pthread
)
add_test(
NAME hikvision_camera_callback_test
COMMAND hikvision_camera_callback_test
)
set_tests_properties(hikvision_camera_callback_test PROPERTIES TIMEOUT 10)
if(UNIX AND NOT APPLE)
get_property(_hikvision_test_library_dirs DIRECTORY PROPERTY LINK_DIRECTORIES)
list(PREPEND _hikvision_test_library_dirs "${CMAKE_BINARY_DIR}/cmvr_compiler_runtime")
list(JOIN _hikvision_test_library_dirs ":" _hikvision_test_library_path)
set_tests_properties(hikvision_camera_callback_test PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${_hikvision_test_library_path}"
)
endif()
endif()

View File

@ -0,0 +1,117 @@
#ifndef CMVR_ES_HIKVISION_CAMERA_H
#define CMVR_ES_HIKVISION_CAMERA_H
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "common/base/ring_buffer.h"
#include "devices/camera/abstract_camera.h"
namespace cmvr::device {
class HikvisionCamera final : public AbstractCamera {
public:
explicit HikvisionCamera(const cmvr::config::HikvisionCameraConfig& camera);
~HikvisionCamera() override;
std::string typeName() const override { return "HikvisionCamera"; }
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) override;
void getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void startRecording(const std::string& video_path) override;
void stopRecording() override;
void pauseRecording() override;
void resumeRecording() override;
void getEncodedFrame(StreamFrameData& frame_data, size_t& index) override;
bool waitEncodedFrame(StreamFrameData& frame_data, size_t& index, std::chrono::milliseconds timeout) override;
bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) override;
bool startStreaming() override;
void stopStreaming() override;
bool controlPtz(PtzCommand command, bool stop, int speed) override;
bool executeJsonCommand(const std::string& request_json, std::string& response_json) override;
bool requestKeyFrame() override;
void onEsData(long real_handle,
unsigned int packet_type,
unsigned char* buffer,
unsigned int buffer_size,
unsigned int width,
unsigned int height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode);
private:
bool initSdk_();
void releaseSdk_();
bool login_();
bool startPreview_();
void stopPreview_();
bool requestKeyFrame_();
void stopRecordingUnlocked_();
void fillIntrinsics_(Rs2Intrinsics& intrinsics) const;
void setError_(const std::string& message);
std::string sdkError_(const std::string& action) const;
void pushEncodedFrame_(const unsigned char* buffer,
unsigned int buffer_size,
bool is_key_frame,
unsigned int width,
unsigned int height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode);
void resetStreamState_();
config::HikvisionCameraConfig camera_;
std::string ip_;
std::string username_;
std::string password_;
std::string sdk_path_;
int port_ = 8000;
int channel_ = 1;
int stream_type_ = 0;
int link_mode_ = 0;
int fps_ = 25;
int width_ = 0;
int height_ = 0;
size_t buffer_size_ = 30;
std::string codec_ = "H264";
int user_id_ = -1;
int real_handle_ = -1;
bool sdk_acquired_ = false;
std::shared_ptr<SPMCRingBuffer<StreamFrameData>> stream_frame_buffer_;
std::string current_video_path_;
mutable std::mutex ctrl_mtx_;
// SDK callbacks must never take ctrl_mtx_: NET_DVR_StopRealPlay may wait
// for an in-flight callback while stop() owns that mutex. This mutex is the
// single synchronization domain for callback publication and stream state.
mutable std::mutex callback_mtx_;
bool callback_publishing_enabled_ = false;
long callback_preview_handle_ = -1;
std::vector<uint8_t> es_stream_header_;
bool has_es_stream_header_ = false;
bool awaiting_key_frame_ = false;
int stream_count_ = 0;
uint64_t stream_epoch_ = 0;
uint64_t stream_sequence_ = 0;
uint32_t codec_config_generation_ = 0;
};
} // namespace cmvr::device
#endif // CMVR_ES_HIKVISION_CAMERA_H

View File

@ -0,0 +1,961 @@
#include "../include/hikvision_camera.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <limits.h>
#include <sstream>
#include <string>
#include <vector>
#if defined(__linux__)
#include <unistd.h>
#endif
#include "HCNetSDK.h"
#include "common/base/logging/logger.h"
#include "json/json.h"
namespace {
std::mutex g_sdk_mutex;
int g_sdk_ref_count = 0;
bool g_sdk_initialized = false;
std::atomic<int> g_ignored_data_type_log_count{0};
std::atomic<int> g_es_video_log_count{0};
std::atomic<int> g_unexpected_packet_mode_log_count{0};
constexpr DWORD kHikvisionPacketFileHeader = 0;
constexpr DWORD kHikvisionPacketVideoIFrame = 1;
constexpr DWORD kHikvisionPacketVideoBFrame = 2;
constexpr DWORD kHikvisionPacketVideoPFrame = 3;
std::string normalizeSdkPath(std::string path)
{
if (path.empty()) {
return path;
}
const char last = path.back();
if (last != '/' && last != '\\') {
path.push_back('/');
}
return path;
}
std::string defaultRuntimeSdkPath()
{
#if defined(__linux__)
char executable_path[PATH_MAX] = {0};
const ssize_t count = readlink("/proc/self/exe", executable_path, PATH_MAX - 1);
if (count > 0) {
executable_path[count] = '\0';
const auto lib_path =
std::filesystem::path(executable_path).parent_path() / ".." / "lib";
return normalizeSdkPath(std::filesystem::weakly_canonical(lib_path).string());
}
#endif
return normalizeSdkPath((std::filesystem::current_path() / ".." / "lib").string());
}
void copyCString(char* dest, size_t dest_size, const std::string& source)
{
if (dest_size == 0) {
return;
}
std::snprintf(dest, dest_size, "%s", source.c_str());
}
DWORD toHikvisionPtzCommand(cmvr::device::PtzCommand command)
{
switch (command) {
case cmvr::device::PtzCommand::TiltUp:
return TILT_UP;
case cmvr::device::PtzCommand::TiltDown:
return TILT_DOWN;
case cmvr::device::PtzCommand::PanLeft:
return PAN_LEFT;
case cmvr::device::PtzCommand::PanRight:
return PAN_RIGHT;
case cmvr::device::PtzCommand::UpLeft:
return UP_LEFT;
case cmvr::device::PtzCommand::UpRight:
return UP_RIGHT;
case cmvr::device::PtzCommand::DownLeft:
return DOWN_LEFT;
case cmvr::device::PtzCommand::DownRight:
return DOWN_RIGHT;
case cmvr::device::PtzCommand::ZoomIn:
return ZOOM_IN;
case cmvr::device::PtzCommand::ZoomOut:
return ZOOM_OUT;
case cmvr::device::PtzCommand::PanAuto:
return PAN_AUTO;
}
return 0;
}
DWORD normalizePtzSpeed(int speed)
{
if (speed <= 0) {
return 4;
}
if (speed > 7) {
return 7;
}
return static_cast<DWORD>(speed);
}
std::string lowerString(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
}
std::string jsonEscape(const std::string& value)
{
std::ostringstream out;
for (const char c : value) {
switch (c) {
case '\\':
out << "\\\\";
break;
case '"':
out << "\\\"";
break;
case '\n':
out << "\\n";
break;
case '\r':
out << "\\r";
break;
case '\t':
out << "\\t";
break;
default:
out << c;
break;
}
}
return out.str();
}
std::string jsonStringField(const Json::Value& value, const char* name)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
return member && member->isString() ? member->asString() : "";
}
bool jsonBoolField(const Json::Value& value, const char* name, bool& out)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
if (!member || !member->isBool()) {
return false;
}
out = member->asBool();
return true;
}
int jsonIntField(const Json::Value& value, const char* name, int default_value)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
return member && member->isInt() ? member->asInt() : default_value;
}
bool parseJsonCommand(const std::string& request_json, Json::Value& root, std::string& error)
{
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
return reader->parse(
request_json.data(), request_json.data() + request_json.size(), &root, &error);
}
bool parsePtzCommandName(const std::string& command, cmvr::device::PtzCommand& out)
{
const std::string normalized = lowerString(command);
if (normalized == "tilt_up" || normalized == "up") {
out = cmvr::device::PtzCommand::TiltUp;
} else if (normalized == "tilt_down" || normalized == "down") {
out = cmvr::device::PtzCommand::TiltDown;
} else if (normalized == "pan_left" || normalized == "left") {
out = cmvr::device::PtzCommand::PanLeft;
} else if (normalized == "pan_right" || normalized == "right") {
out = cmvr::device::PtzCommand::PanRight;
} else if (normalized == "up_left") {
out = cmvr::device::PtzCommand::UpLeft;
} else if (normalized == "up_right") {
out = cmvr::device::PtzCommand::UpRight;
} else if (normalized == "down_left") {
out = cmvr::device::PtzCommand::DownLeft;
} else if (normalized == "down_right") {
out = cmvr::device::PtzCommand::DownRight;
} else if (normalized == "zoom_in") {
out = cmvr::device::PtzCommand::ZoomIn;
} else if (normalized == "zoom_out") {
out = cmvr::device::PtzCommand::ZoomOut;
} else if (normalized == "pan_auto" || normalized == "auto") {
out = cmvr::device::PtzCommand::PanAuto;
} else {
return false;
}
return true;
}
std::string makeJsonResult(bool success, const std::string& error_message = "")
{
std::ostringstream out;
out << "{\"success\":" << (success ? "true" : "false");
if (!error_message.empty()) {
out << ",\"error_message\":\"" << jsonEscape(error_message) << "\"";
}
out << "}";
return out.str();
}
void CALLBACK hikvisionEsRealPlayCallback(
LONG real_handle, NET_DVR_PACKET_INFO_EX* packet_info, void* user)
{
auto* camera = static_cast<cmvr::device::HikvisionCamera*>(user);
if (!camera || !packet_info) {
return;
}
const uint64_t source_timestamp =
(static_cast<uint64_t>(packet_info->dwTimeStampHigh) << 32U) |
static_cast<uint64_t>(packet_info->dwTimeStamp);
camera->onEsData(real_handle,
packet_info->dwPacketType,
packet_info->pPacketBuffer,
packet_info->dwPacketSize,
packet_info->wWidth,
packet_info->wHeight,
source_timestamp,
packet_info->dwFrameNum,
packet_info->dwFrameRate,
packet_info->dwPacketMode);
}
} // namespace
namespace cmvr::device {
HikvisionCamera::HikvisionCamera(const config::HikvisionCameraConfig& camera)
: camera_(camera)
{
id_ = camera_.id();
ip_ = camera_.ip();
username_ = camera_.username().empty() ? "admin" : camera_.username();
password_ = camera_.password();
port_ = camera_.port() > 0 ? camera_.port() : 8000;
channel_ = camera_.channel() > 0 ? camera_.channel() : 1;
stream_type_ = camera_.stream_type() >= 0 ? camera_.stream_type() : 0;
link_mode_ = camera_.link_mode() >= 0 ? camera_.link_mode() : 0;
fps_ = camera_.fps() > 0 ? camera_.fps() : 25;
width_ = camera_.width();
height_ = camera_.height();
buffer_size_ = camera_.buffer_size() > 0 ? static_cast<size_t>(camera_.buffer_size()) : 30;
codec_ = camera_.codec().empty() ? "H264" : camera_.codec();
sdk_path_ = normalizeSdkPath(
camera_.sdk_path().empty() ? defaultRuntimeSdkPath() : camera_.sdk_path());
// Keep the shared_ptr itself immutable after construction. Readers and the
// SDK callback may use it concurrently; the ring buffer owns its locking.
stream_frame_buffer_ = std::make_shared<SPMCRingBuffer<StreamFrameData>>(buffer_size_);
state_.fps = fps_;
state_.width = width_;
state_.height = height_;
if (id_.empty()) {
setError_("[HikvisionCamera] camera id is empty");
} else if (ip_.empty()) {
setError_("[HikvisionCamera] ip is empty");
} else if (password_.empty()) {
setError_("[HikvisionCamera] password is empty");
}
}
HikvisionCamera::~HikvisionCamera()
{
stop();
}
bool HikvisionCamera::init()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
state_.is_initialized = false;
if (ip_.empty() || username_.empty() || password_.empty()) {
setError_("ip, username or password is empty");
return false;
}
stream_frame_buffer_->clear();
resetStreamState_();
if (!initSdk_()) {
return false;
}
state_.is_initialized = true;
state_.is_error = false;
CMVR_LOG(INFO) << "[HikvisionCamera] initialized: id=" << id_
<< ", ip=" << ip_
<< ", channel=" << channel_
<< ", stream_type=" << stream_type_;
return true;
}
bool HikvisionCamera::start()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_initialized) {
setError_("camera not initialized");
return false;
}
if (state_.is_opened) {
return true;
}
if (!login_()) {
return false;
}
if (!startPreview_()) {
if (user_id_ >= 0) {
NET_DVR_Logout(user_id_);
user_id_ = -1;
}
return false;
}
state_.is_opened = true;
return true;
}
bool HikvisionCamera::stop()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (state_.is_recording) {
stopRecordingUnlocked_();
}
state_.is_streaming = false;
stream_count_ = 0;
resetStreamState_();
stopPreview_();
if (user_id_ >= 0) {
NET_DVR_Logout(user_id_);
user_id_ = -1;
}
state_.is_opened = false;
releaseSdk_();
return true;
}
void HikvisionCamera::getState(CameraState& state)
{
std::lock_guard lock(ctrl_mtx_);
state = state_;
}
void HikvisionCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
fillIntrinsics_(intrinsics);
setError_("getRGBImage unsupported: HikvisionCamera only forwards stream data");
color.release();
}
void HikvisionCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics&)
{
std::lock_guard lock(ctrl_mtx_);
setError_("getDepthImage unsupported usage");
depth.release();
}
void HikvisionCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
getRGBImage(color, intrinsics);
depth.release();
}
void HikvisionCamera::startRecording(const std::string& video_path)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || real_handle_ < 0) {
setError_("camera not opened");
return;
}
if (state_.is_recording) {
setError_("already recording");
return;
}
if (video_path.empty()) {
setError_("video path is empty");
return;
}
current_video_path_ = video_path;
if (!NET_DVR_SaveRealData(real_handle_, const_cast<char*>(current_video_path_.c_str()))) {
setError_(sdkError_("NET_DVR_SaveRealData"));
current_video_path_.clear();
return;
}
state_.is_recording = true;
}
void HikvisionCamera::stopRecording()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
stopRecordingUnlocked_();
}
void HikvisionCamera::stopRecordingUnlocked_()
{
clear_error_();
if (!state_.is_recording) {
return;
}
if (real_handle_ >= 0 && !NET_DVR_StopSaveRealData(real_handle_)) {
setError_(sdkError_("NET_DVR_StopSaveRealData"));
return;
}
state_.is_recording = false;
current_video_path_.clear();
}
void HikvisionCamera::pauseRecording()
{
std::lock_guard lock(ctrl_mtx_);
setError_("pauseRecording unsupported by Hikvision SDK recording");
}
void HikvisionCamera::resumeRecording()
{
std::lock_guard lock(ctrl_mtx_);
setError_("resumeRecording unsupported by Hikvision SDK recording");
}
void HikvisionCamera::getEncodedFrame(StreamFrameData& frame_data, size_t& index)
{
if (!stream_frame_buffer_) {
return;
}
auto frame = stream_frame_buffer_->pop(index);
if (frame) {
frame_data = std::move(*frame);
}
}
bool HikvisionCamera::waitEncodedFrame(
StreamFrameData& frame_data,
size_t& index,
const std::chrono::milliseconds timeout)
{
if (!stream_frame_buffer_) {
return false;
}
auto frame = stream_frame_buffer_->waitPop(index, timeout);
if (!frame) {
return false;
}
frame_data = std::move(*frame);
return !frame_data.rgbFrame.empty() || !frame_data.depthFrame.empty();
}
bool HikvisionCamera::getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index)
{
if (!stream_frame_buffer_) {
return false;
}
auto frame = stream_frame_buffer_->getLatest(next_index);
if (!frame.has_value()) {
return false;
}
frame_data = frame.value();
return true;
}
bool HikvisionCamera::startStreaming()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened) {
setError_("camera not opened");
return false;
}
const bool first_stream = stream_count_ == 0;
if (first_stream) {
if (stream_frame_buffer_) {
stream_frame_buffer_->clear();
}
std::lock_guard callback_lock(callback_mtx_);
if (callback_preview_handle_ < 0 ||
callback_preview_handle_ != static_cast<long>(real_handle_)) {
setError_("preview callback is not active");
return false;
}
++stream_epoch_;
stream_sequence_ = 0;
++codec_config_generation_;
awaiting_key_frame_ = true;
callback_publishing_enabled_ = true;
}
state_.is_streaming = true;
++stream_count_;
if (first_stream && !requestKeyFrame_()) {
// Keep waiting for the next natural I-frame. Publishing P/B frames
// immediately would make a newly attached decoder start corrupted.
CMVR_LOG(WARNING) << "[HikvisionCamera] key-frame request failed; "
<< "waiting for the next natural I-frame";
}
return true;
}
void HikvisionCamera::stopStreaming()
{
std::lock_guard lock(ctrl_mtx_);
if (stream_count_ > 0) {
--stream_count_;
}
if (stream_count_ == 0) {
// Wait for an already-running callback to finish its publication, then
// prevent both queued and future callbacks from publishing. No frame
// can be pushed after this critical section has completed.
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
awaiting_key_frame_ = false;
state_.is_streaming = false;
}
}
bool HikvisionCamera::controlPtz(PtzCommand command, bool stop, int speed)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || user_id_ < 0) {
setError_("camera not opened");
return false;
}
const DWORD sdk_command = toHikvisionPtzCommand(command);
if (sdk_command == 0) {
setError_("unsupported PTZ command");
return false;
}
const DWORD sdk_stop = stop ? 1 : 0;
const DWORD sdk_speed = normalizePtzSpeed(speed);
if (!NET_DVR_PTZControlWithSpeed_Other(user_id_, channel_, sdk_command, sdk_stop, sdk_speed)) {
setError_(sdkError_("NET_DVR_PTZControlWithSpeed_Other"));
return false;
}
return true;
}
bool HikvisionCamera::requestKeyFrame()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || user_id_ < 0) {
setError_("camera not opened");
return false;
}
if (!requestKeyFrame_()) {
setError_(sdkError_(
stream_type_ == 0 ? "NET_DVR_MakeKeyFrame" : "NET_DVR_MakeKeyFrameSub"));
return false;
}
return true;
}
bool HikvisionCamera::requestKeyFrame_()
{
if (user_id_ < 0) {
CMVR_LOG(WARNING) << "[HikvisionCamera] skip key frame request: camera not logged in";
return false;
}
const bool is_sub_stream = stream_type_ != 0;
const BOOL success = is_sub_stream
? NET_DVR_MakeKeyFrameSub(user_id_, channel_)
: NET_DVR_MakeKeyFrame(user_id_, channel_);
if (!success) {
CMVR_LOG(WARNING) << "[HikvisionCamera] "
<< (is_sub_stream ? "NET_DVR_MakeKeyFrameSub" : "NET_DVR_MakeKeyFrame")
<< " failed, error_code=" << NET_DVR_GetLastError();
return false;
}
CMVR_LOG(INFO) << "[HikvisionCamera] requested key frame"
<< ", channel=" << channel_
<< ", stream_type=" << stream_type_;
return true;
}
bool HikvisionCamera::executeJsonCommand(const std::string& request_json, std::string& response_json)
{
Json::Value root;
std::string parse_error;
if (!parseJsonCommand(request_json, root, parse_error) || !root.isObject()) {
response_json = makeJsonResult(false, "invalid json: " + parse_error);
return false;
}
const std::string command_type = lowerString(
!jsonStringField(root, "command").empty() ? jsonStringField(root, "command") :
!jsonStringField(root, "type").empty() ? jsonStringField(root, "type") :
jsonStringField(root, "action"));
if (command_type != "ptz") {
response_json = makeJsonResult(false, "unsupported json command: " + command_type);
return false;
}
const std::string ptz_command_name =
!jsonStringField(root, "direction").empty() ? jsonStringField(root, "direction") :
!jsonStringField(root, "ptz_command").empty() ? jsonStringField(root, "ptz_command") :
jsonStringField(root, "operation");
PtzCommand ptz_command{};
if (!parsePtzCommandName(ptz_command_name, ptz_command)) {
response_json = makeJsonResult(false, "invalid ptz command: " + ptz_command_name);
return false;
}
bool stop = false;
if (!jsonBoolField(root, "stop", stop)) {
const std::string ptz_action = lowerString(jsonStringField(root, "ptz_action"));
const std::string action = lowerString(jsonStringField(root, "action"));
stop = ptz_action == "stop" || action == "stop";
}
const int speed = jsonIntField(root, "speed", 0);
if (!controlPtz(ptz_command, stop, speed)) {
CameraState state;
getState(state);
response_json = makeJsonResult(
false,
state.error_message.empty() ? "failed to control PTZ" : state.error_message);
return false;
}
std::ostringstream result;
result << "{\"success\":true"
<< ",\"command\":\"ptz\""
<< ",\"direction\":\"" << jsonEscape(ptz_command_name) << "\""
<< ",\"stop\":" << (stop ? "true" : "false")
<< ",\"speed\":" << static_cast<int>(normalizePtzSpeed(speed))
<< "}";
response_json = result.str();
return true;
}
void HikvisionCamera::onEsData(
const long real_handle,
const unsigned int packet_type,
unsigned char* buffer,
const unsigned int buffer_size,
const unsigned int packet_width,
const unsigned int packet_height,
const uint64_t source_timestamp,
const uint64_t source_frame_number,
const unsigned int source_frame_rate,
const unsigned int source_packet_mode)
{
if (!buffer || buffer_size == 0) {
return;
}
// Never take ctrl_mtx_ from an SDK callback. NET_DVR_StopRealPlay may wait
// for this callback while stop() owns ctrl_mtx_. Holding callback_mtx_
// through publication also preserves the ring's single-producer contract.
std::lock_guard callback_lock(callback_mtx_);
if (callback_preview_handle_ != real_handle || !stream_frame_buffer_) {
return;
}
if (packet_type == kHikvisionPacketFileHeader) {
const bool changed =
!has_es_stream_header_ ||
es_stream_header_.size() != buffer_size ||
!std::equal(es_stream_header_.begin(), es_stream_header_.end(), buffer);
if (changed) {
es_stream_header_.assign(buffer, buffer + buffer_size);
has_es_stream_header_ = true;
++codec_config_generation_;
if (callback_publishing_enabled_) {
awaiting_key_frame_ = true;
}
CMVR_LOG(INFO) << "[HikvisionCamera] received ES stream header, size=" << buffer_size;
}
return;
}
const bool is_video_packet =
packet_type == kHikvisionPacketVideoIFrame ||
packet_type == kHikvisionPacketVideoBFrame ||
packet_type == kHikvisionPacketVideoPFrame;
if (!is_video_packet) {
const int log_count = g_ignored_data_type_log_count.fetch_add(1);
if (log_count < 10) {
CMVR_LOG(INFO) << "[HikvisionCamera] ignore ES packet_type="
<< packet_type << ", size=" << buffer_size;
}
return;
}
const int video_log_count = g_es_video_log_count.fetch_add(1);
if (video_log_count < 10) {
CMVR_LOG(INFO) << "[HikvisionCamera] ES video packet_type="
<< packet_type << ", size=" << buffer_size
<< ", source_timestamp=" << source_timestamp
<< ", source_frame_number=" << source_frame_number
<< ", source_frame_rate=" << source_frame_rate
<< ", source_packet_mode=" << source_packet_mode;
}
if (!callback_publishing_enabled_) {
return;
}
const bool is_key_frame = packet_type == kHikvisionPacketVideoIFrame;
if (awaiting_key_frame_) {
if (!is_key_frame) {
return;
}
awaiting_key_frame_ = false;
CMVR_LOG(INFO) << "[HikvisionCamera] received first key frame after stream start";
}
pushEncodedFrame_(buffer,
buffer_size,
is_key_frame,
packet_width,
packet_height,
source_timestamp,
source_frame_number,
source_frame_rate,
source_packet_mode);
}
void HikvisionCamera::pushEncodedFrame_(
const unsigned char* buffer,
unsigned int buffer_size,
bool is_key_frame,
unsigned int packet_width,
unsigned int packet_height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode)
{
if (!buffer || buffer_size == 0 || !stream_frame_buffer_) {
return;
}
StreamFrameData frame_data;
const auto capture_monotonic = std::chrono::steady_clock::now();
const auto capture_utc = std::chrono::system_clock::now();
frame_data.rgbFrame.assign(buffer, buffer + buffer_size);
frame_data.codec = codec_;
frame_data.fps =
source_frame_rate >= 1U && source_frame_rate <= 1000U
? static_cast<int>(source_frame_rate)
: fps_;
frame_data.width = packet_width > 0 ? static_cast<int>(packet_width) : width_;
frame_data.height = packet_height > 0 ? static_cast<int>(packet_height) : height_;
frame_data.bKey = is_key_frame;
frame_data.stream_epoch = stream_epoch_;
frame_data.sequence = stream_sequence_++;
frame_data.source_timestamp = source_timestamp;
frame_data.source_frame_number = source_frame_number;
frame_data.capture_monotonic_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
capture_monotonic.time_since_epoch()).count();
frame_data.capture_utc_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
capture_utc.time_since_epoch()).count();
frame_data.pts = static_cast<int64_t>(frame_data.sequence);
frame_data.dts = frame_data.pts;
frame_data.time_base_num = 1;
frame_data.time_base_den = std::max(1, frame_data.fps);
frame_data.duration = 1;
frame_data.codec_config_generation = codec_config_generation_;
// Hikvision labels packet type 0 as a "file header", but the bundled SDK
// does not guarantee that it is a decoder-ready VPS/SPS/PPS blob. Keep it
// only for change detection until its format is verified on real hardware.
fillIntrinsics_(frame_data.intrinsics);
if (source_packet_mode > 1U) {
const int log_count = g_unexpected_packet_mode_log_count.fetch_add(1);
if (log_count < 5) {
CMVR_LOG(WARNING) << "[HikvisionCamera] unexpected ES source_packet_mode="
<< source_packet_mode
<< ", source_frame_number=" << source_frame_number;
}
}
stream_frame_buffer_->push(frame_data);
}
void HikvisionCamera::resetStreamState_()
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
es_stream_header_.clear();
has_es_stream_header_ = false;
}
bool HikvisionCamera::initSdk_()
{
if (sdk_acquired_) {
return true;
}
std::lock_guard<std::mutex> lock(g_sdk_mutex);
if (g_sdk_ref_count == 0) {
if (!NET_DVR_Init()) {
setError_(sdkError_("NET_DVR_Init"));
return false;
}
NET_DVR_SetConnectTime(3000, 3);
NET_DVR_SetReconnect(10000, TRUE);
NET_DVR_SetLogToFile(3, nullptr, TRUE);
g_sdk_initialized = true;
}
++g_sdk_ref_count;
sdk_acquired_ = true;
return g_sdk_initialized;
}
void HikvisionCamera::releaseSdk_()
{
if (!sdk_acquired_) {
return;
}
std::lock_guard<std::mutex> lock(g_sdk_mutex);
sdk_acquired_ = false;
if (g_sdk_ref_count > 0) {
--g_sdk_ref_count;
}
if (g_sdk_ref_count == 0 && g_sdk_initialized) {
NET_DVR_Cleanup();
g_sdk_initialized = false;
}
}
bool HikvisionCamera::login_()
{
NET_DVR_USER_LOGIN_INFO login_info{};
NET_DVR_DEVICEINFO_V40 device_info{};
copyCString(login_info.sDeviceAddress, sizeof(login_info.sDeviceAddress), ip_);
copyCString(login_info.sUserName, sizeof(login_info.sUserName), username_);
copyCString(login_info.sPassword, sizeof(login_info.sPassword), password_);
login_info.wPort = static_cast<WORD>(port_);
login_info.bUseAsynLogin = FALSE;
user_id_ = NET_DVR_Login_V40(&login_info, &device_info);
if (user_id_ < 0) {
setError_(sdkError_("NET_DVR_Login_V40"));
return false;
}
return true;
}
bool HikvisionCamera::startPreview_()
{
NET_DVR_PREVIEWINFO preview_info{};
preview_info.lChannel = channel_;
preview_info.dwStreamType = static_cast<DWORD>(stream_type_);
preview_info.dwLinkMode = static_cast<DWORD>(link_mode_);
preview_info.hPlayWnd = 0;
preview_info.bBlocked = 1;
preview_info.dwDisplayBufNum = 1;
preview_info.byReconnect = 1;
real_handle_ = NET_DVR_RealPlay_V40(user_id_, &preview_info, nullptr, nullptr);
if (real_handle_ < 0) {
setError_(sdkError_("NET_DVR_RealPlay_V40"));
return false;
}
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = static_cast<long>(real_handle_);
awaiting_key_frame_ = false;
}
if (!NET_DVR_SetESRealPlayCallBack(real_handle_, hikvisionEsRealPlayCallback, this)) {
setError_(sdkError_("NET_DVR_SetESRealPlayCallBack"));
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
}
NET_DVR_StopRealPlay(real_handle_);
real_handle_ = -1;
return false;
}
return true;
}
void HikvisionCamera::stopPreview_()
{
const int preview_handle = real_handle_;
{
// Invalidate before asking the SDK to stop. We intentionally release
// callback_mtx_ before NET_DVR_StopRealPlay because that function may
// wait for an SDK callback to return.
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
}
if (preview_handle >= 0) {
NET_DVR_StopRealPlay(preview_handle);
real_handle_ = -1;
}
}
void HikvisionCamera::fillIntrinsics_(Rs2Intrinsics& intrinsics) const
{
intrinsics.fx = camera_.fx();
intrinsics.fy = camera_.fy();
intrinsics.cx = camera_.cx() > 0.0f ? camera_.cx() : static_cast<float>(width_) * 0.5f;
intrinsics.cy = camera_.cy() > 0.0f ? camera_.cy() : static_cast<float>(height_) * 0.5f;
for (int i = 0; i < 5; ++i) {
intrinsics.coeffs[i] = i < camera_.coeffs_size() ? camera_.coeffs(i) : 0.0f;
}
}
void HikvisionCamera::setError_(const std::string& message)
{
state_.is_error = true;
state_.error_message = message;
CMVR_LOG(ERROR) << "[HikvisionCamera] " << message;
}
std::string HikvisionCamera::sdkError_(const std::string& action) const
{
return action + " failed, error_code=" + std::to_string(NET_DVR_GetLastError());
}
} // namespace cmvr::device

View File

@ -0,0 +1,425 @@
#include "../include/hikvision_camera.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "HCNetSDK.h"
namespace {
using EsDataCallback =
void(CALLBACK*)(LONG, NET_DVR_PACKET_INFO_EX*, void*);
constexpr DWORD kFileHeader = 0;
constexpr DWORD kVideoIFrame = 1;
constexpr DWORD kVideoPFrame = 3;
std::mutex g_fake_sdk_mutex;
EsDataCallback g_es_data_callback = nullptr;
void* g_es_data_user = nullptr;
LONG g_real_handle = 42;
std::atomic<int> g_stop_callback_count{0};
std::atomic<int> g_key_frame_request_count{0};
std::atomic<int> g_ptz_call_count{0};
std::atomic<DWORD> g_last_ptz_command{0};
std::atomic<DWORD> g_last_ptz_stop{0};
std::atomic<DWORD> g_last_ptz_speed{0};
void resetFakeSdk()
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = nullptr;
g_es_data_user = nullptr;
g_stop_callback_count = 0;
g_key_frame_request_count = 0;
g_ptz_call_count = 0;
g_last_ptz_command = 0;
g_last_ptz_stop = 0;
g_last_ptz_speed = 0;
}
void emitEsPacket(
const DWORD packet_type,
const LONG real_handle,
std::vector<BYTE> payload,
const WORD width = 640,
const WORD height = 360,
const DWORD timestamp_low = 0,
const DWORD timestamp_high = 0,
const DWORD frame_number = 0,
const DWORD frame_rate = 0,
const DWORD packet_mode = 0)
{
EsDataCallback callback = nullptr;
void* user = nullptr;
{
std::lock_guard lock(g_fake_sdk_mutex);
callback = g_es_data_callback;
user = g_es_data_user;
}
if (!callback) {
return;
}
NET_DVR_PACKET_INFO_EX packet{};
packet.wWidth = width;
packet.wHeight = height;
packet.dwTimeStamp = timestamp_low;
packet.dwTimeStampHigh = timestamp_high;
packet.dwFrameNum = frame_number;
packet.dwFrameRate = frame_rate;
packet.dwPacketType = packet_type;
packet.dwPacketSize = static_cast<DWORD>(payload.size());
packet.pPacketBuffer = payload.data();
packet.dwPacketMode = packet_mode;
callback(real_handle, &packet, user);
}
void emitIFrame(
const LONG real_handle = g_real_handle,
const DWORD timestamp_low = 0,
const DWORD timestamp_high = 0,
const DWORD frame_number = 0,
const DWORD frame_rate = 0,
const DWORD packet_mode = 0)
{
emitEsPacket(
kVideoIFrame,
real_handle,
{0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21},
640,
360,
timestamp_low,
timestamp_high,
frame_number,
frame_rate,
packet_mode);
}
void emitPFrame(const LONG real_handle = g_real_handle)
{
emitEsPacket(
kVideoPFrame,
real_handle,
{0x00, 0x00, 0x00, 0x01, 0x41, 0x9A, 0x20});
}
bool check(const bool condition, const char* expression, const int line)
{
if (condition) {
return true;
}
std::cerr << "CHECK failed at line " << line << ": " << expression << '\n';
return false;
}
#define CHECK_TRUE(expression) \
do { \
if (!check(static_cast<bool>(expression), #expression, __LINE__)) { \
return false; \
} \
} while (false)
cmvr::config::HikvisionCameraConfig makeConfig()
{
cmvr::config::HikvisionCameraConfig config;
config.set_id("hikvision_callback_test");
config.set_ip("127.0.0.1");
config.set_username("admin");
config.set_password("test-only");
config.set_port(8000);
config.set_channel(1);
config.set_stream_type(0);
config.set_link_mode(0);
config.set_width(1920);
config.set_height(1080);
config.set_fps(25);
config.set_codec("H264");
config.set_buffer_size(32);
return config;
}
bool testCallbackPublicationLifecycle()
{
resetFakeSdk();
cmvr::device::HikvisionCamera camera(makeConfig());
CHECK_TRUE(camera.init());
CHECK_TRUE(camera.start());
CHECK_TRUE(camera.startStreaming());
CHECK_TRUE(g_key_frame_request_count.load() == 1);
size_t cursor = 0;
cmvr::device::StreamFrameData frame;
// A new subscriber must not receive an undecodable inter frame, and a
// delayed callback from an older preview handle must also be rejected.
emitPFrame();
emitIFrame(g_real_handle - 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame(
g_real_handle,
0x89ABCDEFU,
0x01234567U,
42U,
30U,
1U);
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 1);
CHECK_TRUE(frame.sequence == 0);
CHECK_TRUE(frame.codec_config_generation == 1);
CHECK_TRUE(frame.bKey);
CHECK_TRUE(frame.width == 640);
CHECK_TRUE(frame.height == 360);
CHECK_TRUE(frame.source_timestamp == 0x0123456789ABCDEFULL);
CHECK_TRUE(frame.source_frame_number == 42U);
CHECK_TRUE(frame.fps == 30);
CHECK_TRUE(frame.time_base_den == 30);
CHECK_TRUE(camera.requestKeyFrame());
CHECK_TRUE(g_key_frame_request_count.load() == 2);
CHECK_TRUE(camera.controlPtz(
cmvr::device::PtzCommand::PanLeft, false, 99));
CHECK_TRUE(g_ptz_call_count.load() == 1);
CHECK_TRUE(g_last_ptz_command.load() == PAN_LEFT);
CHECK_TRUE(g_last_ptz_stop.load() == 0);
CHECK_TRUE(g_last_ptz_speed.load() == 7);
std::string json_response;
CHECK_TRUE(camera.executeJsonCommand(
R"({"command":"ptz","direction":"zoom_in","speed":3})",
json_response));
CHECK_TRUE(json_response.find(R"("success":true)") != std::string::npos);
CHECK_TRUE(g_ptz_call_count.load() == 2);
CHECK_TRUE(g_last_ptz_command.load() == ZOOM_IN);
CHECK_TRUE(g_last_ptz_speed.load() == 3);
// A changed SDK file header invalidates the descriptor generation, but is
// not exposed as decoder config until its vendor-specific format is known.
emitEsPacket(kFileHeader, g_real_handle, {0x01, 0x02, 0x03});
emitPFrame();
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame(
g_real_handle,
0x76543210U,
0xFEDCBA98U,
99U,
1001U,
0U);
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.sequence == 1);
CHECK_TRUE(frame.codec_config_generation == 2);
CHECK_TRUE(frame.codec_config.empty());
CHECK_TRUE(frame.source_timestamp == 0xFEDCBA9876543210ULL);
CHECK_TRUE(frame.source_frame_number == 99U);
CHECK_TRUE(frame.fps == 25);
CHECK_TRUE(frame.time_base_den == 25);
constexpr int kConcurrentCallbacks = 8;
std::vector<std::thread> producers;
producers.reserve(kConcurrentCallbacks);
for (int i = 0; i < kConcurrentCallbacks; ++i) {
producers.emplace_back(emitPFrame, g_real_handle);
}
for (auto& producer : producers) {
producer.join();
}
size_t latest_cursor = 0;
cmvr::device::StreamFrameData latest;
CHECK_TRUE(camera.getLatestEncodedFrame(latest, latest_cursor));
CHECK_TRUE(latest.stream_epoch == 1);
CHECK_TRUE(latest.sequence == static_cast<uint64_t>(kConcurrentCallbacks + 1));
CHECK_TRUE(latest_cursor == static_cast<size_t>(kConcurrentCallbacks + 2));
for (int sequence = 2; sequence <= kConcurrentCallbacks + 1; ++sequence) {
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 1);
CHECK_TRUE(frame.sequence == static_cast<uint64_t>(sequence));
}
camera.stopStreaming();
emitIFrame();
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
CHECK_TRUE(camera.startStreaming());
CHECK_TRUE(g_key_frame_request_count.load() == 3);
emitPFrame();
emitIFrame(g_real_handle - 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame();
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 2);
CHECK_TRUE(frame.sequence == 0);
CHECK_TRUE(frame.codec_config_generation == 3);
// The fake StopRealPlay invokes the SDK callback synchronously. stop()
// owns ctrl_mtx_ here, proving the callback neither takes that mutex nor
// publishes after the preview handle has been invalidated.
CHECK_TRUE(camera.stop());
CHECK_TRUE(g_stop_callback_count.load() == 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
return true;
}
} // namespace
extern "C" {
BOOL NET_DVR_Init()
{
return TRUE;
}
BOOL NET_DVR_Cleanup()
{
return TRUE;
}
BOOL NET_DVR_SetConnectTime(DWORD, DWORD)
{
return TRUE;
}
BOOL NET_DVR_SetReconnect(DWORD, BOOL)
{
return TRUE;
}
BOOL NET_DVR_SetLogToFile(DWORD, char*, BOOL)
{
return TRUE;
}
LONG NET_DVR_Login_V40(
LPNET_DVR_USER_LOGIN_INFO,
LPNET_DVR_DEVICEINFO_V40)
{
return 7;
}
BOOL NET_DVR_Logout(LONG)
{
return TRUE;
}
DWORD NET_DVR_GetLastError()
{
return 0;
}
LONG NET_DVR_RealPlay_V40(
LONG,
LPNET_DVR_PREVIEWINFO,
REALDATACALLBACK,
void*)
{
return g_real_handle;
}
BOOL NET_DVR_SetESRealPlayCallBack(
LONG,
EsDataCallback callback,
void* user)
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = callback;
g_es_data_user = user;
return TRUE;
}
BOOL NET_DVR_StopRealPlay(const LONG real_handle)
{
EsDataCallback callback = nullptr;
void* user = nullptr;
{
std::lock_guard lock(g_fake_sdk_mutex);
callback = g_es_data_callback;
user = g_es_data_user;
}
if (callback) {
std::vector<BYTE> idr{
0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21
};
NET_DVR_PACKET_INFO_EX packet{};
packet.wWidth = 640;
packet.wHeight = 360;
packet.dwPacketType = kVideoIFrame;
packet.dwPacketSize = static_cast<DWORD>(idr.size());
packet.pPacketBuffer = idr.data();
callback(real_handle, &packet, user);
++g_stop_callback_count;
}
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = nullptr;
g_es_data_user = nullptr;
}
return TRUE;
}
BOOL NET_DVR_SaveRealData(LONG, char*)
{
return TRUE;
}
BOOL NET_DVR_StopSaveRealData(LONG)
{
return TRUE;
}
BOOL NET_DVR_MakeKeyFrame(LONG, LONG)
{
++g_key_frame_request_count;
return TRUE;
}
BOOL NET_DVR_MakeKeyFrameSub(LONG, LONG)
{
++g_key_frame_request_count;
return TRUE;
}
BOOL NET_DVR_PTZControlWithSpeed_Other(
LONG,
LONG,
const DWORD command,
const DWORD stop,
const DWORD speed)
{
++g_ptz_call_count;
g_last_ptz_command = command;
g_last_ptz_stop = stop;
g_last_ptz_speed = speed;
return TRUE;
}
} // extern "C"
int main()
{
if (!testCallbackPublicationLifecycle()) {
return 1;
}
std::cout << "hikvision_camera_callback_test: PASS\n";
return 0;
}

View File

@ -30,6 +30,7 @@ namespace cmvr::device
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) override;
void getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void getRGBDImages(cv::Mat &color, cv::Mat &depth, Rs2Intrinsics& intrinsics) override;

View File

@ -50,6 +50,11 @@ bool MechmindCamera::init() {
return true;
}
void MechmindCamera::getState(CameraState& state) {
std::lock_guard lock(dev_mtx_);
state = state_;
}
bool MechmindCamera::start() {
try {
std::lock_guard lock(dev_mtx_);
@ -68,6 +73,7 @@ bool MechmindCamera::start() {
return true;
}
catch (const std::exception& e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (start): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -85,6 +91,7 @@ bool MechmindCamera::stop() {
return true;
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (stop): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -141,6 +148,7 @@ void MechmindCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) {
}
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getRGBImage): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -175,6 +183,7 @@ void MechmindCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) {
depth = cv::Mat(depthMap.height(), depthMap.width(), CV_32FC1, depthMap.data());
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getDepthImage): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -258,6 +267,7 @@ void MechmindCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics
textured_plc_to_rgbd_(textured_pcl, color, depth);
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getRGBDImages): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -269,12 +279,14 @@ void MechmindCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics
}
void MechmindCamera::startRecording(const std::string& video_path) {
std::lock_guard lock(dev_mtx_);
state_.is_error = true;
state_.error_message = "startRecording is not implemented";
CMVR_LOG(ERROR) << "[MechmindCamera] (startRecording): " << state_.error_message;
}
void MechmindCamera::stopRecording() {
std::lock_guard lock(dev_mtx_);
state_.is_error = true;
state_.error_message = "stopRecording is not implemented";
CMVR_LOG(ERROR) << "[MechmindCamera] (stopRecording): " << state_.error_message;

View File

@ -39,6 +39,7 @@ public:
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void setFetchRgbdFn(FetchRgbdFn fetch_rgbd_fn);
void setFovyDeg(double fovy_deg);

View File

@ -52,6 +52,12 @@ MujocoCamera::~MujocoCamera()
stop();
}
void MujocoCamera::getState(CameraState& state)
{
std::lock_guard<std::mutex> lock(mtx_);
state = state_;
}
bool MujocoCamera::init()
{
std::lock_guard<std::mutex> lock(mtx_);

View File

@ -24,6 +24,7 @@ namespace cmvr::device{
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) override;
void getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void getRGBDImages(cv::Mat &color, cv::Mat &depth, Rs2Intrinsics& intrinsics) override;

View File

@ -143,6 +143,12 @@ RealsenseCamera::~RealsenseCamera() {
}
}
void RealsenseCamera::getState(CameraState& state)
{
std::lock_guard lock(ctrl_mtx_);
state = state_;
}
bool RealsenseCamera::init() {
try {
std::lock_guard lock(ctrl_mtx_);

View File

@ -29,6 +29,7 @@ namespace cmvr::device {
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) override;
void getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void getRGBDImages(cv::Mat &color, cv::Mat &depth, Rs2Intrinsics& intrinsics) override;

View File

@ -57,6 +57,12 @@ UVCCamera::~UVCCamera() {
recording_thread_->join();
}
void UVCCamera::getState(CameraState& state)
{
std::lock_guard lock(ctrl_mtx_);
state = state_;
}
bool UVCCamera::init() {
std::lock_guard lock(ctrl_mtx_);
clear_error_();

View File

@ -1,7 +1,9 @@
#ifndef CMVR_ES_DEVICE_TYPES_H
#define CMVR_ES_DEVICE_TYPES_H
#include <cstdint>
#include <string>
#include <vector>
namespace cmvr::device {
@ -68,6 +70,87 @@ namespace cmvr::device {
std::string type_name;
};
// DeviceManager lifecycle and device-reported health are deliberately
// separate. A device can, for example, be READY from the manager's point
// of view while its backend has not implemented health reporting yet.
enum class ManagedDeviceState {
Unknown,
Disabled,
Initializing,
Registered,
Ready,
Running,
Stopped,
Error,
};
enum class DeviceHealthState {
Unknown,
Healthy,
Degraded,
Fault,
};
inline std::string toString(ManagedDeviceState state) {
switch (state) {
case ManagedDeviceState::Disabled:
return "Disabled";
case ManagedDeviceState::Initializing:
return "Initializing";
case ManagedDeviceState::Registered:
return "Registered";
case ManagedDeviceState::Ready:
return "Ready";
case ManagedDeviceState::Running:
return "Running";
case ManagedDeviceState::Stopped:
return "Stopped";
case ManagedDeviceState::Error:
return "Error";
case ManagedDeviceState::Unknown:
default:
return "Unknown";
}
}
inline std::string toString(DeviceHealthState state) {
switch (state) {
case DeviceHealthState::Healthy:
return "Healthy";
case DeviceHealthState::Degraded:
return "Degraded";
case DeviceHealthState::Fault:
return "Fault";
case DeviceHealthState::Unknown:
default:
return "Unknown";
}
}
struct DeviceHealthSnapshot {
DeviceHealthState state = DeviceHealthState::Unknown;
std::string error_message;
};
struct ManagedDeviceSnapshot {
std::string id;
DeviceKind kind = DeviceKind::Unknown;
std::string type_name;
bool enabled = false;
ManagedDeviceState state = ManagedDeviceState::Unknown;
DeviceHealthSnapshot health;
bool abnormal = false;
std::string error_message;
std::uint64_t status_updated_at_unix_ms = 0;
};
struct DeviceManagerSnapshot {
std::string name;
std::string version;
std::string description;
std::vector<ManagedDeviceSnapshot> devices;
};
} // namespace cmvr::device
#endif // CMVR_ES_DEVICE_TYPES_H

View File

@ -130,6 +130,24 @@ namespace cmvr::device {
virtual Status state() const = 0;
virtual std::string lastError() const = 0;
DeviceHealthSnapshot healthSnapshot() override {
const auto lifecycle = state();
const auto error = lastError();
DeviceHealthSnapshot health;
health.error_message = error;
if (lifecycle == Status::FAULT) {
health.state = DeviceHealthState::Fault;
} else if (!error.empty()) {
health.state = DeviceHealthState::Degraded;
} else if (lifecycle == Status::INITIALIZED ||
lifecycle == Status::STREAMING ||
lifecycle == Status::STOPPED) {
health.state = DeviceHealthState::Healthy;
}
return health;
}
virtual void getState(DexHandState& state) {
state = DexHandState{};
const auto lifecycle = this->state();

View File

@ -6,6 +6,9 @@
#define CMVR_ES_ABSTRACT_MICROPHONE_H
#pragma once
#include <chrono>
#include <cstddef>
#include "devices/abstract_device.h"
#include "cmvr/config/microphone_config/microphone_config.pb.h"
namespace cmvr::device{
@ -16,12 +19,41 @@ namespace cmvr::device{
DeviceKind kind() const noexcept override { return DeviceKind::Microphone; }
virtual void getState(MicrophoneState &state) {}
DeviceHealthSnapshot healthSnapshot() override {
MicrophoneState state{};
getState(state);
DeviceHealthSnapshot health;
health.error_message = state.error_message;
if (state.is_error) {
health.state = DeviceHealthState::Fault;
} else if (!state.error_message.empty()) {
health.state = DeviceHealthState::Degraded;
} else if (state.is_initialized) {
health.state = DeviceHealthState::Healthy;
}
return health;
}
virtual void startRecording(const std::string& outputFilePath) {}
virtual void stopRecording() {}
virtual void pause() {}
virtual void resume() {}
virtual void setVolume(const int volume) {}
virtual int getVolume() {return 0;}
virtual bool startStreaming() {return true;}
virtual void stopStreaming() {}
virtual void getEncodedFrame(AudioStreamFrameData& frame_data, size_t& index) {}
virtual bool waitEncodedFrame(
AudioStreamFrameData& frame_data,
size_t& index,
std::chrono::milliseconds timeout) {
(void)timeout;
getEncodedFrame(frame_data, index);
return !frame_data.data.empty();
}
virtual bool getLatestEncodedFrame(AudioStreamFrameData& frame_data, size_t& next_index) {
return false;
}
protected:
MicrophoneState state_{};

View File

@ -3,26 +3,30 @@
#ifndef CMVR_ES_FFMPEG_MICROPHONE_H
#define CMVR_ES_FFMPEG_MICROPHONE_H
#include <thread>
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <condition_variable>
#include "microphone/abstract_microphone.h"
#include <thread>
#include <boost/lockfree/spsc_queue.hpp>
#include "common/base/ring_buffer.h"
#include "microphone/abstract_microphone.h"
#include "speaker/ffmpeg_speaker/include/ffmpeg_ptr.h"
namespace cmvr::device {
class ffmpegMicroPhone final : public AbstractMicrophone {
public:
// 录制状态
enum class RecordingState {
STOPPED,
RECORDING,
PAUSED
};
public:
ffmpegMicroPhone(const config::FFMpegMicroPhoneConfig& cfg);
explicit ffmpegMicroPhone(const config::FFMpegMicroPhoneConfig& cfg);
~ffmpegMicroPhone() override;
std::string typeName() const override { return "FFMpegMicroPhone"; }
bool init() override;
@ -36,49 +40,64 @@ namespace cmvr::device {
void setVolume(const int volume) override;
int getVolume() override;
bool startStreaming() override;
void stopStreaming() override;
void getEncodedFrame(AudioStreamFrameData& frame_data, size_t& index) override;
bool waitEncodedFrame(AudioStreamFrameData& frame_data, size_t& index, std::chrono::milliseconds timeout) override;
bool getLatestEncodedFrame(AudioStreamFrameData& frame_data, size_t& next_index) override;
private:
// 音频采集和编码线程
void audioThread();
// 初始化FFmpeg采集和编码
bool initFFmpeg();
// 关闭FFmpeg资源
void closeFFmpeg();
bool startCapture_(bool write_file);
void stopCapture_();
bool recoverStreamingCapture_(int read_error);
static int interruptCallback_(void* opaque);
AudioStreamFormat currentStreamFormat_() const;
void pushEncodedPacket_(const AVPacket* packet);
private:
// FFmpeg采集相关
AVFormatContext* input_fmt_ctx;
AVCodecContext* input_codec_ctx;
AVFrame* input_frame;
// FFmpeg编码相关
AVFormatContext* output_fmt_ctx;
AVOutputFormat* output_fmt;
const AVOutputFormat* output_fmt;
AVStream* audio_st;
AVCodecContext* audio_codec_ctx;
AVCodec* audio_codec;
const AVCodec* audio_codec;
SwrContext* swr_ctx;
AVFrame* audio_frame;
std::string output_file;
std::string format_name;
std::atomic<bool> is_recording;
std::atomic<bool> is_capturing_{false};
std::atomic<bool> interrupt_requested_{false};
std::atomic<bool> write_output_file_{false};
std::atomic<bool> is_paused;
std::mutex pause_mutex;
std::condition_variable pause_cv;
// 音频参数
int sample_rate_;
int channels_;
int64_t next_pts;
private:
std::string input_device_;
std::shared_ptr<std::thread> audio_thread_;
// Serializes FFmpeg context start/stop and keeps a new shared lease from
// starting until the previous capture thread has exited and been joined.
std::mutex capture_mutex_;
std::mutex mtx_;
std::shared_ptr<SPMCRingBuffer<AudioStreamFrameData>> stream_frame_buffer_;
// Protected by mtx_. The audio thread snapshots these values while it
// assigns metadata to an encoded frame.
int stream_count_ = 0;
size_t buffer_size_ = 256;
uint64_t stream_epoch_ = 0;
uint64_t stream_sequence_ = 0;
uint32_t codec_config_generation_ = 0;
config::FFMpegMicroPhoneConfig config_;
};

View File

@ -21,6 +21,8 @@ namespace cmvr::device{
virtual int getVolume() const {return 0;}
virtual void pause() {}
virtual void resume() {}
virtual bool pushAudioFrame(const AudioStreamFrameData& frame_data) { return false; }
virtual void stopStreaming() {}
protected:
SpeakerState state_{};

View File

@ -114,291 +114,6 @@ enum class ImageType {
DEPTH, // 深度图像
GRAYSCALE // 灰度图像
};
class HEVCEncoder {
public:
HEVCEncoder(int width, int height, int fps, int bitrate,
const std::string& deviceName = "/dev/dri/renderD128",
ImageType imageType = ImageType::COLOR)
: width_(width), height_(height), fps_(fps), bitrate_(bitrate),
deviceName_(deviceName), imageType_(imageType) {
initialized_ = init();
}
bool encodeFrame(const cv::Mat& image) {
if (!initialized_ || !codec_ctx_ || !frame_) {
CMVR_LOG(ERROR) << "HEVC encoder is not initialized";
return false;
}
if (!prepareFrame(image)) {
CMVR_LOG(ERROR) << "Failed to prepare frame";
return false;
}
if (avcodec_send_frame(codec_ctx_.get(), frame_.get()) < 0) {
CMVR_LOG(ERROR) << "Error sending frame to encoder";
return false;
}
return true;
}
std::vector<uint8_t> receivePacket(bool& is_key) {
std::vector<uint8_t> packetData;
if (!initialized_ || !codec_ctx_) {
CMVR_LOG(ERROR) << "HEVC encoder is not initialized";
return packetData;
}
FFmpegPtr<AVPacket> packet(av_packet_alloc());
while (true)
{
int ret = avcodec_receive_packet(codec_ctx_.get(), packet.get());
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
CMVR_LOG(ERROR) << "Error during encoding";
break;
}
is_key = (packet->flags & AV_PKT_FLAG_KEY) != 0;
// 预留足够空间,避免多次内存分配
packetData.reserve(packetData.size() + packet->size);
// 复制数据包内容到输出向量
packetData.insert(packetData.end(),
packet->data,
packet->data + packet->size);
av_packet_unref(packet.get());
}
return packetData;
}
private:
bool init() {
// 1. 创建硬件设备上下文
AVBufferRef* hw_device_ctx = nullptr;
int ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI,
deviceName_.c_str(), nullptr, 0);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Failed to create VAAPI device context: " << errbuf;
return false;
}
hw_device_ctx_.reset(hw_device_ctx);
// 2. 查找编码器
const AVCodec* codec = avcodec_find_encoder_by_name("hevc_vaapi");
if (!codec) {
CMVR_LOG(ERROR) << "HEVC VAAPI encoder not found";
return false;
}
// 3. 设置编码器上下文
codec_ctx_.reset(avcodec_alloc_context3(codec));
codec_ctx_->width = width_;
codec_ctx_->height = height_;
codec_ctx_->time_base = {1, fps_};
codec_ctx_->pix_fmt = AV_PIX_FMT_VAAPI;
codec_ctx_->bit_rate = bitrate_;
codec_ctx_->gop_size = 1;
codec_ctx_->hw_device_ctx = av_buffer_ref(hw_device_ctx_.get());
// 4. 创建硬件帧上下文
hw_frames_ctx_.reset(av_hwframe_ctx_alloc(hw_device_ctx_.get()));
if (!hw_frames_ctx_) {
CMVR_LOG(ERROR) << "Failed to allocate hardware frame context";
return false;
}
AVHWFramesContext* frames_ctx = (AVHWFramesContext*)hw_frames_ctx_->data;
frames_ctx->format = AV_PIX_FMT_VAAPI;
frames_ctx->sw_format = AV_PIX_FMT_NV12;
frames_ctx->width = width_;
frames_ctx->height = height_;
frames_ctx->initial_pool_size = 20;
if (av_hwframe_ctx_init(hw_frames_ctx_.get()) < 0) {
CMVR_LOG(ERROR) << "Failed to initialize hardware frame context";
return false;
}
codec_ctx_->hw_frames_ctx = av_buffer_ref(hw_frames_ctx_.get());
// 5. 打开编码器
if (avcodec_open2(codec_ctx_.get(), codec, nullptr) < 0) {
CMVR_LOG(ERROR) << "Cannot open encoder";
return false;
}
// 6. 初始化硬件帧
frame_.reset(av_frame_alloc());
frame_->format = AV_PIX_FMT_VAAPI;
frame_->width = width_;
frame_->height = height_;
if (av_hwframe_get_buffer(hw_frames_ctx_.get(), frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Could not allocate hardware frame data";
return false;
}
// 7. 初始化软件帧
sw_frame_.reset(av_frame_alloc());
sw_frame_->width = width_;
sw_frame_->height = height_;
sw_frame_->format = (imageType_ == ImageType::DEPTH) ? AV_PIX_FMT_GRAY8 : AV_PIX_FMT_NV12;
if (av_frame_get_buffer(sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Could not allocate software frame buffer";
return false;
}
return true;
}
bool prepareFrame(const cv::Mat& image) {
if (imageType_ == ImageType::DEPTH) {
return prepareDepthFrame(image);
} else {
return prepareColorOrGrayscaleFrame(image);
}
}
bool prepareDepthFrame(const cv::Mat& depthImage) {
cv::Mat processedDepth;
if (depthImage.channels() != 1) {
CMVR_LOG(ERROR) << "Depth image must be single-channel";
return false;
}
if (depthImage.depth() != CV_8U) {
if (depthImage.depth() == CV_16U) {
depthImage.convertTo(processedDepth, CV_8U, 255.0 / 65535.0);
} else {
CMVR_LOG(ERROR) << "Unsupported depth image format";
return false;
}
} else {
processedDepth = depthImage.clone();
}
// 确保图像尺寸匹配
if (processedDepth.cols != width_ || processedDepth.rows != height_) {
CMVR_LOG(ERROR) << "Depth image size mismatch";
return false;
}
// 设置软件帧属性
sw_frame_->pts = frame_counter_++;
sw_frame_->format = AV_PIX_FMT_GRAY8;
av_frame_make_writable(sw_frame_.get());
// 使用av_image_fill_arrays处理行对齐
av_image_fill_arrays(sw_frame_->data, sw_frame_->linesize,
processedDepth.data,
AV_PIX_FMT_GRAY8,
width_, height_, 1);
// 上传到硬件帧
if (av_hwframe_transfer_data(frame_.get(), sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Error transferring depth data to hardware frame";
return false;
}
frame_->pts = sw_frame_->pts;
return true;
}
bool prepareColorOrGrayscaleFrame(const cv::Mat& image) {
if (image.cols != width_ || image.rows != height_) {
CMVR_LOG(ERROR) << "Image size mismatch: " << image.cols << "x" << image.rows
<< " vs " << width_ << "x" << height_;
return false;
}
// 1. 确定输入图像的像素格式OpenCV的Mat格式
AVPixelFormat src_pix_fmt;
if (image.channels() == 3) {
src_pix_fmt = AV_PIX_FMT_BGR24; // OpenCV默认是BGR格式3通道
} else if (image.channels() == 1) {
src_pix_fmt = AV_PIX_FMT_GRAY8; // 灰度图1通道
} else {
CMVR_LOG(ERROR) << "Unsupported channel count: " << image.channels();
return false;
}
// 2. 初始化格式转换上下文swscale
SwsContext* sws_ctx = sws_getContext(
width_, height_, src_pix_fmt, // 源宽高和格式
width_, height_, AV_PIX_FMT_NV12, // 目标宽高和格式NV12
SWS_BILINEAR, // 缩放算法(可根据需求调整)
nullptr, nullptr, nullptr
);
if (!sws_ctx) {
CMVR_LOG(ERROR) << "Failed to create sws context";
return false;
}
// 3. 配置源数据OpenCV的Mat
uint8_t* src_data[4] = {nullptr};
int src_linesize[4] = {0};
if (image.channels() == 3) {
src_data[0] = image.data; // BGR数据起始地址
src_linesize[0] = image.step; // 每行字节数含OpenCV的对齐填充
} else {
src_data[0] = image.data; // 灰度数据起始地址
src_linesize[0] = image.step; // 灰度图每行字节数
}
// 4. 配置目标数据NV12格式的AVFrame
av_frame_make_writable(sw_frame_.get());
sw_frame_->format = AV_PIX_FMT_NV12;
sw_frame_->width = width_;
sw_frame_->height = height_;
// 5. 执行格式转换BGR/GRAY -> NV12
int ret = sws_scale(
sws_ctx,
src_data, // 源图像数据
src_linesize, // 源图像每行字节数
0, // 起始行
height_, // 转换的行数
sw_frame_->data, // 目标图像数据sw_frame_的data指针
sw_frame_->linesize // 目标图像每行字节数
);
if (ret <= 0) {
CMVR_LOG(ERROR) << "sws_scale failed, ret=" << ret;
sws_freeContext(sws_ctx);
return false;
}
// 6. 释放转换上下文
sws_freeContext(sws_ctx);
// 7. 设置帧属性并上传到硬件
sw_frame_->pts = frame_counter_++;
if (av_hwframe_transfer_data(frame_.get(), sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Error transferring data to hardware frame";
return false;
}
frame_->pts = sw_frame_->pts;
return true;
}
int width_;
int height_;
int fps_;
int bitrate_;
int frame_counter_ = 0;
std::string deviceName_;
ImageType imageType_;
bool initialized_{false};
FFmpegPtr<AVCodecContext> codec_ctx_;
FFmpegPtr<AVFrame> frame_;
FFmpegPtr<AVFrame> sw_frame_;
FFmpegPtr<AVBufferRef> hw_device_ctx_;
FFmpegPtr<AVBufferRef> hw_frames_ctx_;
};
} // namespace ffmpeg
#endif //FFMPEG_PTR_H

View File

@ -3,9 +3,14 @@
#ifndef CMVR_ES_FFMPEG_SPEAKER_H
#define CMVR_ES_FFMPEG_SPEAKER_H
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "speaker/abstract_speaker.h"
#include <boost/lockfree/spsc_queue.hpp>
#include <pulse/simple.h>
@ -33,6 +38,8 @@ namespace cmvr::device {
void pause() override;
void resume() override;
void getState(SpeakerState& state) override;
bool pushAudioFrame(const AudioStreamFrameData& frame_data) override;
void stopStreaming() override;
void resetPlayState();
bool initPulseDevice_();
@ -40,6 +47,12 @@ namespace cmvr::device {
private:
void decode_audio_();
void play_audio_();
bool startStreamingPlayback_(const AudioStreamFrameData& frame_data);
bool pushPcmFrame_(const AudioStreamFrameData& frame_data);
bool decodeStreamFrame_(const AudioStreamFrameData& frame_data);
bool initStreamDecoder_(const AudioStreamFrameData& frame_data);
void releaseStreamDecoder_();
void releaseStreamDecoderUnlocked_();
pa_simple* pulse_simple_ = nullptr; // 修改PulseAudio 简单 API 句柄
pa_sample_spec sample_spec_{}; // 新增PulseAudio 采样规格
@ -51,6 +64,7 @@ namespace cmvr::device {
std::shared_ptr<std::thread> decode_thread_;
std::shared_ptr<std::thread> play_thread_;
std::mutex mtx_;
std::mutex stop_mtx_;
std::string audio_path_;
// Boost 单生产者单消费者无锁队列
@ -60,10 +74,20 @@ namespace cmvr::device {
mutable std::mutex mtx_pause_;
std::condition_variable cv_pause_;
std::atomic<bool> is_paused_{false};
std::atomic<bool> is_stopping_{false};
// 音频时钟同步
std::atomic<int64_t> audio_pts_{0};
std::chrono::time_point<std::chrono::steady_clock> playback_start_time_;
std::mutex stream_decode_mtx_;
AVCodecContext* stream_decoder_ctx_ = nullptr;
SwrContext* stream_swr_ctx_ = nullptr;
AVFrame* stream_decode_frame_ = nullptr;
AVPacket* stream_decode_packet_ = nullptr;
std::string stream_codec_;
int stream_input_sample_rate_ = 0;
int stream_input_channels_ = 0;
bool is_streaming_input_ = false;
config::FFMpegSpeakerConfig config_;
};

View File

@ -4,6 +4,9 @@
//
#include <filesystem>
#include <algorithm>
#include <climits>
#include <cstring>
#include "../include/ffmpeg_speaker.h"
@ -18,7 +21,7 @@ ffmpegSpeaker::ffmpegSpeaker(const config::FFMpegSpeakerConfig& cfg):config_(cfg
try {
id_ = config_.id();
memset(&sample_spec_, 0, sizeof(sample_spec_));
// state_.volume = config_.volume();
state_.volume = 100;
}
catch (const exception& e) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] ([ffmpegSpeaker]): Failed to parse config: " << e.what();
@ -28,6 +31,7 @@ ffmpegSpeaker::ffmpegSpeaker(const config::FFMpegSpeakerConfig& cfg):config_(cfg
ffmpegSpeaker::~ffmpegSpeaker() {
is_stopping_ = true;
{
std::lock_guard<std::mutex> lock(mtx_);
state_.is_running = false;
@ -76,6 +80,8 @@ bool ffmpegSpeaker::start() {
void ffmpegSpeaker::resetPlayState()
{
releaseStreamDecoder_();
if (pulse_simple_) {
pa_simple_flush(pulse_simple_, nullptr);
pa_simple_free(pulse_simple_);
@ -89,19 +95,24 @@ void ffmpegSpeaker::resetPlayState()
}
state_.is_initialized = false;
is_streaming_input_ = false;
audio_path_.clear();
CMVR_LOG(INFO) << "[ffmpegSpeaker] (resetPlayState): Success, id=" << id_;
}
bool ffmpegSpeaker::stop() {
std::lock_guard<std::mutex> stop_lock(stop_mtx_);
is_stopping_ = true;
{
lock_guard lock(mtx_);
state_.is_running = false;
state_.is_decoding = false;
state_.is_paused = false;
is_streaming_input_ = false;
}
cv_pause_.notify_all();
// 等待线程结束
if (decode_thread_ && decode_thread_->joinable()) {
decode_thread_->join();
@ -114,6 +125,7 @@ bool ffmpegSpeaker::stop() {
}
resetPlayState();
is_stopping_ = false;
return true;
}
@ -150,6 +162,7 @@ void ffmpegSpeaker::resume() {
return;
}
state_.is_paused = false;
cv_pause_.notify_all();
CMVR_LOG(INFO) << "[ffmpegSpeaker] (resume): Success, id=" << id_;
}
catch (const exception& e) {
@ -531,6 +544,310 @@ void ffmpegSpeaker::play_audio_() {
CMVR_LOG(INFO) << "[ffmpegSpeaker] Playback finished";
}
bool ffmpegSpeaker::pushAudioFrame(const AudioStreamFrameData& frame_data)
{
if (is_stopping_) {
return false;
}
if (frame_data.data.empty()) {
return true;
}
bool need_start = false;
bool need_restart = false;
{
std::lock_guard<std::mutex> lock(mtx_);
need_start = !state_.is_running;
need_restart = state_.is_running && is_streaming_input_ &&
(frame_data.sample_rate != sample_rate_ || frame_data.channels != channels_);
if (state_.is_running && !is_streaming_input_) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] cannot stream audio while file playback is active";
return false;
}
}
if (need_restart) {
stopStreaming();
need_start = true;
}
if (need_start && !startStreamingPlayback_(frame_data)) {
return false;
}
if (frame_data.format == AudioStreamFormat::PCM ||
frame_data.codec == "pcm_s16le" ||
frame_data.codec == "pcm") {
return pushPcmFrame_(frame_data);
}
return decodeStreamFrame_(frame_data);
}
void ffmpegSpeaker::stopStreaming()
{
std::lock_guard<std::mutex> stop_lock(stop_mtx_);
is_stopping_ = true;
{
std::lock_guard<std::mutex> lock(mtx_);
if (!is_streaming_input_) {
is_stopping_ = false;
return;
}
state_.is_decoding = false;
state_.is_paused = false;
is_streaming_input_ = false;
}
cv_pause_.notify_all();
if (play_thread_ && play_thread_->joinable()) {
play_thread_->join();
play_thread_.reset();
}
resetPlayState();
is_stopping_ = false;
}
bool ffmpegSpeaker::startStreamingPlayback_(const AudioStreamFrameData& frame_data)
{
std::lock_guard<std::mutex> lock(mtx_);
sample_rate_ = frame_data.sample_rate > 0 ? frame_data.sample_rate : 44100;
channels_ = frame_data.channels > 0 ? frame_data.channels : 2;
AudioFrame queued_frame;
while (audio_queue_.pop(queued_frame)) {
}
if (!initPulseDevice_()) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] PulseAudio device initialization failed";
return false;
}
state_.is_running = true;
state_.is_decoding = true;
state_.is_paused = false;
is_streaming_input_ = true;
play_thread_ = std::make_shared<std::thread>([this]() {
try {
this->play_audio_();
} catch (const std::exception& e) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Stream play thread exception: " << e.what();
std::lock_guard<std::mutex> lock(mtx_);
state_.is_running = false;
state_.is_decoding = false;
}
});
return true;
}
bool ffmpegSpeaker::pushPcmFrame_(const AudioStreamFrameData& frame_data)
{
const size_t sample_count = frame_data.data.size() / sizeof(int16_t);
if (sample_count == 0) {
return true;
}
auto buffer = std::make_shared<std::vector<int16_t>>(sample_count);
std::memcpy(buffer->data(), frame_data.data.data(), sample_count * sizeof(int16_t));
float volume_scale = 1.0f;
{
std::lock_guard<std::mutex> lock(mtx_);
volume_scale = static_cast<float>(state_.volume) / 100.0f;
}
for (int16_t& sample : *buffer) {
const float scaled = static_cast<float>(sample) * volume_scale;
sample = static_cast<int16_t>(std::clamp(scaled, -32768.f, 32767.f));
}
while (true) {
{
std::lock_guard<std::mutex> lock(mtx_);
if (!state_.is_running || is_stopping_) {
return false;
}
}
if (audio_queue_.push(buffer)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return true;
}
bool ffmpegSpeaker::decodeStreamFrame_(const AudioStreamFrameData& frame_data)
{
std::lock_guard<std::mutex> decode_lock(stream_decode_mtx_);
if (!stream_decoder_ctx_ ||
stream_codec_ != frame_data.codec ||
stream_input_sample_rate_ != frame_data.sample_rate ||
stream_input_channels_ != frame_data.channels) {
releaseStreamDecoderUnlocked_();
if (!initStreamDecoder_(frame_data)) {
return false;
}
}
av_packet_unref(stream_decode_packet_);
if (av_new_packet(stream_decode_packet_, static_cast<int>(frame_data.data.size())) < 0) {
return false;
}
std::memcpy(stream_decode_packet_->data, frame_data.data.data(), frame_data.data.size());
stream_decode_packet_->pts = frame_data.pts;
int ret = avcodec_send_packet(stream_decoder_ctx_, stream_decode_packet_);
av_packet_unref(stream_decode_packet_);
if (ret < 0) {
return false;
}
while ((ret = avcodec_receive_frame(stream_decoder_ctx_, stream_decode_frame_)) == 0) {
if (!stream_swr_ctx_) {
stream_swr_ctx_ = swr_alloc_set_opts(nullptr,
av_get_default_channel_layout(channels_),
AV_SAMPLE_FMT_S16,
sample_rate_,
av_get_default_channel_layout(stream_decode_frame_->channels),
static_cast<AVSampleFormat>(stream_decode_frame_->format),
stream_decode_frame_->sample_rate,
0,
nullptr);
if (!stream_swr_ctx_ || swr_init(stream_swr_ctx_) < 0) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Failed to init stream resampler";
av_frame_unref(stream_decode_frame_);
return false;
}
}
const int64_t max_samples64 = av_rescale_rnd(
swr_get_delay(stream_swr_ctx_, stream_decode_frame_->sample_rate) + stream_decode_frame_->nb_samples,
sample_rate_,
stream_decode_frame_->sample_rate,
AV_ROUND_UP);
const int max_samples = static_cast<int>(std::min<int64_t>(max_samples64, INT_MAX));
auto buffer = std::make_shared<std::vector<int16_t>>(max_samples * channels_);
uint8_t* out[] = {reinterpret_cast<uint8_t*>(buffer->data()), nullptr};
const int out_samples = swr_convert(stream_swr_ctx_,
out,
max_samples,
const_cast<const uint8_t**>(stream_decode_frame_->data),
stream_decode_frame_->nb_samples);
av_frame_unref(stream_decode_frame_);
if (out_samples <= 0) {
continue;
}
buffer->resize(out_samples * channels_);
float volume_scale = 1.0f;
{
std::lock_guard<std::mutex> lock(mtx_);
volume_scale = static_cast<float>(state_.volume) / 100.0f;
}
for (int16_t& sample : *buffer) {
const float scaled = static_cast<float>(sample) * volume_scale;
sample = static_cast<int16_t>(std::clamp(scaled, -32768.f, 32767.f));
}
while (true) {
{
std::lock_guard<std::mutex> lock(mtx_);
if (!state_.is_running || is_stopping_) {
return false;
}
}
if (audio_queue_.push(buffer)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
return ret == AVERROR(EAGAIN) || ret == AVERROR_EOF;
}
bool ffmpegSpeaker::initStreamDecoder_(const AudioStreamFrameData& frame_data)
{
const AVCodec* decoder = nullptr;
if (!frame_data.codec.empty()) {
decoder = avcodec_find_decoder_by_name(frame_data.codec.c_str());
}
if (!decoder) {
AVCodecID codec_id = AV_CODEC_ID_NONE;
if (frame_data.format == AudioStreamFormat::AAC) {
codec_id = AV_CODEC_ID_AAC;
} else if (frame_data.format == AudioStreamFormat::MP3) {
codec_id = AV_CODEC_ID_MP3;
}
if (codec_id != AV_CODEC_ID_NONE) {
decoder = avcodec_find_decoder(codec_id);
}
}
if (!decoder) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Unsupported stream audio codec: " << frame_data.codec;
return false;
}
stream_decoder_ctx_ = avcodec_alloc_context3(decoder);
if (!stream_decoder_ctx_) {
return false;
}
stream_decoder_ctx_->sample_rate = frame_data.sample_rate > 0 ? frame_data.sample_rate : sample_rate_;
stream_decoder_ctx_->channels = frame_data.channels > 0 ? frame_data.channels : channels_;
stream_decoder_ctx_->channel_layout =
stream_decoder_ctx_->channels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
if (avcodec_open2(stream_decoder_ctx_, decoder, nullptr) < 0) {
releaseStreamDecoderUnlocked_();
return false;
}
stream_decode_frame_ = av_frame_alloc();
stream_decode_packet_ = av_packet_alloc();
if (!stream_decode_frame_ || !stream_decode_packet_) {
releaseStreamDecoderUnlocked_();
return false;
}
stream_codec_ = frame_data.codec;
stream_input_sample_rate_ = frame_data.sample_rate;
stream_input_channels_ = frame_data.channels;
return true;
}
void ffmpegSpeaker::releaseStreamDecoder_()
{
std::lock_guard<std::mutex> decode_lock(stream_decode_mtx_);
releaseStreamDecoderUnlocked_();
}
void ffmpegSpeaker::releaseStreamDecoderUnlocked_()
{
if (stream_swr_ctx_) {
swr_free(&stream_swr_ctx_);
}
if (stream_decode_frame_) {
av_frame_free(&stream_decode_frame_);
}
if (stream_decode_packet_) {
av_packet_free(&stream_decode_packet_);
}
if (stream_decoder_ctx_) {
avcodec_free_context(&stream_decoder_ctx_);
}
stream_codec_.clear();
stream_input_sample_rate_ = 0;
stream_input_channels_ = 0;
}
bool ffmpegSpeaker::initPulseDevice_() {
// 验证参数
if (sample_rate_ <= 0 || channels_ <= 0) {

View File

@ -6,12 +6,14 @@
#define CMVR_ES_STATE_DEFINE_H
#include <atomic>
#include <cstdint>
#include <cmath>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <set>
#include <vector>
#include "common/types/agv/agv_types.h"
#include "common/types/geometry_types.h"
@ -34,10 +36,35 @@ namespace cmvr::device{
UNKNOWN
};
// ------------------------------------- AGV -------------------------------------
typedef struct{
enum class AudioStreamFormat {
PCM = 0,
MP3 = 1,
AAC = 2,
WAV = 3,
OPUS = 4,
UNKNOWN = 99
};
} AGVState;
struct AudioStreamFrameData {
std::vector<uint8_t> data;
int sample_rate = 44100;
int channels = 2;
AudioStreamFormat format = AudioStreamFormat::PCM;
std::string codec = "pcm_s16le";
int64_t pts = 0;
int64_t dts = 0;
int nb_samples = 0;
uint64_t stream_epoch = 0;
uint64_t sequence = 0;
int64_t capture_monotonic_ns = 0;
int64_t capture_utc_ns = 0;
int32_t time_base_num = 1;
int32_t time_base_den = 44100;
int64_t duration = 0;
bool discontinuity = false;
uint32_t codec_config_generation = 0;
std::vector<uint8_t> codec_config;
};
// ------------------------------------- robot -------------------------------------
typedef enum {

View File

@ -0,0 +1,89 @@
# Hardware 模块开发指南
`hardware/` 提供串口等可复用的底层传输能力。设备命令、寄存器含义和厂商协议应保留在 `devices/` 的具体后端中。
返回[项目总览](../../README.md)。
## 当前组件
| 文件 | 作用 |
| --- | --- |
| [`abstract_serial_transport.h`](include/abstract_serial_transport.h) | 可替换、可测试的串口传输接口 |
| [`posix_serial_transport.h`](include/posix_serial_transport.h) | Linux POSIX 串口实现,支持超时和任意正波特率 |
| [`serial_interface.h`](include/serial_interface.h) | 现有 RS485/CAN 寄存器辅助接口 |
| [`esp32_serial_port.h`](include/esp32_serial_port.h) | ESP32/舵机串口辅助实现 |
新设备优先依赖 `AbstractSerialTransport`,通过构造或 setter 注入实现。这样单元测试可以使用 fake transport不需要真实 `/dev/tty*`
## 底层与设备层边界
适合放在 `hardware/`
- 打开、关闭和查询传输状态;
- 有限超时的字节读写;
- 输入缓冲清理;
- OS 错误转换和统一错误文本;
- 与设备语义无关的帧读写工具。
应放在 `devices/<category>/<backend>/`
- 帧头、命令码、寄存器地址和校验规则;
- 重试、握手和设备状态机;
- 设备 ID、关节、传感器和错误码含义
- 后台接收线程及其与设备状态的同步。
## 新增传输实现
1. 实现 `AbstractSerialTransport` 或为新总线定义同样窄的抽象;
2. 析构函数必须可靠释放 fd、handle 和后台线程;
3. `open()` 失败后对象保持可再次打开;
4. `close()` 应幂等;
5. 所有阻塞读取必须支持有限 timeout
6. `lastError()` 返回最近一次操作的可诊断错误;
7. 更新 [`CMakeLists.txt`](CMakeLists.txt) 和 `cmvr_es::hardware` target
8. 使用 pseudo terminal、socketpair 或 fake 实现编写无设备测试。
## 并发与事务
`PosixSerialTransport` 使用互斥锁保护单次 `open/close/read/write`。这不等于一个“write command + read response”复合事务天然不会与其他线程交错。
具体驱动必须:
- 由一个 I/O 线程独占 transport或在驱动层为完整请求/响应加锁;
- 在关闭前先通知并 join 接收线程;
- 不持有设备状态锁执行长时间 I/O
- 超时后清理残留输入,避免下一请求读到旧响应;
- 不让回调在 transport 锁内反向调用设备;
- 为部分写、短读、EINTR、EAGAIN 和设备拔出定义行为。
## 权限和部署
Linux 设备通常需要:
- 将运行用户加入 `dialout`、`video` 或设备专用组;
- 使用 udev rule 固定权限和稳定设备别名;
- CAN 接口在服务启动前完成 `ip link` 配置;
- 不依赖开发机上的临时 `chmod 777`
- 在日志中记录逻辑设备名,不输出密码和完整敏感报文。
## 新增总线类型
新增 CAN、TCP、USB 等通用传输时:
1. 定义只描述字节/帧传输的抽象接口;
2. 将 Linux/SDK 实现与抽象分离;
3. 让设备后端依赖抽象而不是全局单例;
4. 提供 fake 实现和故障注入;
5. 记录线程安全级别和调用顺序;
6. 更新安装依赖及 systemd 权限说明。
## 测试清单
- [ ] 打开、重复打开、关闭和重复关闭
- [ ] 非法路径和非法参数
- [ ] 完整写、部分写和对端断开
- [ ] 精确长度读取、短读和超时
- [ ] EINTR/EAGAIN 重试
- [ ] I/O 期间关闭
- [ ] 多线程调用或显式拒绝并发
- [ ] 析构后没有 fd 和线程泄漏

252
cmvr-es/manager/README.md Normal file
View File

@ -0,0 +1,252 @@
# Manager 模块开发指南
`manager/` 负责组织设备、任务和协议无关媒体源。Manager 管理对象生命周期和调度,不实现厂商协议,也不实现平台 wire protocol。
返回[项目总览](../../README.md)。
## 当前管理器
| 目录 | CMake target | 职责 |
| --- | --- | --- |
| [`device_manager/`](device_manager/) | `cmvr_es::device_manager` | 按配置创建、初始化、查询和批量启停设备 |
| [`task_manager/`](task_manager/) | `cmvr_es::task_manager` | 创建任务、校验运行模式、统一启停和调度周期任务 |
| [`media_source_hub/`](media_source_hub/) | `cmvr_es::media_source_hub`、`cmvr_es::device_media_source_adapter` | 实时媒体源注册、按需启停和多消费者分发 |
`manager/` 当前没有聚合 `CMakeLists.txt`,三个子目录由 [`../CMakeLists.txt`](../CMakeLists.txt) 分别加入。新增 manager 时必须显式更新该文件。
## 进程生命周期
当前 [`../main.cpp`](../main.cpp) 的顺序是:
1. 加载根配置并设置全局配置根;
2. 构造 `DeviceManager`
3. 创建启用的设备并调用 `device->init()`
4. 注册 gRPC、QUIC TaskFactory creator
5. 构造 `TaskManager` 并调用启用任务的 `init()`
6. `startRunTask()` 启动任务和周期调度线程;
7. 收到 SIGINT/SIGTERM 后调用 `TaskManager::stopRunTask()`
重要限制:
- DeviceManager 构造不会自动调用全部设备的 `start()`
- 当前主退出路径没有调用 `DeviceManager::stop()`
- `SystemService/StopAll` 会调用 DeviceManager stop
- `DeviceManager::destroyInstance()` 不调用设备 stop销毁前必须先显式停止
- `TaskManager::destroyInstance()` 会调用 `stopRunTask()`,但 manager 未处于 running 状态时该调用会直接返回;
- DeviceManager 和 TaskManager 都是首次配置生效的单例,不支持热加载。
## DeviceManager
关键文件:
- [`device_manager/include/device_manager.h`](device_manager/include/device_manager.h)
- [`device_manager/include/device_factory.h`](device_manager/include/device_factory.h)
- [`device_manager/src/device_factory.cpp`](device_manager/src/device_factory.cpp)
- [`../devices/README.md`](../devices/README.md)
### 增加现有类别的新后端
例如增加一种摄像头:
1. 扩展类别配置 Proto 的 `oneof backend`
2. 实现对应抽象设备;
3. 修改 `CameraFactory::create()`
4. 增加 CMake target
5. 在摄像头集合配置中增加实例;
6. 在 DeviceManager 配置中增加相同 ID 的条目。
这种扩展通常不修改全局 `DeviceFactory`,因为 `DEVICE_TYPE_CAMERA` 已经路由到 CameraFactory。
### 增加全新设备类别
还需要:
1. 扩展 `DeviceConfigEntry::DeviceType`
2. 扩展 `DeviceKind` 和字符串映射;
3. 在 `DeviceFactory::DeviceFactory()` 注册 creator
4. 扩展 DeviceManager 的设备类型日志映射;
5. 为 `getDevice<NewAbstractType>()` 增加显式模板实例化;
6. 在 device_manager target 链接新设备 target
7. 如需平台访问,增加 API Proto、gRPC service 和系统设备类型映射。
`DeviceFactory::registerCreator()` 虽然是 public但 factory 是 DeviceManager 的私有成员,当前不是运行时插件入口。新增全局设备类别仍需修改 `device_factory.cpp`
### 容器和顺序约束
- 配置启用条目按配置顺序创建并 `init()`
- 有初始化依赖的设备应把依赖项写在使用方之前;
- `start()`、`stop()` 遍历 `unordered_map`,不能依赖启停顺序;
- 某个设备 start 返回 false 时,当前实现会继续启动其他设备且不会回滚;
- DeviceManager 会把 create/init/start/stop 和健康探针异常转换成设备状态错误,
但后端仍应把预期失败转换为返回值;
- collection 配置要求 manager entry ID 能找到同 ID 子配置;
- ID 重复、不匹配或配置路径为空都会拒绝创建;
- `registerDevice()` 不会替调用方调用 `init()`
- `devices_` 和状态表由读写锁保护,运行期动态注册不会与 heartbeat/query
形成数据竞争;但动态设备不会自动补执行已经发生的 `start()`,也暂不支持移除;
- `getDevice<T>()` 类型不匹配或 ID 不存在时返回空指针。
### 状态快照与 QUIC Heartbeat
`DeviceManager::snapshot()` 返回协议无关的纯值快照,包含 Manager
名称、版本、描述以及按设备 ID 排序的完整设备表。状态表与可用设备对象表分开:
- 禁用、创建失败、初始化失败的配置项仍会出现在快照中;
- `devices_` 仍只保存可供业务查询的已初始化对象,不改变现有 service 语义;
- 动态注册设备初始为 `REGISTERED`,不会冒充已经由 Manager 初始化或启动;
- create/init/start/stop 的已知错误会设置 Manager `ERROR``has_error`;设备
health 与生命周期独立,仍由探针报告 `HEALTHY/DEGRADED/FAULT/UNKNOWN`
- `healthSnapshot()` 未实现时为 `UNKNOWN`,不能解释为健康;
- heartbeat 只消费该内存快照,并在 QUIC 映射边界过滤 `enabled=false` 的设备;
禁止从发送线程同步访问厂商 SDK 或设备网络。
快照复制设备元数据和临时 `shared_ptr` 后立即释放容器锁,再调用设备的轻量健康
探针避免持锁进入设备代码。start/stop 同样在锁外调用设备,并由单独的生命周期
锁防止同一 Manager 并发启停。
不要在仍有 service/task 持有 manager 引用时调用 `destroyInstance()`
## TaskManager
完整任务实现指南见 [`../task/README.md`](../task/README.md)。
### PERIODIC_STEP
- `control_period_s` 必须是有限正数;
- 所有周期任务的 `step()` 串行运行在同一个 scheduler 线程;
- 慢 I/O 或长计算会延迟其他周期任务;
- 只有 state 为 `RUNNING` 的任务会执行 `step()`
- `step()` 内不能同步调用 `stopRunTask()` 等待当前 scheduler 自身。
### BLOCKING_SERVICE
- TaskManager 只调用 `init/start/stop`,不调用 `step()`
- `start()` 仍必须快速返回,由任务自己持有服务线程或事件循环;
- `stop()` 必须唤醒阻塞操作并 join 自己创建的线程。
### 启停语义
- manager 构造阶段只调用 task `init()`
- 任一 task `start()` 失败,会停止此前已启动的任务且不启动 scheduler
- 返回 false 的 task 必须自行清理本次 start 已经创建的部分资源TaskManager 不会再调用该失败 task 的 stop
- TaskManager 处于 running 状态时,停止流程先停止并 join scheduler再调用每个 task 的 `stop()`
- 未启动或已经停止时,`stopRunTask()` 会直接返回,不会再次逐个调用 task stop
- 任务存储在 `unordered_map`,启动和停止顺序不确定;
- 有顺序依赖的工作应放入同一协调任务或显式建模;
- task 返回后,其内部状态并发安全由具体实现负责。
## MediaSourceHub
关键文件:
- [`media_source_hub/include/media_source_hub.h`](media_source_hub/include/media_source_hub.h)
- [`media_source_hub/src/device_media_source_adapter.cpp`](media_source_hub/src/device_media_source_adapter.cpp)
- [`../common/media/media_frame.h`](../common/media/media_frame.h)
- [`../common/base/ring_buffer.h`](../common/base/ring_buffer.h)
当前默认轨道:
| 来源 | Track ID | 默认 ring capacity |
| --- | --- | ---: |
| 摄像头彩色流 | `<device_id>/video/color` | 64 |
| 麦克风主流 | `<device_id>/audio/main` | 256 |
当前 gRPC RGB/麦克风流和 QUIC 彩色/麦克风轨道使用 HubgRPC Depth/RGBD 仍直接读取设备帧。
### 注册新媒体源
1. 创建不可变 `TrackDescriptor`
2. 使用唯一且稳定的 `track_id`
3. 提供 `start(sink, cancelled)`
4. 提供同步 `stop()`
5. 视频源按需提供 `request_key_frame()`
6. 调用 `registerSource()`
7. 生产不可变 `MediaFramePtr`
8. 保证 frame descriptor ID 与注册轨道一致;
9. 在重启、编码变化和中断时更新 producer 元数据和 discontinuity由 adapter 更新 descriptor。
Descriptor 最低要求:
- `id`、`source_id` 非空;
- kind 不能是 `UNKNOWN`
- time base 分子和分母均大于零;
- generation 大于零。
### Source callback 生命周期
- 第一位 subscriber 触发一次 source start
- 多位 subscriber 共享同一个采集生产者;
- 最后一份 Subscription reset/析构时同步调用 source stop
- Hub 会先 close ring 并唤醒消费者stop callback 必须解除生产端阻塞、停止采集并 join producer形成同步发布屏障
- start 必须在阻塞阶段检查 cancellation
- cancellation predicate 必须快速、非阻塞,不能回调同一个 Hub
- active source 不能 unregister应先销毁全部 subscriptions
- request-key-frame 与 stop 串行化,只在 source 运行时调用;
- adapter 的最后 lease 只停止媒体 streaming不等于设备级 stop。
永久不响应 cancellation 的 start 虽会被隔离以避免 use-after-free仍可能泄漏线程和外部资源不能依赖该隔离替代正确实现。
### Subscription 与广播环形队列
- 每个消费线程单独调用一次 `subscribe()`
- Subscription move-only 且为单消费者对象;
- 同一 Subscription 不能跨线程并发 read/reset
- 满队列覆盖最旧帧,不阻塞生产者;
- `dropped_since_last_read` 是该消费者实际错过的帧数;
- ring 全局 dropped count 与 cursor dropped count 含义不同;
- `NEXT_PUBLISHED` 忽略已有帧;
- `OLDEST_AVAILABLE` 从最旧保留帧开始;
- `LATEST_AVAILABLE` 读取当前最新帧;
- `discardPendingIfExceeds(limit)` 在积压超过阈值时原子地把该消费者游标推进到
当前发布末尾,并把主动丢弃数量计入下一次读取的 `dropped_since_last_read`
- `discardPendingIfExceeds()``read()` 一样只能由该 Subscription 的单一消费
线程调用,不能用它替代 Subscription 的线程所有权约束;
- source 重启会 reset ring、提升 `BroadcastFrameRing` 内部 generation并把 ring 的 `ReadResult.sequence` 从 0 重新计数;
- close 唤醒等待者并拒绝新 publish。
ring generation 不等于 `TrackDescriptor::generation`ring 的 `ReadResult.sequence` 也不等于 `MediaFrame::sequence`。Descriptor generation 和媒体帧 sequence 仍由 producer/adapter 维护。
协议消费者需要分别处理 ring `generation_changed`、descriptor generation、消费者 drop 和 frame discontinuity任一不连续发生时都应传播状态帧间编码还应请求关键帧。主动丢弃 H.264/H.265 积压后不得直接发送 P/B 帧,必须等新的关键帧恢复。
## 新增第四种 Manager
1. 先确认能力不是 DeviceManager、TaskManager 或 MediaSourceHub 的子职责;
2. 定义所有权、初始化、start/stop 和线程模型;
3. 避免新增无必要的全局单例;
4. 新建独立目录、头文件、实现和 CMake target
5. 在 [`../CMakeLists.txt`](../CMakeLists.txt) 增加子目录;
6. 只在 `main.cpp` 或明确的上层 owner 组装;
7. 增加无设备生命周期、失败回滚和并发测试。
## 测试
MediaSourceHub
```bash
cmake --build build --target media_source_hub_test
ctest \
--test-dir build \
-R '^media_source_hub_test$' \
--output-on-failure
```
DeviceManager 已有 `device_manager_snapshot_test`,覆盖全量状态表、生命周期失败、
异常限长、排序和值快照并发读取。TaskManager 仍缺少独立 CTest。修改其行为时
至少补充:
- fake device 创建、ID 冲突和 init/start/stop 失败;
- 设备依赖顺序;
- 周期任务调度和慢 step
- service task 启停、重复 stop 和启动失败回滚;
- 并发查询、取消和 shutdown。
## 提交检查
- [ ] manager 没有包含厂商 wire protocol
- [ ] 初始化、start、stop 和 destroy 语义明确
- [ ] 不依赖 unordered_map 的遍历顺序
- [ ] 动态集合修改不会与查询并发
- [ ] Media Subscription 每线程独立
- [ ] stop 能唤醒并 join 所有工作线程
- [ ] 新 manager 已加入 `cmvr-es/CMakeLists.txt`
- [ ] 无设备失败路径有测试

View File

@ -22,3 +22,29 @@ target_link_libraries(device_manager PRIVATE
add_library(cmvr_es::device_manager ALIAS device_manager)
install(TARGETS device_manager LIBRARY DESTINATION lib)
if(BUILD_TESTING)
add_executable(device_manager_snapshot_test
tests/device_manager_snapshot_test.cpp
)
target_link_libraries(device_manager_snapshot_test PRIVATE
cmvr_es::device_manager
)
add_test(
NAME device_manager_snapshot_test
COMMAND device_manager_snapshot_test
)
set_tests_properties(device_manager_snapshot_test PROPERTIES TIMEOUT 20)
if(UNIX AND NOT APPLE)
get_property(_device_manager_test_library_dirs
DIRECTORY PROPERTY LINK_DIRECTORIES)
list(PREPEND _device_manager_test_library_dirs
"${CMAKE_BINARY_DIR}/cmvr_compiler_runtime")
list(JOIN _device_manager_test_library_dirs ":"
_device_manager_test_library_path)
set_tests_properties(device_manager_snapshot_test PROPERTIES
ENVIRONMENT
"LD_LIBRARY_PATH=${_device_manager_test_library_path}"
)
endif()
endif()

View File

@ -7,9 +7,12 @@
#include <list>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "device_factory.h"
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
@ -31,6 +34,8 @@ namespace cmvr::device {
void getDeviceList(std::list<std::pair<std::string, std::string>> &device_list);
void registerDevice(const std::shared_ptr<AbstractDevice>& device);
void registerDevice(const std::string& device_id, const std::shared_ptr<AbstractDevice>& device);
std::shared_ptr<AbstractDevice> getDeviceBase(const std::string& device_id);
DeviceManagerSnapshot snapshot() const;
std::string version() const;
std::string name() const;
@ -44,7 +49,10 @@ namespace cmvr::device {
static std::shared_ptr<DeviceManager> instance_;
config::DeviceManagerConfig cfg_;
mutable std::shared_mutex devices_mutex_;
std::mutex lifecycle_mutex_;
std::unordered_map<std::string, DeviceRecord> devices_;
std::unordered_map<std::string, ManagedDeviceSnapshot> device_statuses_;
std::unique_ptr<DeviceFactory> dev_factory_;
explicit DeviceManager(const config::DeviceManagerConfig &cfg);
@ -52,6 +60,8 @@ namespace cmvr::device {
void pre_scan_robot_arm_dependencies_() const;
void init_devices_();
void configure_mujoco_viewer_pip_();
void start_devices_();
void stop_devices_();
};
} // cmvr

View File

@ -5,6 +5,9 @@
#include "../include/device_manager.h"
#include <algorithm>
#include <exception>
#include "devices/agv/abstract_agv.h"
#include "devices/arm/robot_arm.h"
#include "devices/battery/abstract_battery.h"
@ -203,6 +206,17 @@ std::shared_ptr<DeviceType> DeviceManager::getDevice(const std::string& device_i
return ptr;
}
std::shared_ptr<AbstractDevice> DeviceManager::getDeviceBase(const std::string& device_id)
{
std::shared_lock lock(devices_mutex_);
const auto it = devices_.find(device_id);
if (it == devices_.end() || !it->second.device) {
CMVR_LOG(WARNING) << "[DeviceManager]: Device ID " << device_id << " not found.";
return nullptr;
}
return it->second.device;
}
void DeviceManager::getDeviceList(std::list<std::pair<std::string, std::string>>& device_list){
device_list.clear();
for (const auto& [device_id, record] : devices_) {
@ -246,6 +260,63 @@ void DeviceManager::registerDevice(const std::string& device_id,
<< ", kind=" << toString(device->kind());
}
DeviceManagerSnapshot DeviceManager::snapshot() const
{
struct SnapshotSource {
ManagedDeviceSnapshot status;
std::shared_ptr<AbstractDevice> device;
};
std::vector<SnapshotSource> sources;
{
std::shared_lock lock(devices_mutex_);
sources.reserve(devices_.size());
for (const auto& [id, record] : devices_) {
SnapshotSource source;
source.status.id = id;
source.status.kind = record.kind;
source.status.type_name = record.type_name;
source.status.enabled = true;
source.status.state = ManagedDeviceState::Ready;
source.device = record.device;
sources.push_back(std::move(source));
}
}
DeviceManagerSnapshot result;
result.name = name();
result.version = version();
result.description = description();
result.devices.reserve(sources.size());
for (auto& source : sources) {
if (source.device) {
try {
source.status.health = source.device->healthSnapshot();
} catch (const std::exception& error) {
source.status.health.state = DeviceHealthState::Fault;
source.status.health.error_message = error.what();
} catch (...) {
source.status.health.state = DeviceHealthState::Fault;
source.status.health.error_message =
"device health snapshot threw an unknown exception";
}
}
source.status.abnormal =
source.status.health.state == DeviceHealthState::Degraded ||
source.status.health.state == DeviceHealthState::Fault;
source.status.error_message = source.status.health.error_message;
result.devices.push_back(std::move(source.status));
}
std::sort(result.devices.begin(), result.devices.end(),
[](const ManagedDeviceSnapshot& lhs,
const ManagedDeviceSnapshot& rhs) {
return lhs.id < rhs.id;
});
return result;
}
std::string DeviceManager::version() const {
return cfg_.version().empty() ? "1.0" : cfg_.version();
}

View File

@ -0,0 +1,379 @@
#include "manager/device_manager/include/device_manager.h"
#include "devices/camera/abstract_camera.h"
#include "devices/dexhand/abstract_dexhand.h"
#include "devices/microphone/abstract_microphone.h"
#include <atomic>
#include <cstddef>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
namespace {
#define CHECK_TRUE(condition) \
do { \
if (!(condition)) { \
return false; \
} \
} while (false)
using cmvr::device::AbstractDevice;
using cmvr::device::DeviceHealthSnapshot;
using cmvr::device::DeviceHealthState;
using cmvr::device::DeviceKind;
using cmvr::device::DeviceManager;
using cmvr::device::DeviceManagerSnapshot;
using cmvr::device::ManagedDeviceSnapshot;
using cmvr::device::ManagedDeviceState;
class MemoryCamera final : public cmvr::device::AbstractCamera {
public:
std::string typeName() const override { return "MemoryCamera"; }
void getState(cmvr::device::CameraState& output) override
{
output = state;
}
cmvr::device::CameraState state{};
};
class MemoryMicrophone final : public cmvr::device::AbstractMicrophone {
public:
std::string typeName() const override { return "MemoryMicrophone"; }
void getState(cmvr::device::MicrophoneState& output) override
{
output = state;
}
cmvr::device::MicrophoneState state{};
};
class MemoryDexHand final : public cmvr::device::AbstractDexHand {
public:
std::string typeName() const override { return "MemoryDexHand"; }
Status state() const override { return lifecycle; }
std::string lastError() const override { return error; }
void setAngles(const std::vector<int>&) override {}
void setTactilePollingRegions(
const std::vector<TactileRegionKey>&) override {}
std::vector<TactileRegionData> getSensorData() override { return {}; }
TactileRegionData getSensorData(FingerType, TactileRegion) override
{
return {};
}
ResultantForce getResultantForce(FingerType, TactileRegion) override
{
return {};
}
Status lifecycle{Status::CREATED};
std::string error;
};
class FakeDevice final : public AbstractDevice {
public:
explicit FakeDevice(std::string id,
DeviceKind kind = DeviceKind::Camera)
: AbstractDevice(std::move(id)), kind_(kind)
{
}
DeviceKind kind() const noexcept override { return kind_; }
std::string typeName() const override { return "FakeDevice"; }
bool start() override
{
++start_calls;
if (throw_on_start) {
throw std::runtime_error(std::string(700, 's'));
}
return start_result;
}
bool stop() override
{
++stop_calls;
if (throw_on_stop) {
throw std::runtime_error(std::string(700, 't'));
}
return stop_result;
}
DeviceHealthSnapshot healthSnapshot() override
{
++health_calls;
if (throw_on_health) {
throw std::runtime_error(std::string(700, 'h'));
}
return health;
}
DeviceKind kind_;
bool start_result{true};
bool stop_result{true};
bool throw_on_start{false};
bool throw_on_stop{false};
bool throw_on_health{false};
DeviceHealthSnapshot health{DeviceHealthState::Healthy, {}};
std::atomic<int> start_calls{0};
std::atomic<int> stop_calls{0};
std::atomic<int> health_calls{0};
};
const ManagedDeviceSnapshot* findDevice(const DeviceManagerSnapshot& snapshot,
const std::string& id)
{
for (const auto& device : snapshot.devices) {
if (device.id == id) {
return &device;
}
}
return nullptr;
}
bool isSorted(const DeviceManagerSnapshot& snapshot)
{
for (std::size_t i = 1; i < snapshot.devices.size(); ++i) {
if (snapshot.devices[i].id < snapshot.devices[i - 1].id) {
return false;
}
}
return true;
}
bool testCategoryHealthAdapters()
{
MemoryCamera camera;
CHECK_TRUE(camera.healthSnapshot().state ==
DeviceHealthState::Unknown);
camera.state.is_initialized = true;
CHECK_TRUE(camera.healthSnapshot().state ==
DeviceHealthState::Healthy);
camera.state.error_message = "camera warning";
CHECK_TRUE(camera.healthSnapshot().state ==
DeviceHealthState::Degraded);
camera.state.is_error = true;
CHECK_TRUE(camera.healthSnapshot().state ==
DeviceHealthState::Fault);
MemoryMicrophone microphone;
microphone.state.is_initialized = true;
CHECK_TRUE(microphone.healthSnapshot().state ==
DeviceHealthState::Healthy);
microphone.state.is_error = true;
microphone.state.error_message = "microphone fault";
const auto microphone_health = microphone.healthSnapshot();
CHECK_TRUE(microphone_health.state == DeviceHealthState::Fault);
CHECK_TRUE(microphone_health.error_message == "microphone fault");
MemoryDexHand dexhand;
CHECK_TRUE(dexhand.healthSnapshot().state ==
DeviceHealthState::Unknown);
dexhand.lifecycle = MemoryDexHand::Status::INITIALIZED;
CHECK_TRUE(dexhand.healthSnapshot().state ==
DeviceHealthState::Healthy);
dexhand.error = "temporary warning";
CHECK_TRUE(dexhand.healthSnapshot().state ==
DeviceHealthState::Degraded);
dexhand.lifecycle = MemoryDexHand::Status::FAULT;
CHECK_TRUE(dexhand.healthSnapshot().state ==
DeviceHealthState::Fault);
return true;
}
bool testConfiguredAndDynamicSnapshots()
{
cmvr::config::DeviceManagerConfig config;
config.set_name("snapshot-test");
config.set_version("9.1");
config.set_description("device manager snapshot test");
auto* disabled = config.add_devices();
disabled->set_id("disabled_camera");
disabled->set_type(
cmvr::config::DeviceConfigEntry::DEVICE_TYPE_CAMERA);
disabled->set_enable(false);
auto* broken = config.add_devices();
broken->set_id("broken_device");
broken->set_type(
cmvr::config::DeviceConfigEntry::DEVICE_TYPE_UNKNOWN);
broken->set_enable(true);
auto* duplicate_disabled = config.add_devices();
duplicate_disabled->set_id("duplicate_device");
duplicate_disabled->set_type(
cmvr::config::DeviceConfigEntry::DEVICE_TYPE_CAMERA);
duplicate_disabled->set_enable(false);
auto* duplicate_enabled = config.add_devices();
duplicate_enabled->set_id("duplicate_device");
duplicate_enabled->set_type(
cmvr::config::DeviceConfigEntry::DEVICE_TYPE_CAMERA);
duplicate_enabled->set_enable(true);
auto& manager = DeviceManager::getInstance(config);
auto configured = manager.snapshot();
CHECK_TRUE(configured.name == "snapshot-test");
CHECK_TRUE(configured.version == "9.1");
CHECK_TRUE(configured.description == "device manager snapshot test");
CHECK_TRUE(configured.devices.size() == 3);
CHECK_TRUE(isSorted(configured));
const auto* disabled_status =
findDevice(configured, "disabled_camera");
CHECK_TRUE(disabled_status != nullptr);
CHECK_TRUE(!disabled_status->enabled);
CHECK_TRUE(disabled_status->kind == DeviceKind::Camera);
CHECK_TRUE(disabled_status->type_name == "Camera");
CHECK_TRUE(disabled_status->state == ManagedDeviceState::Disabled);
CHECK_TRUE(disabled_status->health.state ==
DeviceHealthState::Unknown);
CHECK_TRUE(!disabled_status->abnormal);
CHECK_TRUE(disabled_status->status_updated_at_unix_ms != 0);
const auto* broken_status = findDevice(configured, "broken_device");
CHECK_TRUE(broken_status != nullptr);
CHECK_TRUE(broken_status->enabled);
CHECK_TRUE(broken_status->state == ManagedDeviceState::Error);
CHECK_TRUE(broken_status->health.state == DeviceHealthState::Unknown);
CHECK_TRUE(broken_status->abnormal);
CHECK_TRUE(!broken_status->error_message.empty());
CHECK_TRUE(broken_status->error_message.size() <= 512);
const auto* duplicate_status =
findDevice(configured, "duplicate_device");
CHECK_TRUE(duplicate_status != nullptr);
CHECK_TRUE(duplicate_status->enabled);
CHECK_TRUE(duplicate_status->state == ManagedDeviceState::Error);
CHECK_TRUE(duplicate_status->abnormal);
CHECK_TRUE(duplicate_status->error_message ==
"duplicate configured device id: duplicate_device");
auto healthy = std::make_shared<FakeDevice>("z_healthy");
auto degraded = std::make_shared<FakeDevice>("a_degraded");
degraded->health = {
DeviceHealthState::Degraded, std::string(700, 'd')};
auto start_fail = std::make_shared<FakeDevice>("m_start_fail");
start_fail->start_result = false;
auto stop_fail = std::make_shared<FakeDevice>("n_stop_fail");
stop_fail->stop_result = false;
auto health_throw = std::make_shared<FakeDevice>("b_health_throw");
health_throw->throw_on_health = true;
manager.registerDevice(healthy);
manager.registerDevice(degraded);
manager.registerDevice(start_fail);
manager.registerDevice(stop_fail);
manager.registerDevice(health_throw);
// Duplicate registration must retain the original object and status.
manager.registerDevice(
std::make_shared<FakeDevice>("z_healthy", DeviceKind::Speaker));
CHECK_TRUE(manager.getDeviceBase("z_healthy") == healthy);
const auto registered = manager.snapshot();
CHECK_TRUE(isSorted(registered));
const auto* healthy_registered =
findDevice(registered, "z_healthy");
CHECK_TRUE(healthy_registered != nullptr);
CHECK_TRUE(healthy_registered->state ==
ManagedDeviceState::Registered);
CHECK_TRUE(healthy_registered->health.state ==
DeviceHealthState::Healthy);
CHECK_TRUE(!healthy_registered->abnormal);
const auto* degraded_registered =
findDevice(registered, "a_degraded");
CHECK_TRUE(degraded_registered != nullptr);
CHECK_TRUE(degraded_registered->abnormal);
CHECK_TRUE(degraded_registered->health.state ==
DeviceHealthState::Degraded);
CHECK_TRUE(degraded_registered->health.error_message.size() == 512);
CHECK_TRUE(degraded_registered->error_message.size() == 512);
const auto* thrown_health =
findDevice(registered, "b_health_throw");
CHECK_TRUE(thrown_health != nullptr);
CHECK_TRUE(thrown_health->abnormal);
CHECK_TRUE(thrown_health->health.state ==
DeviceHealthState::Fault);
CHECK_TRUE(thrown_health->health.error_message.size() <= 512);
CHECK_TRUE(thrown_health->error_message.size() <= 512);
manager.start();
const auto running = manager.snapshot();
CHECK_TRUE(findDevice(running, "z_healthy")->state ==
ManagedDeviceState::Running);
CHECK_TRUE(findDevice(running, "m_start_fail")->state ==
ManagedDeviceState::Error);
CHECK_TRUE(findDevice(running, "m_start_fail")->abnormal);
CHECK_TRUE(findDevice(running, "m_start_fail")->health.state ==
DeviceHealthState::Healthy);
CHECK_TRUE(healthy->start_calls.load() == 1);
// The earlier value snapshot remains independent from manager mutations.
CHECK_TRUE(healthy_registered->state ==
ManagedDeviceState::Registered);
manager.stop();
const auto stopped = manager.snapshot();
CHECK_TRUE(findDevice(stopped, "z_healthy")->state ==
ManagedDeviceState::Stopped);
CHECK_TRUE(findDevice(stopped, "n_stop_fail")->state ==
ManagedDeviceState::Error);
CHECK_TRUE(findDevice(stopped, "n_stop_fail")->abnormal);
CHECK_TRUE(healthy->stop_calls.load() == 1);
return true;
}
bool testConcurrentSnapshotAndRegistration()
{
auto& manager = DeviceManager::getInstance();
std::atomic<bool> done{false};
std::atomic<bool> reader_ok{true};
std::thread reader([&] {
while (!done.load(std::memory_order_acquire)) {
const auto current = manager.snapshot();
if (!isSorted(current)) {
reader_ok.store(false, std::memory_order_release);
return;
}
}
});
for (int i = 0; i < 32; ++i) {
manager.registerDevice(
std::make_shared<FakeDevice>(
"concurrent_" + std::to_string(i)));
}
done.store(true, std::memory_order_release);
reader.join();
CHECK_TRUE(reader_ok.load(std::memory_order_acquire));
const auto final_snapshot = manager.snapshot();
CHECK_TRUE(isSorted(final_snapshot));
for (int i = 0; i < 32; ++i) {
CHECK_TRUE(
findDevice(final_snapshot,
"concurrent_" + std::to_string(i)) != nullptr);
}
return true;
}
} // namespace
int main()
{
DeviceManager::destroyInstance();
const bool success =
testCategoryHealthAdapters() &&
testConfiguredAndDynamicSnapshots() &&
testConcurrentSnapshotAndRegistration();
DeviceManager::destroyInstance();
return success ? 0 : 1;
}

View File

@ -0,0 +1,61 @@
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
cmake_minimum_required(VERSION 3.22)
project(cmvr_media_source_hub LANGUAGES CXX)
enable_testing()
endif()
add_library(media_source_hub STATIC
src/media_source_hub.cpp
)
target_compile_features(media_source_hub PUBLIC cxx_std_17)
target_include_directories(media_source_hub
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
add_library(cmvr_es::media_source_hub ALIAS media_source_hub)
if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_library(device_media_source_adapter STATIC
src/device_media_source_adapter.cpp
)
target_compile_features(device_media_source_adapter PUBLIC cxx_std_17)
target_include_directories(device_media_source_adapter
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
target_link_libraries(device_media_source_adapter
PUBLIC
cmvr_es::media_source_hub
cmvr_es::common
cmvr_es::proto
cmvr_es::logging
)
add_library(cmvr_es::device_media_source_adapter ALIAS device_media_source_adapter)
endif()
option(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS
"Build the standalone MediaSourceHub self-test"
${PROJECT_IS_TOP_LEVEL})
if(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS)
find_package(Threads REQUIRED)
add_executable(media_source_hub_test
tests/media_source_hub_test.cpp
)
target_compile_features(media_source_hub_test PRIVATE cxx_std_17)
target_link_libraries(media_source_hub_test
PRIVATE
cmvr_es::media_source_hub
Threads::Threads
)
# This self-test only links the static Hub and pthreads. In the root build,
# the project-wide third-party RUNPATH can otherwise make the loader pick up
# a vendor libstdc++.so (for example from the AUBO SDK), even though the test
# has no dependency on that SDK.
set_target_properties(media_source_hub_test PROPERTIES
SKIP_BUILD_RPATH TRUE
)
add_test(NAME media_source_hub_test COMMAND media_source_hub_test)
endif()

View File

@ -0,0 +1,38 @@
#ifndef CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H
#define CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include "devices/camera/abstract_camera.h"
#include "devices/microphone/abstract_microphone.h"
#include "manager/media_source_hub/include/media_source_hub.h"
namespace cmvr::media {
// Process-wide protocol-neutral media hub shared by gRPC and QUIC services.
MediaSourceHub& globalMediaSourceHub();
std::string cameraColorTrackId(const std::string& device_id);
std::string microphoneTrackId(const std::string& device_id);
// Registration is idempotent for an already registered track. The adapter owns a
// short-lived pump thread and one startStreaming()/stopStreaming() lease only while
// at least one Hub subscription is active. It ensures start() succeeds but deliberately
// does not call stop(), because the base device lifecycle can also be owned by control RPCs.
bool ensureCameraMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractCamera>& camera,
size_t ring_capacity = 64);
bool ensureMicrophoneMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractMicrophone>& microphone,
size_t ring_capacity = 256);
} // namespace cmvr::media
#endif // CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H

View File

@ -0,0 +1,128 @@
#ifndef CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H
#define CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "common/base/ring_buffer.h"
#include "common/media/media_frame.h"
namespace cmvr::media {
// MediaSourceHub owns no protocol-specific state. A device or capture adapter registers
// start/stop callbacks and receives a sink callback when the first consumer subscribes.
class MediaSourceHub final {
public:
using FrameRing = BroadcastFrameRing<MediaFrame>;
using FrameReadResult = FrameRing::ReadResult;
using StartPosition = FrameRing::StartPosition;
using FrameSink = std::function<void(MediaFramePtr)>;
// Cancellation checks run while MediaSourceHub protects source lifecycle
// state. Predicates must therefore be fast, non-blocking and must not call
// back into the same hub.
using CancelPredicate = std::function<bool()>;
struct SourceCallbacks {
// start() may run asynchronously. It must observe cancelled during any
// potentially blocking startup work and return false promptly once set.
// MediaSourceHub retains the callback state until a non-cooperative start
// eventually returns, so late completion cannot access destroyed state.
std::function<bool(
const FrameSink& sink,
const CancelPredicate& cancelled)> start;
// stop() is the synchronous publication barrier for the last lease and
// must unblock and join the source producer before returning.
std::function<void()> stop;
std::function<bool()> request_key_frame;
};
private:
struct SourceState;
public:
class Subscription final {
public:
Subscription() = default;
~Subscription();
Subscription(const Subscription&) = delete;
Subscription& operator=(const Subscription&) = delete;
Subscription(Subscription&& other) noexcept;
Subscription& operator=(Subscription&& other) noexcept;
// A Subscription owns one reader cursor and is single-consumer. Moving,
// resetting, or reading the same object concurrently is unsupported; use
// one independent subscription per consumer thread.
bool valid() const;
explicit operator bool() const { return valid(); }
// Returns the most recently observed immutable descriptor. A callback source may
// replace the initially registered UNKNOWN codec/config descriptor with the first
// real frame descriptor without invalidating existing subscriptions.
TrackDescriptorPtr descriptor() const;
std::optional<FrameReadResult> tryRead();
std::optional<FrameReadResult> waitRead(std::chrono::milliseconds timeout);
uint64_t discardPendingIfExceeds(size_t maximum_pending_frames);
uint64_t droppedCount() const noexcept;
void reset();
private:
friend class MediaSourceHub;
Subscription(std::shared_ptr<SourceState> source, FrameRing::Cursor cursor);
std::shared_ptr<SourceState> source_;
FrameRing::Cursor cursor_;
bool active_{false};
};
MediaSourceHub();
~MediaSourceHub();
MediaSourceHub(const MediaSourceHub&) = delete;
MediaSourceHub& operator=(const MediaSourceHub&) = delete;
bool registerSource(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks callbacks,
size_t ring_capacity = 64);
// Active sources cannot be unregistered. Destroy/reset their subscriptions first.
bool unregisterSource(const std::string& track_id);
bool hasSource(const std::string& track_id) const;
std::vector<TrackDescriptorPtr> listTracks() const;
size_t subscriberCount(const std::string& track_id) const;
// Protocol adapters can request an IDR after a discontinuity without knowing the
// concrete camera implementation. Returns false when unsupported or not running.
bool requestKeyFrame(const std::string& track_id) const;
Subscription subscribe(
const std::string& track_id,
StartPosition start_position = StartPosition::NEXT_PUBLISHED,
CancelPredicate cancelled = {});
// Stops all registered sources and invalidates outstanding subscriptions. The
// subscriptions remain destructible and their waitRead calls are awakened.
// A cooperative in-progress start is cancelled; a callback that violates the
// cancellation contract is quarantined with retained state rather than blocking
// shutdown or risking a use-after-free.
void shutdown();
private:
struct Impl;
std::shared_ptr<Impl> impl_;
};
} // namespace cmvr::media
#endif // CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H

View File

@ -0,0 +1,685 @@
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <exception>
#include <iterator>
#include <limits>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>
#include "common/base/logging/logger.h"
namespace cmvr::media {
namespace {
std::string normalizedCodec(std::string codec) {
codec.erase(
std::remove_if(codec.begin(), codec.end(), [](const unsigned char c) {
return !std::isalnum(c);
}),
codec.end());
std::transform(codec.begin(), codec.end(), codec.begin(), [](const unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return codec;
}
Codec videoCodec(const std::string& value) {
const std::string codec = normalizedCodec(value);
if (codec == "h264" || codec == "avc" || codec == "avc1" || codec == "libx264") {
return Codec::H264;
}
if (codec == "h265" || codec == "hevc" || codec == "hvc1" || codec == "libx265") {
return Codec::H265;
}
return Codec::UNKNOWN;
}
PayloadFormat videoPayloadFormat(
const Codec codec,
const std::vector<uint8_t>& payload) noexcept {
if (codec != Codec::H264 && codec != Codec::H265) {
return PayloadFormat::UNKNOWN;
}
const bool three_byte_start_code = payload.size() >= 3 &&
payload[0] == 0U && payload[1] == 0U && payload[2] == 1U;
const bool four_byte_start_code = payload.size() >= 4 &&
payload[0] == 0U && payload[1] == 0U && payload[2] == 0U && payload[3] == 1U;
return three_byte_start_code || four_byte_start_code
? PayloadFormat::ANNEX_B
: PayloadFormat::UNKNOWN;
}
Codec audioCodec(const std::string& value) {
const std::string codec = normalizedCodec(value);
if (codec == "opus") return Codec::OPUS;
if (codec == "aac") return Codec::AAC;
if (codec == "pcms16le") return Codec::PCM_S16LE;
return Codec::UNKNOWN;
}
Codec audioCodec(const device::AudioStreamFrameData& source) {
const Codec codec = audioCodec(source.codec);
if (codec != Codec::UNKNOWN || !normalizedCodec(source.codec).empty()) {
return codec;
}
switch (source.format) {
case device::AudioStreamFormat::PCM:
return Codec::PCM_S16LE;
case device::AudioStreamFormat::AAC:
return Codec::AAC;
case device::AudioStreamFormat::OPUS:
return Codec::OPUS;
default:
return Codec::UNKNOWN;
}
}
PayloadFormat audioPayloadFormat(
const Codec codec,
const std::vector<uint8_t>& payload) noexcept {
switch (codec) {
case Codec::OPUS:
return PayloadFormat::OPUS_PACKET;
case Codec::PCM_S16LE:
return PayloadFormat::RAW;
case Codec::AAC: {
// FFmpeg encoders commonly expose raw AAC access units plus AudioSpecificConfig;
// only advertise ADTS when the sync word and layer bits are actually present.
const bool has_adts_header = payload.size() >= 2 && payload[0] == 0xFFU &&
(payload[1] & 0xF6U) == 0xF0U;
return has_adts_header ? PayloadFormat::AAC_ADTS : PayloadFormat::RAW;
}
default:
return PayloadFormat::UNKNOWN;
}
}
Rational sanitizedTimeBase(
const int32_t numerator,
const int32_t denominator,
const int32_t fallback_denominator) noexcept {
return Rational{
numerator > 0 ? numerator : 1,
denominator > 0 ? denominator : std::max(1, fallback_denominator)};
}
uint64_t descriptorGeneration(
const uint64_t stream_epoch,
const uint32_t codec_generation) {
const uint64_t generation = (stream_epoch << 32U) | codec_generation;
return generation == 0 ? 1 : generation;
}
template<typename DeviceT>
struct PumpState : public std::enable_shared_from_this<PumpState<DeviceT>> {
explicit PumpState(std::shared_ptr<DeviceT> device_ptr)
: device(std::move(device_ptr)) {}
virtual ~PumpState() {
stop();
}
bool begin(
const MediaSourceHub::FrameSink& frame_sink,
const MediaSourceHub::CancelPredicate& cancelled) {
if (!frame_sink || !device) {
return false;
}
const auto cancellation_requested = [&cancelled] {
if (!cancelled) return false;
try {
return cancelled();
} catch (...) {
return true;
}
};
if (cancellation_requested()) {
return false;
}
std::unique_lock<std::mutex> lock(mutex);
if (running.load(std::memory_order_acquire)) {
return true;
}
if (worker.joinable()) {
// A previous worker must always be collected before a new capture lease starts.
std::thread stale_worker = std::move(worker);
lock.unlock();
collectThread(std::move(stale_worker));
lock.lock();
}
sink = frame_sink;
bool streaming_attempted = false;
try {
if (!device->start()) {
sink = {};
return false;
}
if (cancellation_requested()) {
sink = {};
return false;
}
streaming_attempted = true;
if (!device->startStreaming()) {
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
streaming_started = true;
if (cancellation_requested()) {
streaming_started = false;
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
running.store(true, std::memory_order_release);
try {
// The worker owns the pump while run() is active. This also
// makes the defensive self-stop/detach path lifetime-safe.
const auto self = this->shared_from_this();
worker = std::thread([self] { self->run(); });
} catch (...) {
running.store(false, std::memory_order_release);
streaming_started = false;
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to start media source: "
<< error.what();
sink = {};
lock.unlock();
if (streaming_attempted) stopDeviceStreaming();
return false;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to start media source";
sink = {};
lock.unlock();
if (streaming_attempted) stopDeviceStreaming();
return false;
}
return true;
}
void stop() noexcept {
std::thread thread;
bool stop_streaming = false;
{
std::lock_guard<std::mutex> lock(mutex);
running.store(false, std::memory_order_release);
stop_streaming = streaming_started;
streaming_started = false;
sink = {};
thread = std::move(worker);
}
if (stop_streaming) {
stopDeviceStreaming();
}
if (thread.joinable()) {
collectThread(std::move(thread));
}
}
static void collectThread(std::thread thread) noexcept {
if (!thread.joinable()) {
return;
}
try {
if (thread.get_id() == std::this_thread::get_id()) {
thread.detach();
} else {
thread.join();
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to collect media pump: "
<< error.what();
if (thread.joinable()) {
try {
thread.detach();
} catch (...) {
// std::thread's destructor would terminate if this extremely rare
// platform error occurred; there is no recoverable ownership path.
}
}
}
}
virtual void run() = 0;
void stopDeviceStreaming() noexcept {
try {
if (device) {
device->stopStreaming();
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to stop media source: "
<< error.what();
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to stop media source";
}
}
std::shared_ptr<DeviceT> device;
std::atomic<bool> running{false};
std::mutex mutex;
std::thread worker;
MediaSourceHub::FrameSink sink;
bool streaming_started{false};
};
struct CameraPump final : PumpState<device::AbstractCamera> {
CameraPump(std::shared_ptr<device::AbstractCamera> camera, std::string id)
: PumpState(std::move(camera)), track_id(std::move(id)) {}
~CameraPump() override { stop(); }
void run() override {
size_t cursor = 0;
uint64_t last_epoch = 0;
uint64_t last_sequence = 0;
uint64_t cached_config_generation = 0;
std::vector<uint8_t> cached_codec_config;
TrackDescriptorPtr last_descriptor;
bool have_previous = false;
bool have_cached_config_generation = false;
bool waiting_for_key_frame = true;
bool pending_discontinuity = true;
bool requested_key_frame = false;
std::chrono::steady_clock::time_point last_key_frame_request;
const auto request_key_frame = [&] {
const auto now = std::chrono::steady_clock::now();
if (requested_key_frame &&
now - last_key_frame_request < std::chrono::milliseconds(250)) {
return;
}
requested_key_frame = true;
last_key_frame_request = now;
try {
device->requestKeyFrame();
} catch (...) {
// Unsupported/failed key-frame requests fall back to the encoder's GOP.
}
};
request_key_frame();
while (running.load(std::memory_order_acquire)) {
device::StreamFrameData source;
try {
if (!device->waitEncodedFrame(source, cursor, std::chrono::milliseconds(50))) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Camera frame read failed for "
<< track_id << ": " << error.what();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Camera frame read failed for "
<< track_id;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (source.rgbFrame.empty()) {
continue;
}
const uint64_t descriptor_generation = descriptorGeneration(
source.stream_epoch, source.codec_config_generation);
if (!have_cached_config_generation ||
cached_config_generation != descriptor_generation) {
cached_codec_config.clear();
cached_config_generation = descriptor_generation;
have_cached_config_generation = true;
}
if (!source.codec_config.empty()) {
cached_codec_config = source.codec_config;
}
TrackDescriptor::Config track;
track.id = track_id;
track.source_id = device->id();
track.kind = MediaKind::VIDEO;
track.codec = videoCodec(source.codec);
track.payload_format = videoPayloadFormat(track.codec, source.rgbFrame);
track.time_base = sanitizedTimeBase(
source.time_base_num,
source.time_base_den,
source.fps);
track.width = static_cast<uint32_t>(std::max(0, source.width));
track.height = static_cast<uint32_t>(std::max(0, source.height));
track.nominal_rate = static_cast<uint32_t>(std::max(0, source.fps));
track.fx = source.intrinsics.fx;
track.fy = source.intrinsics.fy;
track.cx = source.intrinsics.cx;
track.cy = source.intrinsics.cy;
track.distortion.assign(
std::begin(source.intrinsics.coeffs),
std::end(source.intrinsics.coeffs));
track.generation = descriptor_generation;
track.codec_config = cached_codec_config;
const bool epoch_changed = have_previous && source.stream_epoch != last_epoch;
const bool sequence_wrapped = have_previous &&
last_sequence == std::numeric_limits<uint64_t>::max() && source.sequence != 0;
const bool sequence_gap = have_previous && !epoch_changed &&
(sequence_wrapped ||
(last_sequence != std::numeric_limits<uint64_t>::max() &&
source.sequence != last_sequence + 1));
TrackDescriptorPtr descriptor;
try {
descriptor = makeTrackDescriptor(std::move(track));
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid camera descriptor for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
have_previous = true;
continue;
}
const bool descriptor_changed = last_descriptor &&
!equivalentTrackDescriptor(*last_descriptor, *descriptor);
const bool discontinuity = !have_previous || source.discontinuity || epoch_changed ||
sequence_gap || descriptor_changed;
const bool inter_frame_codec = descriptor->codec == Codec::H264 ||
descriptor->codec == Codec::H265;
if (discontinuity) {
pending_discontinuity = true;
if (inter_frame_codec) {
waiting_for_key_frame = true;
request_key_frame();
} else {
waiting_for_key_frame = false;
}
}
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
last_descriptor = descriptor;
have_previous = true;
if (waiting_for_key_frame && !source.bKey) {
request_key_frame();
continue;
}
waiting_for_key_frame = false;
MediaFrame::Config frame;
frame.descriptor = std::move(descriptor);
frame.payload = std::move(source.rgbFrame);
frame.sequence = source.sequence;
frame.source_timestamp = source.source_timestamp;
frame.source_frame_number = source.source_frame_number;
frame.pts = source.pts;
frame.dts = source.dts;
frame.duration = source.duration;
frame.capture_time_ns = source.capture_monotonic_ns > 0
? static_cast<uint64_t>(source.capture_monotonic_ns)
: 0;
frame.capture_utc_ns = source.capture_utc_ns;
frame.key_frame = source.bKey;
frame.discontinuity = pending_discontinuity;
MediaSourceHub::FrameSink current_sink;
{
std::lock_guard<std::mutex> lock(mutex);
current_sink = sink;
}
if (current_sink) {
try {
current_sink(makeMediaFrame(std::move(frame)));
pending_discontinuity = false;
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid camera frame for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
}
}
}
}
std::string track_id;
};
struct MicrophonePump final : PumpState<device::AbstractMicrophone> {
MicrophonePump(std::shared_ptr<device::AbstractMicrophone> microphone, std::string id)
: PumpState(std::move(microphone)), track_id(std::move(id)) {}
~MicrophonePump() override { stop(); }
void run() override {
size_t cursor = 0;
uint64_t last_epoch = 0;
uint64_t last_sequence = 0;
uint64_t cached_config_generation = 0;
std::vector<uint8_t> cached_codec_config;
TrackDescriptorPtr last_descriptor;
bool have_previous = false;
bool have_cached_config_generation = false;
bool pending_discontinuity = true;
while (running.load(std::memory_order_acquire)) {
device::AudioStreamFrameData source;
try {
if (!device->waitEncodedFrame(source, cursor, std::chrono::milliseconds(50))) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Microphone frame read failed for "
<< track_id << ": " << error.what();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Microphone frame read failed for "
<< track_id;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (source.data.empty()) {
continue;
}
const uint64_t descriptor_generation = descriptorGeneration(
source.stream_epoch, source.codec_config_generation);
if (!have_cached_config_generation ||
cached_config_generation != descriptor_generation) {
cached_codec_config.clear();
cached_config_generation = descriptor_generation;
have_cached_config_generation = true;
}
if (!source.codec_config.empty()) {
cached_codec_config = source.codec_config;
}
TrackDescriptor::Config track;
track.id = track_id;
track.source_id = device->id();
track.kind = MediaKind::AUDIO;
track.codec = audioCodec(source);
track.payload_format = audioPayloadFormat(track.codec, source.data);
track.time_base = sanitizedTimeBase(
source.time_base_num,
source.time_base_den,
source.sample_rate);
track.sample_rate = static_cast<uint32_t>(std::max(0, source.sample_rate));
track.channels = static_cast<uint32_t>(std::max(0, source.channels));
// Packet sample counts belong to MediaFrame::duration, not immutable track metadata.
track.nominal_rate = 0;
track.generation = descriptor_generation;
track.codec_config = cached_codec_config;
const bool epoch_changed = have_previous && source.stream_epoch != last_epoch;
const bool sequence_wrapped = have_previous &&
last_sequence == std::numeric_limits<uint64_t>::max() && source.sequence != 0;
const bool sequence_gap = have_previous && !epoch_changed &&
(sequence_wrapped ||
(last_sequence != std::numeric_limits<uint64_t>::max() &&
source.sequence != last_sequence + 1));
TrackDescriptorPtr descriptor;
try {
descriptor = makeTrackDescriptor(std::move(track));
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid microphone descriptor for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
have_previous = true;
continue;
}
const bool descriptor_changed = last_descriptor &&
!equivalentTrackDescriptor(*last_descriptor, *descriptor);
if (!have_previous || source.discontinuity || epoch_changed || sequence_gap ||
descriptor_changed) {
pending_discontinuity = true;
}
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
last_descriptor = descriptor;
have_previous = true;
MediaFrame::Config frame;
frame.descriptor = std::move(descriptor);
frame.payload = std::move(source.data);
frame.sequence = source.sequence;
frame.pts = source.pts;
frame.dts = source.dts;
frame.duration = source.duration > 0 ? source.duration : source.nb_samples;
frame.capture_time_ns = source.capture_monotonic_ns > 0
? static_cast<uint64_t>(source.capture_monotonic_ns)
: 0;
frame.capture_utc_ns = source.capture_utc_ns;
frame.key_frame = true;
frame.discontinuity = pending_discontinuity;
MediaSourceHub::FrameSink current_sink;
{
std::lock_guard<std::mutex> lock(mutex);
current_sink = sink;
}
if (current_sink) {
try {
current_sink(makeMediaFrame(std::move(frame)));
pending_discontinuity = false;
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid microphone frame for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
}
}
}
}
std::string track_id;
};
TrackDescriptorPtr initialTrack(
std::string track_id,
std::string source_id,
const MediaKind kind) {
TrackDescriptor::Config config;
config.id = std::move(track_id);
config.source_id = std::move(source_id);
config.kind = kind;
// A placeholder descriptor is never emitted as a media sample, but it
// still carries a mathematically valid neutral time base.
config.time_base = Rational{1, 1};
config.generation = 1;
return makeTrackDescriptor(std::move(config));
}
} // namespace
MediaSourceHub& globalMediaSourceHub() {
static MediaSourceHub hub;
return hub;
}
std::string cameraColorTrackId(const std::string& device_id) {
return device_id + "/video/color";
}
std::string microphoneTrackId(const std::string& device_id) {
return device_id + "/audio/main";
}
bool ensureCameraMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractCamera>& camera,
const size_t ring_capacity) {
if (!camera || camera->id().empty()) {
return false;
}
const std::string track_id = cameraColorTrackId(camera->id());
if (hub.hasSource(track_id)) {
return true;
}
const auto pump = std::make_shared<CameraPump>(camera, track_id);
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [pump](
const MediaSourceHub::FrameSink& sink,
const MediaSourceHub::CancelPredicate& cancelled) {
return pump->begin(sink, cancelled);
};
callbacks.stop = [pump] { pump->stop(); };
callbacks.request_key_frame = [camera] { return camera->requestKeyFrame(); };
const bool registered = hub.registerSource(
initialTrack(track_id, camera->id(), MediaKind::VIDEO),
std::move(callbacks),
ring_capacity);
if (!registered && !hub.hasSource(track_id)) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to register camera track: " << track_id;
return false;
}
return true;
}
bool ensureMicrophoneMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractMicrophone>& microphone,
const size_t ring_capacity) {
if (!microphone || microphone->id().empty()) {
return false;
}
const std::string track_id = microphoneTrackId(microphone->id());
if (hub.hasSource(track_id)) {
return true;
}
const auto pump = std::make_shared<MicrophonePump>(microphone, track_id);
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [pump](
const MediaSourceHub::FrameSink& sink,
const MediaSourceHub::CancelPredicate& cancelled) {
return pump->begin(sink, cancelled);
};
callbacks.stop = [pump] { pump->stop(); };
const bool registered = hub.registerSource(
initialTrack(track_id, microphone->id(), MediaKind::AUDIO),
std::move(callbacks),
ring_capacity);
if (!registered && !hub.hasSource(track_id)) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to register microphone track: " << track_id;
return false;
}
return true;
}
} // namespace cmvr::media

View File

@ -0,0 +1,596 @@
#include "manager/media_source_hub/include/media_source_hub.h"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <utility>
namespace cmvr::media {
struct MediaSourceHub::SourceState final : public std::enable_shared_from_this<SourceState> {
enum class Lifecycle {
STOPPED,
STARTING,
RUNNING,
STOPPING
};
struct StartAttempt {
size_t waiters{0};
bool completed{false};
bool succeeded{false};
std::atomic<bool> cancel_requested{false};
};
SourceState(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks source_callbacks,
const size_t ring_capacity)
: track_id(initial_descriptor->id),
descriptor(std::move(initial_descriptor)),
callbacks(std::move(source_callbacks)),
ring(ring_capacity) {}
FrameSink makeSink() {
const std::weak_ptr<SourceState> weak_source = shared_from_this();
return [weak_source](MediaFramePtr frame) {
if (const auto source = weak_source.lock()) {
source->acceptFrame(std::move(frame));
}
};
}
void acceptFrame(MediaFramePtr frame) {
if (!frame || !frame->descriptor || frame->descriptor->id != track_id) {
return;
}
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (!registered ||
(lifecycle != Lifecycle::STARTING && lifecycle != Lifecycle::RUNNING)) {
return;
}
}
const TrackDescriptorPtr current = std::atomic_load(&descriptor);
if (!current || !equivalentTrackDescriptor(*current, *frame->descriptor)) {
// C++17 atomic shared_ptr free functions provide an atomic descriptor snapshot to
// all subscriptions while frames remain immutable.
std::atomic_store(&descriptor, frame->descriptor);
}
ring.publish(std::move(frame));
}
static bool isCancelled(const CancelPredicate& cancelled) noexcept {
if (!cancelled) return false;
try {
return cancelled();
} catch (...) {
return true;
}
}
bool invokeStart(
const FrameSink& sink,
const CancelPredicate& cancelled) noexcept {
std::lock_guard<std::mutex> callback_lock(callback_mutex);
try {
return callbacks.start && callbacks.start(sink, cancelled);
} catch (...) {
return false;
}
}
void invokeStop() noexcept {
std::lock_guard<std::mutex> callback_lock(callback_mutex);
try {
if (callbacks.stop) callbacks.stop();
} catch (...) {
}
}
void completeStart(
const std::shared_ptr<StartAttempt>& attempt,
const bool started) {
bool stop_abandoned_start = false;
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (start_attempt != attempt || lifecycle != Lifecycle::STARTING) {
return;
}
attempt->completed = true;
attempt->succeeded = started;
if (started && registered && attempt->waiters != 0U) {
lifecycle = Lifecycle::RUNNING;
} else if (started) {
lifecycle = Lifecycle::STOPPING;
ring.close();
stop_abandoned_start = true;
} else {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
if (stop_abandoned_start) {
invokeStop();
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
}
bool acquire(
const StartPosition start_position,
FrameRing::Cursor& cursor,
const CancelPredicate& cancelled) {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
while (lifecycle == Lifecycle::STOPPING) {
if (!registered || isCancelled(cancelled)) return false;
lifecycle_condition.wait_for(lock, std::chrono::milliseconds(10));
}
if (!registered || isCancelled(cancelled)) return false;
if (lifecycle == Lifecycle::RUNNING) {
++subscriber_count;
lock.unlock();
cursor = ring.makeCursor(start_position);
return true;
}
std::shared_ptr<StartAttempt> attempt;
if (lifecycle == Lifecycle::STOPPED) {
lifecycle = Lifecycle::STARTING;
ring.reset();
const FrameSink sink = makeSink();
attempt = std::make_shared<StartAttempt>();
attempt->waiters = 1U;
start_attempt = attempt;
const auto self = shared_from_this();
try {
std::thread([self, attempt, sink]() {
const CancelPredicate cancelled = [attempt] {
return attempt->cancel_requested.load(
std::memory_order_acquire);
};
const bool started = self->invokeStart(sink, cancelled);
self->completeStart(attempt, started);
}).detach();
} catch (...) {
start_attempt.reset();
lifecycle = Lifecycle::STOPPED;
lifecycle_condition.notify_all();
return false;
}
} else if (lifecycle == Lifecycle::STARTING) {
attempt = start_attempt;
if (!attempt || attempt->cancel_requested.load(std::memory_order_acquire)) {
return false;
}
++attempt->waiters;
} else {
return false;
}
while (registered && !attempt->completed) {
if (isCancelled(cancelled)) {
if (attempt->waiters != 0U) --attempt->waiters;
if (attempt->waiters == 0U) {
attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return false;
}
lifecycle_condition.wait_for(lock, std::chrono::milliseconds(10));
}
const bool caller_cancelled = isCancelled(cancelled);
const bool acquired = !caller_cancelled && registered && attempt->completed &&
attempt->succeeded &&
lifecycle == Lifecycle::RUNNING;
if (attempt->waiters != 0U) --attempt->waiters;
if (!acquired) {
if (attempt->waiters == 0U) {
attempt->cancel_requested.store(true, std::memory_order_release);
}
const bool stop_unclaimed_source =
start_attempt == attempt && attempt->waiters == 0U &&
subscriber_count == 0U &&
lifecycle == Lifecycle::RUNNING;
if (stop_unclaimed_source) {
lifecycle = Lifecycle::STOPPING;
ring.close();
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
return false;
}
++subscriber_count;
lock.unlock();
cursor = ring.makeCursor(start_position);
return true;
}
void release() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
if (subscriber_count == 0) {
return;
}
--subscriber_count;
if (subscriber_count != 0 || lifecycle != Lifecycle::RUNNING) {
return;
}
lifecycle = Lifecycle::STOPPING;
ring.close();
lock.unlock();
invokeStop();
lock.lock();
lifecycle = Lifecycle::STOPPED;
lifecycle_condition.notify_all();
}
bool deactivateIfUnused() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
if (subscriber_count != 0) {
return false;
}
registered = false;
ring.close();
if (lifecycle == Lifecycle::STARTING) {
if (start_attempt) {
start_attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return true;
}
if (lifecycle == Lifecycle::STOPPING || lifecycle == Lifecycle::STOPPED) {
lifecycle_condition.notify_all();
return true;
}
lifecycle = Lifecycle::STOPPING;
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
return true;
}
void shutdown() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
registered = false;
ring.close();
if (lifecycle == Lifecycle::STARTING) {
if (start_attempt) {
start_attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return;
}
if (lifecycle == Lifecycle::STOPPING) {
lifecycle_condition.notify_all();
return;
}
if (lifecycle == Lifecycle::STOPPED) {
lifecycle_condition.notify_all();
return;
}
lifecycle = Lifecycle::STOPPING;
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
bool validForSubscription() const {
std::lock_guard<std::mutex> lock(lifecycle_mutex);
return registered && lifecycle == Lifecycle::RUNNING;
}
size_t subscriberCount() const {
std::lock_guard<std::mutex> lock(lifecycle_mutex);
return subscriber_count;
}
bool requestKeyFrame() const {
// Serialize with stop first, then re-check lifecycle. A stop that has
// already begun rejects the request; a stop that begins afterwards
// waits for this callback before invoking the device stop barrier.
std::lock_guard<std::mutex> callback_lock(callback_mutex);
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (!registered || lifecycle != Lifecycle::RUNNING || !callbacks.request_key_frame) {
return false;
}
}
try {
return callbacks.request_key_frame();
} catch (...) {
return false;
}
}
TrackDescriptorPtr currentDescriptor() const {
return std::atomic_load(&descriptor);
}
const std::string track_id;
mutable TrackDescriptorPtr descriptor;
const SourceCallbacks callbacks;
FrameRing ring;
mutable std::mutex callback_mutex;
mutable std::mutex lifecycle_mutex;
std::condition_variable lifecycle_condition;
Lifecycle lifecycle{Lifecycle::STOPPED};
size_t subscriber_count{0};
bool registered{true};
std::shared_ptr<StartAttempt> start_attempt;
};
struct MediaSourceHub::Impl final {
mutable std::mutex mutex;
std::unordered_map<std::string, std::shared_ptr<SourceState>> sources;
};
MediaSourceHub::Subscription::Subscription(
std::shared_ptr<SourceState> source,
FrameRing::Cursor cursor)
: source_(std::move(source)),
cursor_(std::move(cursor)),
active_(static_cast<bool>(source_)) {}
MediaSourceHub::Subscription::~Subscription() {
reset();
}
MediaSourceHub::Subscription::Subscription(Subscription&& other) noexcept
: source_(std::move(other.source_)),
cursor_(other.cursor_),
active_(other.active_) {
other.active_ = false;
}
MediaSourceHub::Subscription& MediaSourceHub::Subscription::operator=(Subscription&& other) noexcept {
if (this == &other) {
return *this;
}
reset();
source_ = std::move(other.source_);
cursor_ = other.cursor_;
active_ = other.active_;
other.active_ = false;
return *this;
}
bool MediaSourceHub::Subscription::valid() const {
return active_ && source_ && source_->validForSubscription();
}
TrackDescriptorPtr MediaSourceHub::Subscription::descriptor() const {
return source_ ? source_->currentDescriptor() : nullptr;
}
std::optional<MediaSourceHub::FrameReadResult> MediaSourceHub::Subscription::tryRead() {
if (!active_ || !source_) {
return std::nullopt;
}
return source_->ring.tryRead(cursor_);
}
std::optional<MediaSourceHub::FrameReadResult> MediaSourceHub::Subscription::waitRead(
const std::chrono::milliseconds timeout) {
if (!active_ || !source_) {
return std::nullopt;
}
return source_->ring.waitRead(cursor_, timeout);
}
uint64_t MediaSourceHub::Subscription::discardPendingIfExceeds(
const size_t maximum_pending_frames) {
if (!active_ || !source_) {
return 0;
}
return source_->ring.discardPendingIfExceeds(cursor_, maximum_pending_frames);
}
uint64_t MediaSourceHub::Subscription::droppedCount() const noexcept {
return cursor_.dropped_count;
}
void MediaSourceHub::Subscription::reset() {
if (active_ && source_) {
source_->release();
}
active_ = false;
source_.reset();
}
MediaSourceHub::MediaSourceHub()
: impl_(std::make_shared<Impl>()) {}
MediaSourceHub::~MediaSourceHub() {
shutdown();
}
bool MediaSourceHub::registerSource(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks callbacks,
const size_t ring_capacity) {
if (!impl_ || !initial_descriptor || initial_descriptor->id.empty() ||
!callbacks.start || ring_capacity == 0) {
return false;
}
std::shared_ptr<SourceState> source;
try {
source = std::make_shared<SourceState>(
std::move(initial_descriptor), std::move(callbacks), ring_capacity);
} catch (...) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->sources.emplace(source->track_id, std::move(source)).second;
}
bool MediaSourceHub::unregisterSource(const std::string& track_id) {
if (!impl_ || track_id.empty()) {
return false;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return false;
}
source = it->second;
}
if (!source->deactivateIfUnused()) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it != impl_->sources.end() && it->second == source) {
impl_->sources.erase(it);
return true;
}
return false;
}
bool MediaSourceHub::hasSource(const std::string& track_id) const {
if (!impl_) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->sources.find(track_id) != impl_->sources.end();
}
std::vector<TrackDescriptorPtr> MediaSourceHub::listTracks() const {
std::vector<std::shared_ptr<SourceState>> sources;
if (!impl_) {
return {};
}
{
std::lock_guard<std::mutex> lock(impl_->mutex);
sources.reserve(impl_->sources.size());
for (const auto& [track_id, source] : impl_->sources) {
(void)track_id;
sources.push_back(source);
}
}
std::vector<TrackDescriptorPtr> descriptors;
descriptors.reserve(sources.size());
for (const auto& source : sources) {
descriptors.push_back(source->currentDescriptor());
}
std::sort(descriptors.begin(), descriptors.end(), [](const auto& lhs, const auto& rhs) {
if (!lhs) return static_cast<bool>(rhs);
if (!rhs) return false;
return lhs->id < rhs->id;
});
return descriptors;
}
size_t MediaSourceHub::subscriberCount(const std::string& track_id) const {
if (!impl_) {
return 0;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return 0;
}
source = it->second;
}
return source->subscriberCount();
}
bool MediaSourceHub::requestKeyFrame(const std::string& track_id) const {
if (!impl_) {
return false;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return false;
}
source = it->second;
}
return source->requestKeyFrame();
}
MediaSourceHub::Subscription MediaSourceHub::subscribe(
const std::string& track_id,
const StartPosition start_position,
CancelPredicate cancelled) {
if (!impl_) {
return {};
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return {};
}
source = it->second;
}
FrameRing::Cursor cursor;
if (!source->acquire(start_position, cursor, cancelled)) {
return {};
}
return Subscription(std::move(source), std::move(cursor));
}
void MediaSourceHub::shutdown() {
if (!impl_) {
return;
}
std::vector<std::shared_ptr<SourceState>> sources;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
sources.reserve(impl_->sources.size());
for (auto& [track_id, source] : impl_->sources) {
(void)track_id;
sources.push_back(std::move(source));
}
impl_->sources.clear();
}
for (const auto& source : sources) {
source->shutdown();
}
}
} // namespace cmvr::media

View File

@ -0,0 +1,683 @@
#include "manager/media_source_hub/include/media_source_hub.h"
#include <atomic>
#include <chrono>
#include <future>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace {
using namespace std::chrono_literals;
using cmvr::media::Codec;
using cmvr::media::MediaFrame;
using cmvr::media::MediaFramePtr;
using cmvr::media::MediaKind;
using cmvr::media::MediaSourceHub;
using cmvr::media::PayloadFormat;
using cmvr::media::Rational;
using cmvr::media::TrackDescriptor;
using cmvr::media::TrackDescriptorPtr;
static_assert(!std::is_copy_assignable<MediaFrame>::value, "MediaFrame must be immutable");
static_assert(!std::is_copy_assignable<TrackDescriptor>::value, "TrackDescriptor must be immutable");
static_assert(std::is_same<MediaFramePtr::element_type, const MediaFrame>::value,
"MediaFramePtr must share const frames");
int failures = 0;
#define CHECK_TRUE(expression) \
do { \
if (!(expression)) { \
std::cerr << __FILE__ << ':' << __LINE__ << " check failed: " #expression << '\n'; \
++failures; \
} \
} while (false)
TrackDescriptorPtr makeVideoDescriptor(
const Codec codec,
const uint64_t generation,
std::vector<uint8_t> codec_config = {}) {
TrackDescriptor::Config config;
config.id = "camera.front.video";
config.source_id = "camera.front";
config.kind = MediaKind::VIDEO;
config.codec = codec;
config.payload_format = codec == Codec::UNKNOWN ? PayloadFormat::UNKNOWN : PayloadFormat::ANNEX_B;
config.time_base = Rational{1, 90000};
config.width = 640;
config.height = 360;
config.nominal_rate = 30;
config.generation = generation;
config.codec_config = std::move(codec_config);
return cmvr::media::makeTrackDescriptor(std::move(config));
}
MediaFramePtr makeFrame(
TrackDescriptorPtr descriptor,
const uint64_t sequence,
const uint8_t marker) {
MediaFrame::Config config;
config.descriptor = std::move(descriptor);
config.payload = {marker, static_cast<uint8_t>(marker + 1)};
config.sequence = sequence;
config.pts = static_cast<int64_t>(sequence * 3000);
config.dts = config.pts;
config.duration = 3000;
config.capture_time_ns = sequence * 1000000;
config.capture_utc_ns = 1700000000000000000LL + static_cast<int64_t>(sequence);
config.key_frame = sequence == 0;
return cmvr::media::makeMediaFrame(std::move(config));
}
void testMediaMetadataValidation() {
TrackDescriptor::Config invalid;
invalid.id = "invalid.video";
invalid.source_id = "invalid";
invalid.kind = MediaKind::VIDEO;
invalid.time_base = Rational{0, 1};
bool rejected = false;
try {
(void)cmvr::media::makeTrackDescriptor(std::move(invalid));
} catch (const std::invalid_argument&) {
rejected = true;
}
CHECK_TRUE(rejected);
const auto frame = makeFrame(makeVideoDescriptor(Codec::H264, 1), 7, 1);
CHECK_TRUE(frame->capture_time_ns == 7000000);
CHECK_TRUE(frame->capture_utc_ns == 1700000000000000007LL);
CHECK_TRUE(frame->duration == 3000);
}
void testLegacySpmcCompatibility() {
bool ring_zero_capacity_rejected = false;
try {
RingBuffer<int> invalid_ring(0);
} catch (const std::invalid_argument&) {
ring_zero_capacity_rejected = true;
}
CHECK_TRUE(ring_zero_capacity_rejected);
bool spmc_zero_capacity_rejected = false;
try {
SPMCRingBuffer<int> invalid_ring(0);
} catch (const std::invalid_argument&) {
spmc_zero_capacity_rejected = true;
}
CHECK_TRUE(spmc_zero_capacity_rejected);
SPMCRingBuffer<int> ring(2);
ring.push(10);
ring.push(20);
CHECK_TRUE(ring.size() == 2);
CHECK_TRUE(ring.getHead() == 2);
CHECK_TRUE(ring.getTail() == 0);
CHECK_TRUE(ring.getLast().has_value() && *ring.getLast() == 20);
size_t reader = 0;
CHECK_TRUE(ring.pop(reader).has_value());
ring.push(30);
ring.push(40);
CHECK_TRUE(!ring.pop(reader).has_value());
CHECK_TRUE(reader == ring.getTail());
CHECK_TRUE(ring.pop(reader).has_value());
ring.clear();
CHECK_TRUE(ring.empty());
CHECK_TRUE(ring.getHead() == 4);
ring.push(50);
CHECK_TRUE(!ring.pop(reader).has_value());
const auto after_clear = ring.pop(reader);
CHECK_TRUE(after_clear.has_value() && *after_clear == 50);
}
void testBroadcastFrameRing() {
using Ring = BroadcastFrameRing<MediaFrame>;
Ring ring(2);
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 0, 10)).value() == 0);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 1, 20)).value() == 1);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 2, 30)).value() == 2);
const auto first = ring.tryRead(cursor);
CHECK_TRUE(first.has_value());
CHECK_TRUE(first->sequence == 1);
CHECK_TRUE(first->value->sequence == 1);
CHECK_TRUE(first->dropped_count == 1);
CHECK_TRUE(first->dropped_since_last_read == 1);
CHECK_TRUE(ring.stats().dropped_count == 1);
const auto second = ring.tryRead(cursor);
CHECK_TRUE(second.has_value() && second->sequence == 2);
CHECK_TRUE(second->dropped_since_last_read == 0);
auto waiting_cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
auto waiting_read = std::async(std::launch::async, [&ring, &waiting_cursor] {
return ring.waitRead(waiting_cursor, 1s);
});
std::this_thread::sleep_for(10ms);
ring.publish(makeFrame(descriptor, 3, 40));
CHECK_TRUE(waiting_read.wait_for(500ms) == std::future_status::ready);
CHECK_TRUE(waiting_read.get().has_value());
const uint64_t next_generation = ring.reset();
CHECK_TRUE(next_generation == 2);
ring.publish(makeFrame(descriptor, 4, 50));
const auto after_reset = ring.tryRead(cursor);
CHECK_TRUE(after_reset.has_value());
CHECK_TRUE(after_reset->generation == 2);
CHECK_TRUE(after_reset->sequence == 0);
CHECK_TRUE(after_reset->generation_changed);
auto close_cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
auto close_wait = std::async(std::launch::async, [&ring, &close_cursor] {
return ring.waitRead(close_cursor, 2s);
});
ring.close();
CHECK_TRUE(close_wait.wait_for(500ms) == std::future_status::ready);
CHECK_TRUE(!close_wait.get().has_value());
CHECK_TRUE(!ring.publish(makeFrame(descriptor, 5, 60)).has_value());
}
void testBroadcastDiscardPending() {
using Ring = BroadcastFrameRing<MediaFrame>;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
{
Ring ring(8);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.publish(makeFrame(descriptor, 1, 20));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 0);
const auto first = ring.tryRead(cursor);
CHECK_TRUE(first.has_value());
CHECK_TRUE(first->sequence == 0);
CHECK_TRUE(first->dropped_since_last_read == 0);
}
{
Ring ring(8);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.publish(makeFrame(descriptor, 1, 20));
ring.publish(makeFrame(descriptor, 2, 30));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 3);
CHECK_TRUE(!ring.tryRead(cursor).has_value());
ring.publish(makeFrame(descriptor, 3, 40));
const auto after_discard = ring.tryRead(cursor);
CHECK_TRUE(after_discard.has_value());
CHECK_TRUE(after_discard->sequence == 3);
CHECK_TRUE(after_discard->dropped_count == 3);
CHECK_TRUE(after_discard->dropped_since_last_read == 3);
}
{
// Two frames are overwritten before the explicit three-frame discard.
// Both kinds of loss must be reported by the next successful read.
Ring ring(3);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
for (uint64_t sequence = 0; sequence < 5; ++sequence) {
ring.publish(makeFrame(
descriptor,
sequence,
static_cast<uint8_t>(sequence)));
}
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 3);
CHECK_TRUE(cursor.dropped_count == 5);
ring.publish(makeFrame(descriptor, 5, 50));
const auto after_overwrite_and_discard = ring.tryRead(cursor);
CHECK_TRUE(after_overwrite_and_discard.has_value());
CHECK_TRUE(after_overwrite_and_discard->sequence == 5);
CHECK_TRUE(after_overwrite_and_discard->dropped_count == 5);
CHECK_TRUE(after_overwrite_and_discard->dropped_since_last_read == 5);
}
{
// An old-generation OLDEST_AVAILABLE cursor adopts the reset generation
// before deciding whether that generation's pending frames are excessive.
Ring ring(4);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.reset();
ring.publish(makeFrame(descriptor, 1, 20));
ring.publish(makeFrame(descriptor, 2, 30));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 1) == 2);
ring.publish(makeFrame(descriptor, 3, 40));
const auto after_reset = ring.tryRead(cursor);
CHECK_TRUE(after_reset.has_value());
CHECK_TRUE(after_reset->generation == 2);
CHECK_TRUE(after_reset->sequence == 2);
CHECK_TRUE(after_reset->generation_changed);
CHECK_TRUE(after_reset->dropped_since_last_read == 2);
}
}
void testBroadcastDiscardConcurrentPublish() {
using Ring = BroadcastFrameRing<int>;
constexpr uint64_t frame_count = 4000;
Ring ring(64);
auto cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
std::atomic<bool> start{false};
std::atomic<bool> publisher_done{false};
std::thread publisher([&] {
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
for (uint64_t sequence = 0; sequence < frame_count; ++sequence) {
ring.publish(std::make_shared<const int>(static_cast<int>(sequence)));
if ((sequence & 7U) == 0U) {
std::this_thread::yield();
}
}
publisher_done.store(true, std::memory_order_release);
});
uint64_t read_count = 0;
uint64_t actively_discarded = 0;
start.store(true, std::memory_order_release);
while (true) {
actively_discarded += ring.discardPendingIfExceeds(cursor, 8);
if (ring.tryRead(cursor)) {
++read_count;
continue;
}
if (publisher_done.load(std::memory_order_acquire)) {
actively_discarded += ring.discardPendingIfExceeds(cursor, 8);
if (ring.tryRead(cursor)) {
++read_count;
continue;
}
break;
}
std::this_thread::yield();
}
publisher.join();
CHECK_TRUE(read_count + cursor.dropped_count == frame_count);
CHECK_TRUE(actively_discarded <= cursor.dropped_count);
}
void testBroadcastConcurrency() {
using Ring = BroadcastFrameRing<MediaFrame>;
constexpr uint64_t frame_count = 500;
Ring ring(frame_count);
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
auto first_cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
auto second_cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
auto consume = [&ring](Ring::Cursor& cursor) {
uint64_t expected = 0;
while (expected < frame_count) {
const auto result = ring.waitRead(cursor, 1s);
if (!result || result->sequence != expected || result->value->sequence != expected) {
return false;
}
++expected;
}
return cursor.dropped_count == 0;
};
auto first_consumer = std::async(std::launch::async, consume, std::ref(first_cursor));
auto second_consumer = std::async(std::launch::async, consume, std::ref(second_cursor));
std::thread producer([&ring, &descriptor] {
for (uint64_t sequence = 0; sequence < frame_count; ++sequence) {
ring.publish(makeFrame(descriptor, sequence, static_cast<uint8_t>(sequence)));
}
});
producer.join();
CHECK_TRUE(first_consumer.get());
CHECK_TRUE(second_consumer.get());
}
void testHubLifecycleAndDescriptorRefresh() {
MediaSourceHub hub;
const auto initial_descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<int> start_count{0};
std::atomic<int> stop_count{0};
std::atomic<int> key_frame_requests{0};
std::mutex sink_mutex;
MediaSourceHub::FrameSink sink;
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink& callback_sink,
const MediaSourceHub::CancelPredicate&) {
{
std::lock_guard<std::mutex> lock(sink_mutex);
sink = callback_sink;
}
++start_count;
return true;
};
callbacks.stop = [&] {
++stop_count;
std::lock_guard<std::mutex> lock(sink_mutex);
sink = {};
};
callbacks.request_key_frame = [&] {
++key_frame_requests;
return true;
};
CHECK_TRUE(hub.registerSource(initial_descriptor, callbacks, 4));
CHECK_TRUE(!hub.registerSource(initial_descriptor, callbacks, 4));
CHECK_TRUE(hub.hasSource(initial_descriptor->id));
CHECK_TRUE(hub.listTracks().size() == 1);
auto first = hub.subscribe(initial_descriptor->id);
auto second = hub.subscribe(initial_descriptor->id);
CHECK_TRUE(first.valid() && second.valid());
CHECK_TRUE(hub.requestKeyFrame(initial_descriptor->id));
CHECK_TRUE(key_frame_requests == 1);
CHECK_TRUE(start_count == 1);
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 2);
CHECK_TRUE(first.descriptor()->codec == Codec::UNKNOWN);
CHECK_TRUE(!hub.unregisterSource(initial_descriptor->id));
// Content changes at the same generation must atomically replace the initial descriptor.
const auto actual_descriptor = makeVideoDescriptor(Codec::H264, 1, {0, 0, 0, 1, 0x67});
MediaSourceHub::FrameSink producer;
{
std::lock_guard<std::mutex> lock(sink_mutex);
producer = sink;
}
CHECK_TRUE(static_cast<bool>(producer));
const auto shared_frame = makeFrame(actual_descriptor, 0, 70);
producer(shared_frame);
const auto first_read = first.waitRead(500ms);
const auto second_read = second.waitRead(500ms);
CHECK_TRUE(first_read.has_value() && first_read->value == shared_frame);
CHECK_TRUE(second_read.has_value() && second_read->value == shared_frame);
CHECK_TRUE(first.descriptor() == actual_descriptor);
CHECK_TRUE(second.descriptor()->codec_config == actual_descriptor->codec_config);
first.reset();
CHECK_TRUE(stop_count == 0);
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 1);
second.reset();
CHECK_TRUE(stop_count == 1);
CHECK_TRUE(!hub.requestKeyFrame(initial_descriptor->id));
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 0);
{
auto restarted = hub.subscribe(initial_descriptor->id);
CHECK_TRUE(restarted.valid());
CHECK_TRUE(start_count == 2);
}
CHECK_TRUE(stop_count == 2);
CHECK_TRUE(hub.unregisterSource(initial_descriptor->id));
CHECK_TRUE(!hub.hasSource(initial_descriptor->id));
}
void testSubscriptionDiscardPending() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1);
std::mutex sink_mutex;
MediaSourceHub::FrameSink sink;
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink& callback_sink,
const MediaSourceHub::CancelPredicate&) {
std::lock_guard<std::mutex> lock(sink_mutex);
sink = callback_sink;
return true;
};
callbacks.stop = [&] {
std::lock_guard<std::mutex> lock(sink_mutex);
sink = {};
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 8));
auto subscription = hub.subscribe(descriptor->id);
CHECK_TRUE(subscription.valid());
MediaSourceHub::FrameSink producer;
{
std::lock_guard<std::mutex> lock(sink_mutex);
producer = sink;
}
CHECK_TRUE(static_cast<bool>(producer));
producer(makeFrame(descriptor, 0, 10));
producer(makeFrame(descriptor, 1, 20));
producer(makeFrame(descriptor, 2, 30));
CHECK_TRUE(subscription.discardPendingIfExceeds(2) == 3);
CHECK_TRUE(!subscription.tryRead().has_value());
producer(makeFrame(descriptor, 3, 40));
const auto next = subscription.tryRead();
CHECK_TRUE(next.has_value());
CHECK_TRUE(next->value->sequence == 3);
CHECK_TRUE(next->dropped_since_last_read == 3);
CHECK_TRUE(subscription.droppedCount() == 3);
}
void testHubFailedStartAndShutdown() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<int> start_attempts{0};
std::atomic<int> retry_stop_count{0};
MediaSourceHub::SourceCallbacks failed_callbacks;
failed_callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return ++start_attempts >= 2;
};
failed_callbacks.stop = [&] { ++retry_stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(failed_callbacks), 2));
auto failed = hub.subscribe(descriptor->id);
CHECK_TRUE(!failed.valid());
auto retry = hub.subscribe(descriptor->id);
CHECK_TRUE(retry.valid());
retry.reset();
CHECK_TRUE(start_attempts == 2);
CHECK_TRUE(retry_stop_count == 1);
CHECK_TRUE(hub.unregisterSource(descriptor->id));
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return true;
};
callbacks.stop = [&] { ++stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto live = hub.subscribe(descriptor->id);
CHECK_TRUE(live.valid());
hub.shutdown();
CHECK_TRUE(stop_count == 1);
CHECK_TRUE(!live.valid());
CHECK_TRUE(!live.waitRead(50ms).has_value());
}
void testKeyFrameRequestIsOrderedBeforeStop() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1);
std::atomic<bool> key_frame_entered{false};
std::atomic<bool> release_key_frame{false};
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return true;
};
callbacks.stop = [&] { ++stop_count; };
callbacks.request_key_frame = [&] {
key_frame_entered.store(true, std::memory_order_release);
while (!release_key_frame.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(1ms);
}
return true;
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription = hub.subscribe(descriptor->id);
CHECK_TRUE(subscription.valid());
auto key_frame = std::async(std::launch::async, [&] {
return hub.requestKeyFrame(descriptor->id);
});
const auto enter_deadline = std::chrono::steady_clock::now() + 500ms;
while (!key_frame_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < enter_deadline) {
std::this_thread::sleep_for(1ms);
}
CHECK_TRUE(key_frame_entered.load(std::memory_order_acquire));
auto stop = std::async(std::launch::async, [&] { subscription.reset(); });
CHECK_TRUE(stop.wait_for(20ms) == std::future_status::timeout);
CHECK_TRUE(stop_count.load(std::memory_order_acquire) == 0);
release_key_frame.store(true, std::memory_order_release);
CHECK_TRUE(key_frame.get());
CHECK_TRUE(stop.wait_for(500ms) == std::future_status::ready);
stop.get();
CHECK_TRUE(stop_count.load(std::memory_order_acquire) == 1);
CHECK_TRUE(!hub.requestKeyFrame(descriptor->id));
}
void testHubCancelsBlockedStartWithoutBlockingShutdown() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<bool> start_entered{false};
std::atomic<bool> start_exited{false};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate& cancelled) {
start_entered.store(true, std::memory_order_release);
while (!cancelled()) {
std::this_thread::sleep_for(2ms);
}
start_exited.store(true, std::memory_order_release);
return false;
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription_future = std::async(std::launch::async, [&] {
return hub.subscribe(descriptor->id);
});
const auto start_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < start_deadline) {
std::this_thread::sleep_for(2ms);
}
auto shutdown_future = std::async(std::launch::async, [&] { hub.shutdown(); });
const bool shutdown_completed = shutdown_future.wait_for(500ms) ==
std::future_status::ready;
if (shutdown_completed) shutdown_future.get();
const bool subscribe_completed = subscription_future.wait_for(500ms) ==
std::future_status::ready;
bool invalid_subscription = false;
if (subscribe_completed) {
invalid_subscription = !subscription_future.get().valid();
}
const auto exit_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_exited.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < exit_deadline) {
std::this_thread::sleep_for(2ms);
}
CHECK_TRUE(start_entered.load(std::memory_order_acquire));
CHECK_TRUE(shutdown_completed);
CHECK_TRUE(subscribe_completed);
CHECK_TRUE(invalid_subscription);
CHECK_TRUE(start_exited.load(std::memory_order_acquire));
}
void testHubQuarantinesNonCooperativeStart() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<bool> start_entered{false};
std::atomic<bool> release_start{false};
std::atomic<bool> start_exited{false};
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
start_entered.store(true, std::memory_order_release);
while (!release_start.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(2ms);
}
start_exited.store(true, std::memory_order_release);
return true;
};
callbacks.stop = [&] { ++stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription_future = std::async(std::launch::async, [&] {
return hub.subscribe(descriptor->id);
});
const auto start_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < start_deadline) {
std::this_thread::sleep_for(2ms);
}
const auto shutdown_started = std::chrono::steady_clock::now();
hub.shutdown();
const bool shutdown_was_bounded =
std::chrono::steady_clock::now() - shutdown_started < 500ms;
const bool subscribe_completed = subscription_future.wait_for(500ms) ==
std::future_status::ready;
bool invalid_subscription = false;
if (subscribe_completed) {
invalid_subscription = !subscription_future.get().valid();
}
// Release the deliberately non-cooperative test callback before its stack
// captures go out of scope. Late successful startup must be stopped once.
release_start.store(true, std::memory_order_release);
const auto exit_deadline = std::chrono::steady_clock::now() + 500ms;
while ((!start_exited.load(std::memory_order_acquire) || stop_count.load() != 1) &&
std::chrono::steady_clock::now() < exit_deadline) {
std::this_thread::sleep_for(2ms);
}
CHECK_TRUE(start_entered.load(std::memory_order_acquire));
CHECK_TRUE(shutdown_was_bounded);
CHECK_TRUE(subscribe_completed);
CHECK_TRUE(invalid_subscription);
CHECK_TRUE(start_exited.load(std::memory_order_acquire));
CHECK_TRUE(stop_count.load() == 1);
}
} // namespace
int main() {
testMediaMetadataValidation();
testLegacySpmcCompatibility();
testBroadcastFrameRing();
testBroadcastDiscardPending();
testBroadcastDiscardConcurrentPublish();
testBroadcastConcurrency();
testHubLifecycleAndDescriptorRefresh();
testSubscriptionDiscardPending();
testHubFailedStartAndShutdown();
testKeyFrameRequestIsOrderedBeforeStop();
testHubCancelsBlockedStartWithoutBlockingShutdown();
testHubQuarantinesNonCooperativeStart();
if (failures != 0) {
std::cerr << failures << " media_source_hub checks failed\n";
return 1;
}
std::cout << "media_source_hub self-test passed\n";
return 0;
}

View File

@ -36,6 +36,10 @@ const char* taskConfigTypeToString(const config::TaskConfigEntry::TaskType type)
return "TASK_TYPE_TOUCH_SCREEN";
case config::TaskConfigEntry::TASK_TYPE_GRPC_SERVER:
return "TASK_TYPE_GRPC_SERVER";
case config::TaskConfigEntry::TASK_TYPE_SELF_COLLISION:
return "TASK_TYPE_SELF_COLLISION";
case config::TaskConfigEntry::TASK_TYPE_QUIC_EDGE:
return "TASK_TYPE_QUIC_EDGE";
case config::TaskConfigEntry::TASK_TYPE_UNKNOWN:
default:
return "TASK_TYPE_UNKNOWN";

View File

@ -12,6 +12,7 @@ target_link_libraries(cmvr_runtime PUBLIC
cmvr_es::device_manager
cmvr_es::task_manager
cmvr_es::service
cmvr_es::quic_edge_task
cmvr_es::mujoco_viewer
)

View File

@ -11,6 +11,7 @@
#include "common/config/config_files.h"
#include "common/io/proto_file_io.h"
#include "task/grpc_server_task/include/grpc_server_task.h"
#include "task/quic_edge_task/include/quic_edge_task.h"
namespace cmvr {
namespace {
@ -102,6 +103,7 @@ bool Runtime::init_(const std::string& config_path,
device::DeviceManager::getInstance(device_manager_root.device_manager());
task::registerGrpcServerTaskFactory();
task::registerQuicEdgeTaskFactory();
if (app_config.task_manager_config_file().empty()) {
CMVR_LOG(ERROR) << "TaskManager config file is empty";

View File

@ -7,6 +7,7 @@ add_library(service
grpc/src/grpc_head_service.cpp
grpc/src/grpc_dexhand_service.cpp
grpc/src/grpc_arm_service.cpp
grpc/src/grpc_agv_service.cpp
grpc/src/grpc_hlc_service.cpp
../task/grpc_server_task/src/grpc_server_task.cpp
)
@ -20,12 +21,29 @@ target_link_libraries(service PRIVATE
cmvr_es::task_manager
cmvr_es::algorithms::controller
cmvr_es::task
cmvr_es::media_source_hub
cmvr_es::device_media_source_adapter
protobuf::libprotobuf
)
add_library(cmvr_es::service ALIAS service)
install(TARGETS service LIBRARY DESTINATION lib)
if(BUILD_TESTING)
add_executable(grpc_camera_stream_policy_test
grpc/tests/grpc_camera_stream_policy_test.cpp
)
target_include_directories(grpc_camera_stream_policy_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
)
add_test(
NAME grpc_camera_stream_policy_test
COMMAND grpc_camera_stream_policy_test
)
set_tests_properties(grpc_camera_stream_policy_test PROPERTIES TIMEOUT 10)
endif()
# --------------------------------------------------------
# Unit test
# --------------------------------------------------------

233
cmvr-es/service/README.md Normal file
View File

@ -0,0 +1,233 @@
# Service 模块开发指南
`service/` 实现边缘端对外协议和设备抽象之间的适配。Service 负责解析请求、查找设备、转换 DTO 和返回结果,不负责创建具体设备后端。
返回[项目总览](../../README.md)。
## 当前结构
| 目录 | 职责 |
| --- | --- |
| `grpc/` | 入站设备控制、状态查询和兼容流式接口 |
| `quic_edge/` | 边缘端主动连接平台的 QUIC client、控制状态机和媒体 packetizer |
| `quic_edge/tests/` | 已登记到 CTest 的 QUIC 协议测试 |
两个遗留 gRPC client test 位于 `grpc/src/*_client_test.cpp`,当前没有通过 `add_test()` 登记。
gRPC 和 QUIC 的职责边界:
- 机械臂、AGV 等可靠控制继续使用 gRPC
- 节点注册、心跳和 IP 上报使用 QUIC reliable stream
- 实时音视频使用 QUIC DATAGRAM
- `quic_edge/` 不是平台 Gateway也不是浏览器服务器。
## 新增 gRPC Service
当前没有动态 service registry必须完成以下全部步骤。
### 1. 定义 Proto
在 [`../../protos/cmvr/api/`](../../protos/cmvr/api/) 增加或扩展:
- `<domain>_command.proto`
- `<domain>_service.proto`
import 路径必须相对于 `protos/`。兼容规则见 [`../../protos/README.md`](../../protos/README.md)。
### 2. 实现 Service
目录约定:
```text
service/grpc/
├── include/grpc_example_service.h
└── src/grpc_example_service.cpp
```
实现类继承生成的:
```cpp
cmvr::api::ExampleService::Service
```
通过已初始化的 `DeviceManager` 获取抽象设备。不要在 service 中创建厂商 SDK 对象,不要绕过设备 factory。
### 3. 加入 service target
将实现 `.cpp` 加入 [`CMakeLists.txt`](CMakeLists.txt) 的 `service` library并声明最小依赖。
### 4. 注册到 GrpcServerTask
还必须修改:
- [`../task/grpc_server_task/include/grpc_server_task.h`](../task/grpc_server_task/include/grpc_server_task.h)
- [`../task/grpc_server_task/src/grpc_server_task.cpp`](../task/grpc_server_task/src/grpc_server_task.cpp)
完成:
1. 增加 service owner
2. 在 start 中构造;
3. 调用 `builder.RegisterService(...)`
4. 在 `clearServices()` 中 reset。
漏掉该步骤时项目可能编译成功,但服务不会出现在 reflection 或运行时。
### 5. 测试
- 直接测试 service handler 或启动临时 gRPC server
- 覆盖设备不存在、类型不匹配、设备错误和取消;
- 使用 grpcurl/reflection 验证服务全名;
- 流式 RPC 覆盖客户端断开和慢消费者;
- 在 CMake 中使用 `if(BUILD_TESTING)` 包裹测试目标,并通过 `add_test()` 登记。
`grpc_arm_client_test``grpc_hlc_client_test` 是未登记到 CTest 的历史可执行文件,不能代表默认自动覆盖。
## gRPC 实现约束
### 错误语义
当前历史服务存在两种风格:
- gRPC status 返回 OK业务失败写入 Feedback header
- 使用非 OK gRPC status 表达 transport/API 失败。
扩展已有服务时保持其兼容语义。新增服务必须在设计时明确:
- 哪些错误使用 gRPC status
- 哪些错误使用业务 Feedback
- 是否允许部分成功;
- deadline/cancellation 如何映射;
- 不得同时返回互相矛盾的 transport 和业务状态。
### 流式 RPC
- 检查 `context->IsCancelled()`
- 检查 `Read()` / `Write()` 返回;
- 使用 RAII 或 MediaSourceHub Subscription 释放 producer lease
- 不持有设备状态锁进行网络写;
- 为 wait/read 使用有限 timeout
- 慢客户端不能阻塞设备生产线程;
- H.264/H.265 丢帧后等待关键帧恢复;
- gRPC RGB 流在积压超过 `camera_stream_max_pending_frames` 或帧龄超过
`camera_stream_max_frame_age_ms` 时主动丢弃旧帧,请求 IDR并从下一个关键帧恢复。
当前仅 gRPC RGB 和麦克风流使用 MediaSourceHubDepth/RGBD 仍直接读取设备帧。
gRPC 相机实时流默认最多保留 2 帧积压、最大允许 250 ms 帧龄。两个配置项填 0
时使用上述默认值。该策略以低延迟为目标,不保证每个视频帧都到达客户端;控制命令
仍由普通 gRPC RPC 承担。
`FrameData` 附带 `capture_utc_ns`、`source_sequence`、`pts`、`dts`、
`source_fps`、`source_timestamp` 和 `source_frame_number`。平台端可用采集时间
与接收时间的差值区分设备、网络、服务端写阻塞和客户端解码/渲染队列延迟。新增字段
保持 protobuf wire compatibility旧客户端可以继续连接但需要重新生成代码后才能
读取这些诊断字段。
### 当前安全状态
GrpcServerTask 使用同步 `grpc::ServerBuilder``grpc::InsecureServerCredentials()`。reflection 由配置控制。当前没有 gRPC TLS、认证、授权或标准 health service。
QUIC 配置中的 `grpc_endpoint_tls` 只是上报字段,不会启用 gRPC TLS。
## 扩展 QUIC Edge
关键层次:
| 层 | 主要文件 |
| --- | --- |
| 控制流 framing | `quic_edge/src/control_framing.cpp` |
| DATAGRAM 固定头和分片 | `quic_edge/src/datagram_packetizer.cpp` |
| 会话和状态机 | `quic_edge/src/quic_edge_service.cpp` |
| 传输抽象 | `quic_edge/include/quic_transport.h` |
| MsQuic 后端 | `quic_edge/src/msquic_transport.cpp` |
| 设备媒体适配 | `quic_edge/src/quic_edge_device_adapter.cpp` |
| DeviceManager 心跳适配 | `quic_edge/src/quic_edge_device_adapter.cpp` |
线协议见 [`../../protos/cmvr/quic_edge/v1/README.md`](../../protos/cmvr/quic_edge/v1/README.md)。
### 新增 Transport 后端
1. 实现 `QuicTransport` 完整接口;
2. 明确 callback 所在线程;
3. stop/close 后不得再访问已销毁 service
4. `QUEUED` 表示 transport 接管待发送数据;
5. `WOULD_BLOCK``ERROR` 不得接管任何字节;
6. DATAGRAM batch 本地准入必须原子;
7. native send 部分失败时关闭连接并清理 session
8. 添加 fake transport 故障注入测试;
9. 在默认 transport factory 中显式选择后端。
### 新增控制消息
不能只修改 Proto还要同步
- envelope 构造与发送;
- 入站 dispatch
- 合法状态和消息时序;
- message sequence 校验;
- session ID 和 heartbeat sequence 校验;
- reconnect 后状态清理;
- Java Gateway 对端;
- framing、状态机和 fake transport 测试。
当前 Edge 入站只接受:
- `NodeRegisterResponse`
- `NodeHeartbeatAck`
- `ProtocolError`
虽然 Proto 定义了 `MediaSessionClose`,本版本 Edge 收到它仍会判为 unexpected不应将其描述为已实现的双向控制能力。
### DeviceManager 心跳快照
`QuicEdgeService` 通过可注入的 `DeviceSnapshotProvider` 获取协议无关的纯值
快照。生产构造绑定已经初始化的 `DeviceManager`fake transport 测试则注入
合成快照,因此协议状态机不需要创建硬件对象或依赖 DeviceManager 单例。
DeviceManager 的本地快照继续保留禁用设备QUIC wire 映射层仅序列化
`enabled=true` 的设备。已启用但创建、初始化或启动失败的设备不会被过滤。
心跳线程只读取 Manager 维护的内存状态,不能在这里同步访问厂商 SDK、网络或
设备总线。新增设备健康探针必须实现 `AbstractDevice::healthSnapshot()`
线程安全、无阻塞 I/O 契约;未实现时上报 `UNSPECIFIED`,不得伪造为健康。
设备异常字符串会限长,整条消息仍受 `maximum_control_frame_bytes` 约束。
### 跨 QUIC 通道顺序
Edge 会先调用可靠流发送 session/descriptor再调用 DATAGRAM 发送媒体,但 QUIC stream 与 DATAGRAM 没有跨通道到达顺序保证。
Gateway 必须容忍 DATAGRAM 先到,对未知 session epoch 或 codec generation 的数据有界暂存或丢弃。
## QUIC 测试要求
参考 [`quic_edge/CMakeLists.txt`](quic_edge/CMakeLists.txt) 和 `quic_edge_protocol_test`,至少覆盖:
- 控制消息拆包、粘包和超限;
- 重复或倒退 sequence
- 注册、ACK 超时和重连;
- DeviceManager 已启用设备过滤、类型/状态映射和 provider 失败隔离;
- Gateway 返回零心跳周期时采用本地 `heartbeat_interval_ms`
- session epoch 清理;
- DATAGRAM header 字节序;
- 分片边界和超大帧;
- 原子队列准入和背压;
- 丢帧、generation 和关键帧恢复;
- stop 与 callback 并发。
```bash
cmake --build build --target quic_edge_protocol_test
ctest \
--test-dir build \
-R '^quic_edge_protocol_test$' \
--output-on-failure
```
## 提交检查
- [ ] service 不创建具体硬件后端
- [ ] Proto、实现、CMake 和 GrpcServerTask 注册均已更新
- [ ] 错误语义与已有服务兼容
- [ ] deadline、cancel、Read/Write 失败均处理
- [ ] 流式资源通过 RAII 释放
- [ ] gRPC 安全能力没有被配置字段误描述
- [ ] QUIC 状态机与 Gateway 同步更新
- [ ] 协议测试已登记到 CTest

View File

@ -0,0 +1,82 @@
#ifndef CMVR_ES_GRPC_AGV_SERVICE_H
#define CMVR_ES_GRPC_AGV_SERVICE_H
#include "cmvr/api/agv_service.grpc.pb.h"
#include "devices/agv/abstract_agv.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr::service {
class gRPCAgvServiceImpl final : public api::AgvService::Service {
public:
gRPCAgvServiceImpl();
~gRPCAgvServiceImpl() override = default;
grpc::Status getRuntimeState(grpc::ServerContext* context,
const api::AgvRuntimeStateCommand_Request* request,
api::AgvRuntimeStateCommand_Feedback* response) override;
grpc::Status getNavigationStatus(grpc::ServerContext* context,
const api::AgvNavigationStatusCommand_Request* request,
api::AgvNavigationStatusCommand_Feedback* response) override;
grpc::Status emergencyStop(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status clearFault(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status navigateToPose(grpc::ServerContext* context,
const api::AgvNavigateToPoseCommand_Request* request,
api::AgvNavigateToPoseCommand_Feedback* response) override;
grpc::Status navigateToStation(grpc::ServerContext* context,
const api::AgvNavigateToStationCommand_Request* request,
api::AgvNavigateToStationCommand_Feedback* response) override;
grpc::Status followPath(grpc::ServerContext* context,
const api::AgvFollowPathCommand_Request* request,
api::AgvFollowPathCommand_Feedback* response) override;
grpc::Status pauseNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status resumeNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status cancelNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status setVelocity(grpc::ServerContext* context,
const api::AgvSetVelocityCommand_Request* request,
api::AgvSetVelocityCommand_Feedback* response) override;
grpc::Status stopVelocityControl(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
grpc::Status listMaps(grpc::ServerContext* context,
const api::AgvListMapsCommand_Request* request,
api::AgvListMapsCommand_Feedback* response) override;
grpc::Status listStations(grpc::ServerContext* context,
const api::AgvListStationsCommand_Request* request,
api::AgvListStationsCommand_Feedback* response) override;
grpc::Status switchMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response) override;
grpc::Status uploadMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response) override;
grpc::Status downloadMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response) override;
grpc::Status startMapping(grpc::ServerContext* context,
const api::AgvStartMappingCommand_Request* request,
api::AgvStartMappingCommand_Feedback* response) override;
grpc::Status streamMap(grpc::ServerContext* context,
const api::AgvMapStreamCommand_Request* request,
grpc::ServerWriter<api::AgvMapStreamCommand_Feedback>* writer) override;
grpc::Status stopMapping(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response) override;
private:
device::DeviceManager& dmgr_;
};
} // namespace cmvr::service
#endif // CMVR_ES_GRPC_AGV_SERVICE_H

View File

@ -50,6 +50,9 @@ public:
grpc::Status computeForwardKinematics(grpc::ServerContext* context,
const api::ComputeForwardKinematics_Request* request,
api::ComputeForwardKinematics_Response* response) override;
grpc::Status clearFault(grpc::ServerContext *context,
const cmvr::api::CommandHeader_Request *request,
cmvr::api::CommandHeader_Feedback *response) override;
private:
device::DeviceManager& dmgr_;

Some files were not shown because too many files have changed in this diff Show More