diff --git a/README.md b/README.md index 5f5ead7..cdf4832 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回 ## CAN/CANFD 收发与四门位置显示 -实时运行默认打开参考工程 `/home/orin/vision-anti-pinch` 使用的 USBCANFD(设备类型 33、设备索引 0),启用 CH0/CH1:60 MHz 时钟、仲裁域 500 kbps、数据域 2 Mbps。一个合并接收线程读取两个通道,默认启用终端电阻。可在 `config/camera.json` 的 `can` 中设置 `enabled`、`device_index`、`timeout_ms`(默认 1000 ms)和 `termination`;`--no-can` 显式关闭实时 CAN。设备不可用时显示 UNAVAILABLE,摄像头推理继续;接收 API 失败显示 ERROR,需检查设备后重启程序。 +实时运行默认打开参考工程 `/home/orin/vision-anti-pinch` 使用的 USBCANFD(设备类型 33、设备索引 0),仅启用 CH0:60 MHz 时钟、仲裁域 500 kbps、数据域 2 Mbps。一个接收线程读取 CH0,默认启用终端电阻。可在 `config/camera.json` 的 `can` 中设置 `enabled`、`device_index`、`timeout_ms`(默认 1000 ms)和 `termination`;`--no-can` 显式关闭实时 CAN。设备不可用时显示 UNAVAILABLE,摄像头推理继续;接收 API 失败显示 ERROR,需检查设备后重启程序。 首次使用 USB-CAN 时,若节点为 root:root 且普通用户不可写,可运行 `./scripts/enable_can_access.sh`(需要 sudo)。它只为 `3068:0009` 适配器安装 `plugdev` 组读写规则并刷新设备权限;运行用户须属于 `plugdev`,新加组后需重新登录。无需以 root 运行整个推理程序。 @@ -151,15 +151,15 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回 | 2 | LR | 左后门实际位置反馈 | | 3 | RR | 右后门实际位置反馈 | -显示值是无符号原始字节 0–255,尚无物理单位、缩放或无效值定义,因此不标为百分比、不把 255 擅自视为无效。接收 CAN/CANFD 数据帧,过滤远程帧、错误帧和发送回显;少于 4 字节的 `0x391` 不更新信号。修正了参考代码 `get_bit_value(0/1/2/3, 8)` 导致的位域重叠,字节起始位应为 0/8/16/24。 +显示值是无符号原始字节 0–255,尚无物理单位、缩放或无效值定义,因此不标为百分比、不把 255 擅自视为无效。CH0 接收 CAN/CANFD 数据帧,过滤远程帧、错误帧和发送回显;少于 4 字节的 `0x391` 不更新信号。修正了参考代码 `get_bit_value(0/1/2/3, 8)` 导致的位域重叠,字节起始位应为 0/8/16/24。 -默认预览为两个通道分别显示四门位置、LIVE/STALE/WAIT、新鲜度及帧数,并显示总 RX/TX 和错误计数。尚未接收到值时显示 `--`,超时保留最后值但标记 STALE;两个通道互不覆盖。即使相机正在重试或暂无新推理帧,CAN 预览仍约每 100 ms 刷新。CAN 只提供状态显示和下游数据,本次没有新增基于门位置的推理启停或风险门控。 +默认预览显示 CH0 四门实际位置反馈、LIVE/STALE/WAIT、新鲜度及帧数,并显示总 RX/TX 和错误计数。尚未接收到值时显示 `--`,超时保留最后值但标记 STALE;CH1 不初始化、不收发,实时预览不显示 CH1。即使相机正在重试或暂无新推理帧,CAN 预览仍约每 100 ms 刷新。CAN 只提供状态显示和下游数据,本次没有新增基于门位置的推理启停或风险门控。 下游通过 `pipeline.canStatus()` 读取当前状态;`pipeline.latest()->can` 是该推理结果生成时的快照。按需发送使用线程安全的 `pipeline.sendCan()`: ```cpp cms::CanFrame frame; -frame.channel = 0; // CH0 或 CH1 +frame.channel = 0; // 仅 CH0 frame.id = 0x123; // 按实际协议设置 frame.data = {0x01, 0x02}; // 示例数据,应替换为实际报文 bool sent = pipeline.sendCan(frame); @@ -169,10 +169,16 @@ bool sent = pipeline.sendCan(frame); // 标准 ID 最大 0x7FF,扩展 ID 最大 0x1FFFFFFF。 ``` -非法通道/ID/长度/标志抛出 `std::invalid_argument`;设备未就绪或 SDK 发送失败返回 false,发送计数以 SDK 返回为准。接口不自动填充/截断数据,不自动发送周期或风险控制报文。发送调用与设备关闭互斥;双通道接收保持独立线程运行。 +非法通道/ID/长度/标志抛出 `std::invalid_argument`;设备未就绪或 SDK 发送失败返回 false,发送计数以 SDK 返回为准。`sendCan()` 接口不自动填充/截断数据。发送调用与设备关闭互斥;CH0 接收保持独立线程运行。 + +实时推理每产生一个新结果,会将风险提交给后台 CAN 发送线程,由它通过 CH0 发送一帧标准 CAN 数据报文 `0x1F6`,DLC 固定为 8。`data[0]` 的低四位(bit0–3)和高四位(bit4–7)同时填写相同的二值风险:`risk_level == "clear"` 时各为 0,其余状态(包括 `watch`、`warning`、`danger`、`unavailable`)各为 1;后七字节全部为 0。即 `clear` 发送 `00 00 00 00 00 00 00 00`,其余发送 `11 00 00 00 00 00 00 00`(十六进制)。无人或证据不足导致的 `unavailable` 也在高、低四位各发送 1。 + +发送随推理结果触发,没有独立周期;无新推理结果时不重发。SDK 发送等待不占用推理线程;后台最多保留一个待发结果,发送变慢时用最新结果替换尚未发送的结果,避免积压旧风险,并非保证每个推理帧都上总线。CH0 发送失败通过现有 TX 错误计数呈现;结果中的 CAN 快照记录采样时已完成的发送计数,不保证包含当前帧的发送。停止时丢弃待发结果,并等待已经进入 SDK 的调用返回后释放设备。`--no-can` 或 `can.enabled=false` 禁用自动发送,视频回灌始终不发送。按需 `sendCan()` 仍为同步 API。 结果 JSON 和录制会话自动包含每帧 `can` 快照。视频回灌不打开 USB-CAN,不发送报文,直接恢复录制时的状态并标记 `source=replay`;旧会话没有 CAN 字段时显示 NOT_RECORDED。回灌调用 `sendCan()` 会明确拒绝。 依赖 `third_party/zlg/zcan.h` 和 `orin/lib/` 中的参考工程 SDK 库(含 SONAME 链接),独立包已包含它们。纯模拟收发回归:`./tests/run_can.sh`;会话回归同时覆盖 CAN 快照保存。模拟测试不代表物理总线收发验证。 -本次检查范围与延期项目见 [CAN 实现记录](tests/ORIN_CAN_REPORT.md);按当前要求暂不验证真实 CAN 接收。 +本次检查范围与延期项目见 [CAN 实现记录](tests/ORIN_CAN_REPORT.md);CH0 实机排查与单通道修复见 [0x391 实际位置反馈检查](tests/ORIN_391_REPORT.md)。 + +新增 `0x1F6` 二值风险发送的检查范围及部署状态见 [风险发送检查记录](tests/RISK_CAN_REPORT.md)。 diff --git a/include/can_bus.hpp b/include/can_bus.hpp index cfcdd32..0fda9f0 100644 --- a/include/can_bus.hpp +++ b/include/can_bus.hpp @@ -1,6 +1,7 @@ #pragma once #include "can_types.hpp" #include +#include namespace cms { // One lifecycle per instance; only one owner per device index in a process. @@ -19,6 +20,15 @@ public: // Thread-safe on-demand data-frame send; invalid frames throw, SDK failure // returns false. Never generates periodic or inference-triggered messages. bool send(CanFrame const& frame); + // Send one standard 8-byte 0x1F6 frame on CH0. Both nibbles of + // data[0] contain 0 for "clear", 1 for every other level (including + // unavailable): 0x00 or 0x11. Bytes 1-7 are zero. Returns {CH0 result, false}; + // the second slot is retained for API compatibility, CH1 is never sent. + std::array sendRisk(std::string_view riskLevel); + // Nonblocking live-inference path. Keeps at most one pending risk result; + // a newer result replaces one waiting behind a slow SDK send. Returns + // queue acceptance, not delivery. No timer or retransmission of old risk. + bool queueRisk(std::string_view riskLevel); private: struct Impl; std::unique_ptr impl_; diff --git a/include/can_types.hpp b/include/can_types.hpp index 9b6625f..8179283 100644 --- a/include/can_types.hpp +++ b/include/can_types.hpp @@ -5,6 +5,7 @@ #include namespace cms { +inline constexpr unsigned liveCanChannelCount=1; // CH0 only; CH1 is not used. struct CanConfig { bool enabled{true}; int device_index{0}; @@ -22,6 +23,7 @@ struct CanDoorSignals { }; struct CanSnapshot { std::string source{"live"},status{"DISABLED"}; + // Keep two slots for existing recorded sessions; live CH1 stays unset. std::array channels; std::uint64_t received{},transmitted{},transmit_errors{},receive_errors{}; }; diff --git a/orin/README.md b/orin/README.md index 9820d90..39b2c23 100644 --- a/orin/README.md +++ b/orin/README.md @@ -54,8 +54,10 @@ bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查 ## CAN 状态与发送接口 -实时运行默认接收 USBCANFD 双通道(500 kbps / 2 Mbps)。预览新增 `0x391` 的 LF/RF/LR/RR 四门原始位置值,每通道分别显示,并标记新鲜度和收发计数。`--no-can` 可关闭;设备缺失不停止推理。`config/camera.json` 的 `can.termination` 控制终端电阻,默认沿用参考工程的开启设置。 +实时运行默认仅接收 USBCANFD CH0(500 kbps / 2 Mbps)。预览新增 `0x391` 的 LF/RF/LR/RR 四门实际位置反馈原始值,仅显示 CH0,并标记新鲜度和收发计数。`--no-can` 可关闭;设备缺失不停止推理。`config/camera.json` 的 `can.termination` 控制终端电阻,默认沿用参考工程的开启设置。 `lib/` 包含 SDK 与 libusb 及其 SONAME 链接。回灌使用录制的 CAN 快照,不访问总线;旧会话显示 NOT_RECORDED。按需发送通过 C++ `CameraPipeline::sendCan()`,参数与完整示例见工程 README。 +每个新的实时推理结果提交到后台 CAN 发送线程,仅在 CH0 发送一帧标准 CAN `0x1F6`,长度 8 字节:首字节高、低四位同时填写相同风险值,`clear` 时各为 0,其余状态(包括 `unavailable`)各为 1,即首字节为 `0x00` 或 `0x11`,后七字节清零。CH1 不初始化、不收发。发送等待不阻塞推理,最多保留一个待发结果,忙时由最新结果替换;发送失败计入 TX 错误。无新结果时不重发,`--no-can` 和视频回灌禁用发送。停止时清空待发结果,等待正在执行的 SDK 调用结束后释放设备。 + USB-CAN 首次接入如出现权限不足,运行 `./scripts/enable_can_access.sh`(需要 sudo,运行用户属于 plugdev),仅为 `3068:0009` 适配器安装设备访问规则。 diff --git a/orin/SHA256SUMS b/orin/SHA256SUMS index 9b353ff..35cd798 100644 --- a/orin/SHA256SUMS +++ b/orin/SHA256SUMS @@ -1,7 +1,7 @@ 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko -e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461 ./build/camera_risk +418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f ./build/camera_risk d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json 9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine @@ -10,7 +10,7 @@ f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo 8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so.0 ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so.1.0.12 -bb61f669d8b471c5ca42045b31a0e6fd300235da37a80893ae229f37deb1b7b9 ./README.md +584875c5a52f969da7ae50514fd627749123f3db7140058052055982a6c01f2f ./README.md 7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh 41ebe2dcf57674232ca137d024e99447b467049cb1c63a284daae274296cb78c ./scripts/99-usbcanfd.rules 920102fc5e91bde9b78f6f7c5cb2ff8761a0c39b63c7a28b3347d5de5fe7c60a ./scripts/enable_can_access.sh diff --git a/orin/build/camera_risk b/orin/build/camera_risk index 5bff899..baf97c7 100755 Binary files a/orin/build/camera_risk and b/orin/build/camera_risk differ diff --git a/src/camera_pipeline.cpp b/src/camera_pipeline.cpp index d7f1b2f..e491467 100644 --- a/src/camera_pipeline.cpp +++ b/src/camera_pipeline.cpp @@ -173,7 +173,10 @@ struct CameraPipeline::Impl { if(recorded.contains("can"))result->can=deserializeCan(recorded.at("can")); else result->can.status="NOT_RECORDED"; result->can.source="replay"; - }else result->can=can->snapshot(); + }else { + if(!sessionReader&&config.can.enabled)can->queueRisk(result->risk.risk_level); + result->can=can->snapshot(); + } attachPoses(*result,fused); result->image=std::move(detected->frame.image);result->door_mask_pixels=doorPixels; result->door_mask_calibration_verified=config.door_mask_calibration_verified; diff --git a/src/can_bus.cpp b/src/can_bus.cpp index 1cfdd53..28ebd08 100644 --- a/src/can_bus.cpp +++ b/src/can_bus.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -19,7 +20,7 @@ std::uint64_t nowNs() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } void validate(CanFrame const& f) { - if(f.channel<0||f.channel>1||f.id>(f.extended?0x1fffffffu:0x7ffu)||(!f.fd&&f.bitrate_switch)) + if(f.channel<0||unsigned(f.channel)>=liveCanChannelCount||f.id>(f.extended?0x1fffffffu:0x7ffu)||(!f.fd&&f.bitrate_switch)) throw std::invalid_argument("Invalid CAN channel, identifier or BRS flag"); auto size=f.data.size(); if((!f.fd&&size>8)||(f.fd&&!(size<=8||size==12||size==16||size==20||size==24||size==32||size==48||size==64))) @@ -29,13 +30,14 @@ void validate(CanFrame const& f) { struct CanBus::Impl { CanConfig config; mutable std::mutex stateMutex; - std::mutex lifecycleMutex,waitMutex; - std::condition_variable wake; + std::mutex lifecycleMutex,waitMutex,riskMutex; + std::condition_variable wake,riskWake; + std::optional pendingRisk; CanSnapshot state; std::atomic stopped{false}; bool started{},opened{},owned{}; std::array initialized{}; - std::thread receiver; + std::thread receiver,riskSender; explicit Impl(CanConfig c):config(c) { if(c.device_index<0||c.timeout_ms<=0)throw std::invalid_argument("Invalid CAN configuration"); state.status=c.enabled?"STARTING":"DISABLED"; @@ -44,7 +46,7 @@ struct CanBus::Impl { void setStatus(char const* value) {std::lock_guard lock(stateMutex);state.status=value;} void releaseDevice() noexcept { if(opened) { - for(unsigned i=0;i<2;++i)if(initialized[i])VCI_ResetCAN(deviceType,config.device_index,i); + for(unsigned i=0;i lock(riskMutex);pendingRisk.reset();} + riskWake.notify_all(); + } void join() noexcept { requestStop(); + // A sender may be inside send() holding lifecycleMutex. Join it before + // taking that mutex or closing the device. + if(riskSender.joinable())riskSender.join(); std::lock_guard lock(lifecycleMutex); if(receiver.joinable())receiver.join(); releaseDevice(); @@ -100,6 +112,7 @@ struct CanBus::Impl { std::lock_guard lock(stateMutex); // Ignore remote, error and transmit-echo frames; they are not input. if(header.inf.sdf||header.inf.err||header.inf.tx)return; + if(channel>=liveCanChannelCount)return; if(channel>1||header.len>(fd?64:8)||header.id>(header.inf.sef?0x1fffffffu:0x7ffu)) { ++state.receive_errors;return; } @@ -123,6 +136,7 @@ struct CanBus::Impl { } for(unsigned i=0;i=liveCanChannelCount)continue; if(item.dataType==ZCAN_DT_ZCAN_CAN_DATA)receiveFrame(item.data.zcanCANData.hdr,item.data.zcanCANData.dat,false,item.chnl); else if(item.dataType==ZCAN_DT_ZCAN_CANFD_DATA)receiveFrame(item.data.zcanCANFDData.hdr,item.data.zcanCANFDData.dat,true,item.chnl); else if(item.dataType==ZCAN_DT_ZCAN_ERROR_DATA){std::lock_guard lock(stateMutex);++state.receive_errors;} @@ -139,6 +153,29 @@ struct CanBus::Impl { } return result; } + bool queueRisk(std::string_view level) { + std::lock_guard lock(riskMutex); + {std::lock_guard stateLock(stateMutex); + if(stopped||state.status!="OPEN")return false; + } + pendingRisk=level!="clear";riskWake.notify_one();return true; + } + void riskLoop() noexcept { + try { + CanFrame frame;frame.id=0x1f6;frame.data.assign(8,0); + while(!stopped) { + { + std::unique_lock lock(riskMutex); + riskWake.wait(lock,[this]{return stopped||pendingRisk.has_value();}); + if(stopped)break; + frame.data[0]=*pendingRisk?0x11:0x00;pendingRisk.reset(); + } + for(unsigned channel=0;channel lock(lifecycleMutex); @@ -168,4 +205,14 @@ void CanBus::requestStop()noexcept{impl_->requestStop();} void CanBus::join()noexcept{impl_->join();} CanSnapshot CanBus::snapshot()const{return impl_->snapshot();} bool CanBus::send(CanFrame const& f){return impl_->send(f);} +bool CanBus::queueRisk(std::string_view level){return impl_->queueRisk(level);} +std::array CanBus::sendRisk(std::string_view riskLevel) { + CanFrame frame;frame.id=0x1f6;frame.data.assign(8,0); + frame.data[0]=riskLevel=="clear"?0x00:0x11; + std::array sent{}; + for(unsigned channel=0;channel=argc)throw std::invalid_argument("--config needs a file");configPath=argv[i];}}, - {"--help",[](int&){std::cout<<"camera_risk [--record-session NEW_DIR] [--replay-session DIR] [--save-original] [--save-annotated] [--save-results] [--config FILE] [--no-can] [--visualize|--no-visualize]\nLive CAN/CANFD reception is enabled by default; replay uses recorded CAN only.\nVisualization is enabled by default for live inference, recording and replay.\nContinuous camera inference; replay runs to EOF without a camera.\nH.264 sessions require a new recording directory. Stop with Ctrl+C or SIGTERM to finalize video.\n";std::exit(0);}} + {"--help",[](int&){std::cout<<"camera_risk [--record-session NEW_DIR] [--replay-session DIR] [--save-original] [--save-annotated] [--save-results] [--config FILE] [--no-can] [--visualize|--no-visualize]\nLive CAN/CANFD reception and per-result 0x1F6 binary risk transmission on CH0 only are enabled by default.\nRisk: clear=0, all other levels=1; --no-can disables both reception and transmission.\nReplay uses recorded CAN only and never transmits.\nVisualization is enabled by default for live inference, recording and replay.\nContinuous camera inference; replay runs to EOF without a camera.\nH.264 sessions require a new recording directory. Stop with Ctrl+C or SIGTERM to finalize video.\n";std::exit(0);}} }; for(int i=1;isecond(i);} diff --git a/src/result_renderer.cpp b/src/result_renderer.cpp index 5fd15ad..1cc758c 100644 --- a/src/result_renderer.cpp +++ b/src/result_renderer.cpp @@ -25,11 +25,12 @@ void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,b void renderCanOverlay(cv::Mat& canvas,CanSnapshot const& can) { cv::rectangle(canvas,{0,148},{canvas.cols,262},{20,24,30},-1); std::ostringstream header; - header<<"CAN 0x391 | "</exe`、`build/camera_risk` 和 `orin/build/camera_risk` SHA256 一致: + +`418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f` + +独立包可执行文件已更新,`orin/SHA256SUMS` 已刷新并完整校验。正式程序使用原命令 `./run_camera.sh`,SIGTERM 后约 0.868 秒退出、退出码 0。测试前未发现常驻 camera_risk,测试后没有保留临时采样进程;没有修改原启动参数。 + +硬件验证时,本机即 Orin 工作区,检出 `inference-only`,HEAD 为 `515a9f32d8c0ebf83503d4d240ffe917dc09950c`;本次联网读取 Git 远端 `inference-only` 为 `0192fccbf06cb928eac098b3dfad32abf0fc370d`。上述硬件验证在未提交工作区进行。经用户要求,最终修复、后台风险发送、部署产物及验证记录一并纳入本报告所在提交;Git 远端尚未推送同步。以上二进制哈希标识已验证的本地部署产物。 diff --git a/tests/ORIN_FPS_REPORT.md b/tests/ORIN_FPS_REPORT.md new file mode 100644 index 0000000..bc27944 --- /dev/null +++ b/tests/ORIN_FPS_REPORT.md @@ -0,0 +1,41 @@ +# 实时摄像头 FPS 修复(2026-09-28) + +> 以下为后台双通道发送与 FPS 修复阶段的历史验证记录,通道、二进制哈希与 Git 状态均指当时版本。最终 CH0 单通道实现和实机结果见 [0x391 修复记录](ORIN_391_REPORT.md)。 + +用户通过默认 `./run_camera.sh` 运行实时摄像头时卡顿。原因已在宿主 Orin 复现:`inferenceLoop()` 同步发送 CH0、CH1 的 `0x1F6`,SDK 发送失败路径两通道合计等待约 3 秒,使结果发布和后续推理一起停顿。 + +## 真实硬件对照 + +设备:Jetson AGX Orin、`/dev/video5` 1920×1536 30 FPS、现有 YOLO/RTMW TensorRT 引擎、ZLG USBCANFD-200U。保留配置、模型、CAN 波特率和终端电阻设置;未调整功耗或时钟。 + +使用链接相应版本 `camera_risk_core` 的临时采样程序,各运行约 15 秒,按实际 published 结果增量计算 FPS,并调用 `renderResult()` 绘制。该指标为真实推理输出吞吐,非相机配置 FPS;采样程序不调用 GUI 的 imshow/waitKey,不录制、不保存图片。 + +| 路径 | 稳定输出 | 风险融合及发送阶段 | 结果延迟 | CAN 状态 | +|---|---|---|---|---| +| 修复前,同步双通道 CAN | 约 0.33 FPS | 3016–3019 ms/帧 | 随积压升至约 12 秒 | OPEN,TX=0,发送错误递增 | +| 修复前,仅对照关闭 CAN | 29.93–30.00 FPS | 约 5–6.6 ms/帧 | 约 42–46 ms | DISABLED | +| 修复后,后台双通道 CAN | 29.97–30.00 FPS | 约 6–6.9 ms/帧 | 约 46–50 ms | OPEN,TX=0,发送错误递增 | + +首秒包含启动过渡,后两组约 27 FPS,不纳入稳定区间。画面绘制约 19 ms/帧。另做真实 TensorRT 视频回灌参考采样约 43–48 FPS,不能代替实时摄像头验证。 + +## 修改与模拟回归 + +实时推理改用 `queueRisk()`,不等待 SDK。独立发送线程处理一对双通道帧,最多保留一个待发风险,新的覆盖尚未发送的旧结果;没有无限队列或定时旧值重发。`0x1F6`、DLC 8、`clear→0x00`/其他→`0x11` 和后七字节清零的协议保持不变。按需 `sendCan()` 仍是同步接口。 + +停止时清空待发风险,唤醒后台线程,等待正在执行的 SDK 调用返回,再释放设备;不持有 SDK 生命周期锁等待发送线程,避免死锁。CAN 快照反映已完成的发送,不保证包括当前推理帧。 + +`tests/run_can.sh`(模拟 SDK + UBSan)通过,新增阻塞发送期间 1000 次提交、最新结果替换、双通道、失败和停止清理覆盖;构建及 `git diff --check` 通过。未进行板端故障注入。 + +## 正式程序和文件核对 + +经宿主进程检查确认测试前没有 `camera_risk`,最后按原命令 `./run_camera.sh` 启动正式程序,运行约 12 秒后 SIGTERM,退出码 0、停止约 1.267 秒。未改启动参数;测试后确认没有遗留进程。记录位于 `/tmp/vap-fps-default-verification.json` 和 `/tmp/vap-fps-default-run.log`;临时采样源码为 `/tmp/vap_fps_probe.cpp`。 + +实际 `/proc//exe`、`build/camera_risk`、`orin/build/camera_risk` SHA256 均为: + +`a84680513aa61fb2af353f97e6369bbef80c9f12521ecd88454e72fcecab447f` + +独立包 SHA256SUMS 已刷新并完整校验。修改留在本地工作区,未提交/推送;版本引用详情见 [风险发送记录](RISK_CAN_REPORT.md)。 + +## 未覆盖项 + +物理 CAN 发送依然失败,本次只确认该等待已与推理解耦,未确认 ACK、线缆、接收端或总线配置的故障原因。未使用独立总线分析仪验收线上载荷,也未测试物理断连重连或长期耐久。当前场景约 30 FPS 不代表任意人数/场景都能达到同样吞吐。 diff --git a/tests/RISK_CAN_REPORT.md b/tests/RISK_CAN_REPORT.md new file mode 100644 index 0000000..2c49a5a --- /dev/null +++ b/tests/RISK_CAN_REPORT.md @@ -0,0 +1,27 @@ +# 0x1F6 二值风险发送检查(2026-09-28) + +> 以下为双通道风险发送阶段的历史验证记录,通道、二进制哈希与 Git 状态均指当时版本。最终 CH0 单通道实现和实机结果见 [0x391 修复记录](ORIN_391_REPORT.md)。 + +实现位于当前 `inference-only` 工作区:每个新的实时推理结果提交给后台 CAN 发送线程,在 CH0、CH1 各尝试发送一帧标准 CAN 数据报文,ID `0x1F6`、DLC 8。首字节 bit0–3 和 bit4–7 同时填写相同风险值,`clear` 时各为 0,其他状态(含无人或证据不足时的 `unavailable`)各为 1,即首字节为 `0x00` 或 `0x11`,后七字节清零。最多保留一个待发结果,忙时由最新结果替换,不保证每个推理帧都上总线。回灌和禁用 CAN 时不自动发送,没有独立周期或旧结果重发。 + +## 本机模拟与构建 + +- `./tests/run_can.sh` 通过(UBSan、模拟 ZLG SDK):覆盖 clear/watch/warning/danger/unavailable/未知/空状态、高低四位相同赋值、恢复 clear 后两者同时清零、标准帧标志、ID、DLC、完整载荷、每结果双通道各一帧、任一通道失败不影响另一通道尝试、收发计数、停止和禁用后不访问发送 API。 +- 后台发送补充回归通过:模拟 SDK 阻塞时可连续提交 1000 个新风险并读取快照;释放阻塞后仅发送正在处理及最新待发的两对报文,没有旧结果积压;停止时丢弃待发结果,等待 SDK 返回后释放设备。单通道失败不抑制另一通道发送。 +- `./tests/run_session.sh` 通过:人工图像的 H.264 编解码、录制/回灌顺序、CAN 快照保存、停止收尾与损坏数据检查。这不是摄像头/TensorRT 验证,也不证明实际推理循环的总线发送。 +- `cmake --build build -j2` 通过;`git diff --check` 通过。 +- `--help` 显示新的发送规则并返回 0;SDK 初始化同时报告 NvRmMem/Nvmap 设备不可用,不能据此声称 GPU 推理可运行。 + +## 板端故障注入与真实硬件操作 + +后续经授权在宿主 Orin 上完成真实摄像头、TensorRT 和 USB-CAN 性能对照。原同步发送路径约 0.33 FPS,SDK 双通道等待约 3 秒且均失败;改为后台发送后约 30 FPS,CAN 仍报告发送失败。详见 [FPS 修复记录](ORIN_FPS_REPORT.md)。未进行板端故障注入或物理拔插。未修改启动配置和参数,测试前后均无常驻程序。 + +未覆盖:真实总线成功发送与 ACK、双通道线上载荷、回灌时物理总线无发送、断连重连、长期运行。模拟通过及 FPS 恢复不代表这些项目已验收。 + +## 文件同步与版本状态 + +已更新工作区独立包 `orin/build/camera_risk`,刷新 `orin/SHA256SUMS`,完整清单校验通过;源码构建文件与独立包二进制 SHA256 相同: + +`a84680513aa61fb2af353f97e6369bbef80c9f12521ecd88454e72fcecab447f` + +本次改动未提交、未推送,未进行 Git 发布。当前本地 HEAD 为 `515a9f32d8c0ebf83503d4d240ffe917dc09950c`;本地保存的 `origin/inference-only` 引用为 `0192fccbf06cb928eac098b3dfad32abf0fc370d`,未联网核对远端最新值。未修改共享采集、相机停止或恢复逻辑;其他两分支没有本次修改的 `src/can_bus.cpp`,未同步。已按原命令 `./run_camera.sh` 启动正式程序,核对 `/proc//exe`、构建文件和独立包哈希一致,SIGTERM 约 1.267 秒正常退出,恢复测试前无常驻进程状态。 diff --git a/tests/can_bus_test.cpp b/tests/can_bus_test.cpp index d847a5c..d63a3eb 100644 --- a/tests/can_bus_test.cpp +++ b/tests/can_bus_test.cpp @@ -18,6 +18,11 @@ std::deque incoming; std::atomic opens{0},closes{0},resets{0},mergeCalls{0},txCalls{0},rxCalls{0}; std::atomic unavailable{false},initFailure{false},receiveFailure{false},txFailure{false}; ZCAN_20_MSG sentCan{};ZCAN_FD_MSG sentFd{}; +std::vector sentCanFrames; +int failedTxChannel=-1; +std::mutex txMutex; +std::condition_variable txWake; +std::atomic blockTx{false},txBlocked{false}; void check(bool ok,char const* message){if(!ok)throw std::runtime_error(message);} template void waitFor(F f) { auto deadline=std::chrono::steady_clock::now()+2s; @@ -27,6 +32,8 @@ template void rejects(F f){bool failed=false;try{f();}catch(std::except void reset() { opens=closes=resets=mergeCalls=txCalls=rxCalls=0; unavailable=initFailure=receiveFailure=txFailure=false; + sentCanFrames.clear();failedTxChannel=-1; + blockTx=false;txBlocked=false; std::lock_guard lock(mutex);incoming.clear(); } void inject(unsigned channel,bool fd,std::uint32_t id,std::initializer_list bytes,unsigned stamp=123) { @@ -42,14 +49,16 @@ extern "C" { U32 VCI_OpenDevice(U32 type,U32,U32){check(type==33,"Device type changed");++opens;return unavailable?0:1;} U32 VCI_CloseDevice(U32,U32){++closes;return 1;} U32 VCI_InitCAN(U32,U32,U32 port,ZCAN_INIT* init){ + check(port==0,"CH1 initialized"); check(init->clk==60000000&&init->mode==0&&init->aset.brp==5&&init->dset.brp==1,"Reference timing not retained"); - return initFailure&&port==1?0:1; + return initFailure?0:1; } U32 VCI_SetReference(U32,U32,U32 port,U32 ref,void* data){ + check(port==0,"CH1 configured"); if(ref==ZCAN_CMD_SET_CHNL_RECV_MERGE){check(port==0&&*static_cast(data)==1,"Invalid merged receive configuration");++mergeCalls;}return 1; } -U32 VCI_StartCAN(U32,U32,U32){return 1;} -U32 VCI_ResetCAN(U32,U32,U32){++resets;return 1;} +U32 VCI_StartCAN(U32,U32,U32 port){check(port==0,"CH1 started");return 1;} +U32 VCI_ResetCAN(U32,U32,U32 port){check(port==0,"CH1 reset");++resets;return 1;} U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){ check(port==0,"Multiple merged receive readers");++rxCalls; if(receiveFailure)return std::numeric_limits::max(); @@ -58,10 +67,18 @@ U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){ U32 count=0;while(!incoming.empty()&&counthdr.chn,"Invalid CAN send call");++txCalls;sentCan=*message;return txFailure?0:1; + check(port==0&&count==1&&port==message->hdr.chn,"Invalid CAN send call");++txCalls;sentCan=*message; + { + std::unique_lock lock(txMutex);sentCanFrames.push_back(*message); + if(blockTx) { + txBlocked=true;txWake.notify_all(); + if(!txWake.wait_for(lock,2s,[]{return !blockTx.load();}))throw std::runtime_error("Test send gate timed out"); + } + } + return txFailure||int(port)==failedTxChannel?0:1; } U32 VCI_TransmitFD(U32,U32,U32 port,ZCAN_FD_MSG* message,U32 count){ - check(count==1&&port==message->hdr.chn,"Invalid CANFD send call");++txCalls;sentFd=*message;return txFailure?0:1; + check(port==0&&count==1&&port==message->hdr.chn,"Invalid CANFD send call");++txCalls;sentFd=*message;return txFailure?0:1; } } int main()try { @@ -75,18 +92,21 @@ int main()try { }check(closes==0,"Closed unopened device"); reset();initFailure=true;{ CanBus bus({});bus.start();check(bus.snapshot().status=="UNAVAILABLE","Partial failure not reported"); - }check(closes==1&&resets==1,"Partial initialization leaked device"); + }check(closes==1&&resets==0,"Partial initialization leaked device"); reset();{ CanConfig c;c.timeout_ms=150;CanBus bus(c);bus.start(); check(mergeCalls==1,"Merged receive configured more than once"); CanBus duplicate(c);rejects([&]{duplicate.start();}); inject(0,false,0x391,{1,37,128,255},123); - inject(1,true,0x391,{4,25,76,100},456); + waitFor([&]{return bus.snapshot().received==1;}); + check(bus.snapshot().channels[0].positions==std::array{1,37,128,255},"Four byte positions overlap"); + inject(1,true,0x391,{99,99,99,99},789); + inject(0,true,0x391,{31,25,76,100},456); waitFor([&]{return bus.snapshot().received==2;}); auto s=bus.snapshot(); - check(s.channels[0].positions==std::array{1,37,128,255},"Four byte positions overlap"); - check(s.channels[1].positions==std::array{4,25,76,100},"Channels mixed"); - check(s.channels[0].valid&&s.channels[1].valid&&s.channels[1].fd&&s.channels[1].device_timestamp_us==456,"Freshness/frame metadata wrong"); + check(s.channels[0].positions==std::array{31,25,76,100},"Actual positions not refreshed"); + check(s.channels[1].received==0&&!s.channels[1].valid&&s.channels[1].positions==std::array{-1,-1,-1,-1},"CH1 input accepted"); + check(s.channels[0].valid&&s.channels[0].fd&&s.channels[0].device_timestamp_us==456,"Freshness/frame metadata wrong"); auto timestamp=s.channels[0].received_monotonic_ns; inject(0,false,0x390,{9,9,9,9});inject(0,false,0x391,{99,99,99}); waitFor([&]{return bus.snapshot().received==4;}); @@ -95,18 +115,77 @@ int main()try { inject(0,false,0x391,{0,1,2,3});waitFor([&]{return bus.snapshot().channels[0].valid;}); check(bus.send({0,0x321,false,false,false,{1,2,3}}),"CAN send failed"); check(sentCan.hdr.len==3&&!sentCan.hdr.inf.sef&&sentCan.dat[2]==3,"CAN encoding incorrect"); - CanFrame fd{1,0x12345,true,true,true,std::vector(64,0xa5)}; + CanFrame fd{0,0x12345,true,true,true,std::vector(64,0xa5)}; check(bus.send(fd)&&sentFd.hdr.len==64&&sentFd.hdr.inf.brs&&sentFd.hdr.inf.sef&&sentFd.dat[63]==0xa5,"CANFD encoding incorrect"); fd.data.resize(9);rejects([&]{bus.send(fd);}); - rejects([&]{bus.send({2,0x391,false,false,false,{}});}); + rejects([&]{bus.send({1,0x391,false,false,false,{}});}); rejects([&]{bus.send({0,0x800,false,false,false,{}});}); rejects([&]{bus.send({0,0x391,false,false,true,{}});}); txFailure=true;check(!bus.send({}),"SDK send failure ignored"); check(bus.snapshot().transmitted==2&&bus.snapshot().transmit_errors==1,"TX counters wrong"); receiveFailure=true;waitFor([&]{return bus.snapshot().status=="ERROR";}); check(!bus.snapshot().channels[0].valid&&!bus.send({}),"Receive error left bus live"); - bus.requestStop();bus.join();check(closes==1&&resets==2,"Shutdown did not release both channels"); + bus.requestStop();bus.join();check(closes==1&&resets==1,"Shutdown did not release CH0"); check(!bus.send({}),"Transmission after stop"); }check(closes==1,"Device closed twice"); - std::cout<<"PASS CAN/CANFD RX decoding, independent channels, expiry/recovery, TX validation, errors and lifecycle cleanup\n"; + reset();{ + CanBus bus({});bus.start(); + for(auto level:{"clear","watch","warning","danger","unavailable","unknown","","clear"}) { + auto sent=bus.sendRisk(level); + check(sent==std::array{true,false},"Risk must use CH0 only"); + } + check(sentCanFrames.size()==8,"Risk must send exactly once per channel per result"); + for(std::size_t i=0;i{false,false},"CH0 failure reported success"); + failedTxChannel=-1; + check(bus.sendRisk("clear")==std::array{true,false},"CH0 did not recover after send failure"); + check(bus.snapshot().transmitted==9&&bus.snapshot().transmit_errors==1,"Risk TX counters wrong"); + auto calls=txCalls.load();bus.requestStop(); + check(bus.sendRisk("danger")==std::array{false,false}&&txCalls==calls,"Risk sent after stop"); + } + reset();{ + CanConfig c;c.enabled=false;CanBus bus(c);bus.start(); + check(bus.sendRisk("danger")==std::array{false,false}&&txCalls==0&&opens==0, + "Disabled risk send accessed hardware"); + check(!bus.queueRisk("danger"),"Disabled bus accepted asynchronous risk"); + } + reset();{ + CanBus bus({});check(!bus.queueRisk("danger"),"Unstarted bus accepted risk");bus.start(); + // A blocked hardware SDK call must not stall inference submission or snapshots. + blockTx=true;check(bus.queueRisk("danger"),"Risk queue rejected ready bus"); + waitFor([]{return txBlocked.load();}); + auto start=std::chrono::steady_clock::now(); + for(int i=0;i<1000;++i)check(bus.queueRisk(i%2?"clear":"warning"),"Risk queue stalled/rejected update"); + check(bus.snapshot().transmitted==0,"Blocked SDK falsely reported delivery"); + check(std::chrono::steady_clock::now()-start<250ms,"Risk submission waited for the SDK"); + {std::lock_guard lock(txMutex);blockTx=false;}txWake.notify_all(); + waitFor([&]{return bus.snapshot().transmitted==2;}); + { + std::lock_guard lock(txMutex); + check(sentCanFrames.size()==2,"Stale pending results accumulated"); + for(int i=0;i<2;++i)check(sentCanFrames[i].hdr.chn==0&& + sentCanFrames[i].dat[0]==(i==0?0x11:0x00),"Async risk or latest result incorrect"); + } + failedTxChannel=0; + check(bus.queueRisk("unavailable"),"Ready queue rejected unavailable risk"); + waitFor([&]{auto s=bus.snapshot();return s.transmitted==2&&s.transmit_errors==1;}); + failedTxChannel=-1;txBlocked=false;blockTx=true; + check(bus.queueRisk("clear"),"Ready queue rejected clear risk");waitFor([]{return txBlocked.load();}); + check(bus.queueRisk("danger"),"Queue rejected pending stop fixture"); + auto calls=txCalls.load();bus.requestStop(); + check(!bus.queueRisk("danger"),"Stopped bus accepted risk"); + {std::lock_guard lock(txMutex);blockTx=false;}txWake.notify_all(); + bus.join();check(txCalls==calls,"Stop sent a pending result or second channel"); + check(closes==1&&resets==1,"Async shutdown leaked hardware"); + } + std::cout<<"PASS CAN/CANFD RX decoding, CH0-only reception, expiry/recovery, TX validation, binary risk 0x1F6 on CH0 only, errors and lifecycle cleanup\n"; }catch(std::exception const& e){std::cerr<