Fix live CAN feedback with CH0-only asynchronous risk transmission

Send 0x1F6 risk updates through a bounded latest-result worker on CH0 and disable CH1 initialization, reception, transmission and live display. Preserve the four actual-position bytes from 0x391 and recorded-session compatibility.

Include regression coverage, Orin camera/TensorRT/CAN validation, and the verified standalone binary with refreshed checksums.
This commit is contained in:
zmv
2026-09-28 13:59:26 +08:00
parent 515a9f32d8
commit f87e5aad40
16 changed files with 346 additions and 37 deletions
+12 -6
View File
@@ -138,7 +138,7 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回
## CAN/CANFD 收发与四门位置显示 ## 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 运行整个推理程序。 首次使用 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 | 左后门实际位置反馈 | | 2 | LR | 左后门实际位置反馈 |
| 3 | RR | 右后门实际位置反馈 | | 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()`: 下游通过 `pipeline.canStatus()` 读取当前状态;`pipeline.latest()->can` 是该推理结果生成时的快照。按需发送使用线程安全的 `pipeline.sendCan()`:
```cpp ```cpp
cms::CanFrame frame; cms::CanFrame frame;
frame.channel = 0; // CH0 或 CH1 frame.channel = 0; // 仅 CH0
frame.id = 0x123; // 按实际协议设置 frame.id = 0x123; // 按实际协议设置
frame.data = {0x01, 0x02}; // 示例数据,应替换为实际报文 frame.data = {0x01, 0x02}; // 示例数据,应替换为实际报文
bool sent = pipeline.sendCan(frame); bool sent = pipeline.sendCan(frame);
@@ -169,10 +169,16 @@ bool sent = pipeline.sendCan(frame);
// 标准 ID 最大 0x7FF,扩展 ID 最大 0x1FFFFFFF。 // 标准 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()` 会明确拒绝。 结果 JSON 和录制会话自动包含每帧 `can` 快照。视频回灌不打开 USB-CAN,不发送报文,直接恢复录制时的状态并标记 `source=replay`;旧会话没有 CAN 字段时显示 NOT_RECORDED。回灌调用 `sendCan()` 会明确拒绝。
依赖 `third_party/zlg/zcan.h` 和 `orin/lib/` 中的参考工程 SDK 库(含 SONAME 链接),独立包已包含它们。纯模拟收发回归:`./tests/run_can.sh`;会话回归同时覆盖 CAN 快照保存。模拟测试不代表物理总线收发验证。 依赖 `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)。
+10
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "can_types.hpp" #include "can_types.hpp"
#include <memory> #include <memory>
#include <string_view>
namespace cms { namespace cms {
// One lifecycle per instance; only one owner per device index in a process. // 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 // Thread-safe on-demand data-frame send; invalid frames throw, SDK failure
// returns false. Never generates periodic or inference-triggered messages. // returns false. Never generates periodic or inference-triggered messages.
bool send(CanFrame const& frame); 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<bool,2> 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: private:
struct Impl; struct Impl;
std::unique_ptr<Impl> impl_; std::unique_ptr<Impl> impl_;
+2
View File
@@ -5,6 +5,7 @@
#include <vector> #include <vector>
namespace cms { namespace cms {
inline constexpr unsigned liveCanChannelCount=1; // CH0 only; CH1 is not used.
struct CanConfig { struct CanConfig {
bool enabled{true}; bool enabled{true};
int device_index{0}; int device_index{0};
@@ -22,6 +23,7 @@ struct CanDoorSignals {
}; };
struct CanSnapshot { struct CanSnapshot {
std::string source{"live"},status{"DISABLED"}; std::string source{"live"},status{"DISABLED"};
// Keep two slots for existing recorded sessions; live CH1 stays unset.
std::array<CanDoorSignals,2> channels; std::array<CanDoorSignals,2> channels;
std::uint64_t received{},transmitted{},transmit_errors{},receive_errors{}; std::uint64_t received{},transmitted{},transmit_errors{},receive_errors{};
}; };
+3 -1
View File
@@ -54,8 +54,10 @@ bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查
## CAN 状态与发送接口 ## 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。 `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` 适配器安装设备访问规则。 USB-CAN 首次接入如出现权限不足,运行 `./scripts/enable_can_access.sh`(需要 sudo,运行用户属于 plugdev),仅为 `3068:0009` 适配器安装设备访问规则。
+2 -2
View File
@@ -1,7 +1,7 @@
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko
e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461 ./build/camera_risk 418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f ./build/camera_risk
d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json
9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json 9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine
@@ -10,7 +10,7 @@ f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo
8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so.0 8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so.0
ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so
ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so.1.0.12 ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so.1.0.12
bb61f669d8b471c5ca42045b31a0e6fd300235da37a80893ae229f37deb1b7b9 ./README.md 584875c5a52f969da7ae50514fd627749123f3db7140058052055982a6c01f2f ./README.md
7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh 7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh
41ebe2dcf57674232ca137d024e99447b467049cb1c63a284daae274296cb78c ./scripts/99-usbcanfd.rules 41ebe2dcf57674232ca137d024e99447b467049cb1c63a284daae274296cb78c ./scripts/99-usbcanfd.rules
920102fc5e91bde9b78f6f7c5cb2ff8761a0c39b63c7a28b3347d5de5fe7c60a ./scripts/enable_can_access.sh 920102fc5e91bde9b78f6f7c5cb2ff8761a0c39b63c7a28b3347d5de5fe7c60a ./scripts/enable_can_access.sh
Binary file not shown.
+4 -1
View File
@@ -173,7 +173,10 @@ struct CameraPipeline::Impl {
if(recorded.contains("can"))result->can=deserializeCan(recorded.at("can")); if(recorded.contains("can"))result->can=deserializeCan(recorded.at("can"));
else result->can.status="NOT_RECORDED"; else result->can.status="NOT_RECORDED";
result->can.source="replay"; 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); attachPoses(*result,fused);
result->image=std::move(detected->frame.image);result->door_mask_pixels=doorPixels; result->image=std::move(detected->frame.image);result->door_mask_pixels=doorPixels;
result->door_mask_calibration_verified=config.door_mask_calibration_verified; result->door_mask_calibration_verified=config.door_mask_calibration_verified;
+57 -10
View File
@@ -6,6 +6,7 @@
#include <condition_variable> #include <condition_variable>
#include <iostream> #include <iostream>
#include <mutex> #include <mutex>
#include <optional>
#include <set> #include <set>
#include <stdexcept> #include <stdexcept>
#include <thread> #include <thread>
@@ -19,7 +20,7 @@ std::uint64_t nowNs() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
} }
void validate(CanFrame const& f) { 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"); throw std::invalid_argument("Invalid CAN channel, identifier or BRS flag");
auto size=f.data.size(); 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))) 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 { struct CanBus::Impl {
CanConfig config; CanConfig config;
mutable std::mutex stateMutex; mutable std::mutex stateMutex;
std::mutex lifecycleMutex,waitMutex; std::mutex lifecycleMutex,waitMutex,riskMutex;
std::condition_variable wake; std::condition_variable wake,riskWake;
std::optional<bool> pendingRisk;
CanSnapshot state; CanSnapshot state;
std::atomic<bool> stopped{false}; std::atomic<bool> stopped{false};
bool started{},opened{},owned{}; bool started{},opened{},owned{};
std::array<bool,2> initialized{}; std::array<bool,2> initialized{};
std::thread receiver; std::thread receiver,riskSender;
explicit Impl(CanConfig c):config(c) { explicit Impl(CanConfig c):config(c) {
if(c.device_index<0||c.timeout_ms<=0)throw std::invalid_argument("Invalid CAN configuration"); if(c.device_index<0||c.timeout_ms<=0)throw std::invalid_argument("Invalid CAN configuration");
state.status=c.enabled?"STARTING":"DISABLED"; state.status=c.enabled?"STARTING":"DISABLED";
@@ -44,7 +46,7 @@ struct CanBus::Impl {
void setStatus(char const* value) {std::lock_guard<std::mutex> lock(stateMutex);state.status=value;} void setStatus(char const* value) {std::lock_guard<std::mutex> lock(stateMutex);state.status=value;}
void releaseDevice() noexcept { void releaseDevice() noexcept {
if(opened) { if(opened) {
for(unsigned i=0;i<2;++i)if(initialized[i])VCI_ResetCAN(deviceType,config.device_index,i); for(unsigned i=0;i<liveCanChannelCount;++i)if(initialized[i])VCI_ResetCAN(deviceType,config.device_index,i);
VCI_CloseDevice(deviceType,config.device_index); VCI_CloseDevice(deviceType,config.device_index);
opened=false;initialized.fill(false); opened=false;initialized.fill(false);
} }
@@ -64,7 +66,7 @@ struct CanBus::Impl {
if(VCI_OpenDevice(deviceType,config.device_index,0)!=1)throw std::runtime_error("VCI_OpenDevice failed"); if(VCI_OpenDevice(deviceType,config.device_index,0)!=1)throw std::runtime_error("VCI_OpenDevice failed");
opened=true; opened=true;
// Reference USBCANFD: 60 MHz clock, arbitration 500 kbps, data 2 Mbps. // Reference USBCANFD: 60 MHz clock, arbitration 500 kbps, data 2 Mbps.
for(unsigned i=0;i<2;++i) { for(unsigned i=0;i<liveCanChannelCount;++i) {
ZCAN_INIT init{};init.clk=60000000;init.mode=0; ZCAN_INIT init{};init.clk=60000000;init.mode=0;
init.aset.tseg1=14;init.aset.tseg2=3;init.aset.sjw=2;init.aset.brp=5; init.aset.tseg1=14;init.aset.tseg2=3;init.aset.sjw=2;init.aset.brp=5;
init.dset.tseg1=10;init.dset.tseg2=2;init.dset.sjw=2;init.dset.brp=1; init.dset.tseg1=10;init.dset.tseg2=2;init.dset.sjw=2;init.dset.brp=1;
@@ -77,20 +79,30 @@ struct CanBus::Impl {
U32 merge=1; U32 merge=1;
if(VCI_SetReference(deviceType,config.device_index,0,ZCAN_CMD_SET_CHNL_RECV_MERGE,&merge)!=1) if(VCI_SetReference(deviceType,config.device_index,0,ZCAN_CMD_SET_CHNL_RECV_MERGE,&merge)!=1)
throw std::runtime_error("CAN merged receive configuration failed"); throw std::runtime_error("CAN merged receive configuration failed");
for(unsigned i=0;i<2;++i)if(VCI_StartCAN(deviceType,config.device_index,i)!=1) for(unsigned i=0;i<liveCanChannelCount;++i)if(VCI_StartCAN(deviceType,config.device_index,i)!=1)
throw std::runtime_error("VCI_StartCAN failed"); throw std::runtime_error("VCI_StartCAN failed");
setStatus("OPEN"); setStatus("OPEN");
// Exactly one merged reader for both channels; no competing readers. // Exactly one reader; only CH0 is initialized and accepted.
receiver=std::thread([this]{receiveLoop();}); receiver=std::thread([this]{receiveLoop();});
std::cout<<"CAN device "<<config.device_index<<": CH0/CH1 enabled, 500 kbps / 2 Mbps"<<std::endl; riskSender=std::thread([this]{riskLoop();});
std::cout<<"CAN device "<<config.device_index<<": CH0 enabled, 500 kbps / 2 Mbps"<<std::endl;
}catch(std::exception const& e) { }catch(std::exception const& e) {
requestStop();
if(receiver.joinable())receiver.join();
releaseDevice();setStatus("UNAVAILABLE"); releaseDevice();setStatus("UNAVAILABLE");
std::cerr<<"CAN unavailable; inference continues: "<<e.what()<<std::endl; std::cerr<<"CAN unavailable; inference continues: "<<e.what()<<std::endl;
} }
} }
void requestStop() noexcept {stopped=true;wake.notify_all();} void requestStop() noexcept {
stopped=true;wake.notify_all();
{std::lock_guard<std::mutex> lock(riskMutex);pendingRisk.reset();}
riskWake.notify_all();
}
void join() noexcept { void join() noexcept {
requestStop(); 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<std::mutex> lock(lifecycleMutex); std::lock_guard<std::mutex> lock(lifecycleMutex);
if(receiver.joinable())receiver.join(); if(receiver.joinable())receiver.join();
releaseDevice(); releaseDevice();
@@ -100,6 +112,7 @@ struct CanBus::Impl {
std::lock_guard<std::mutex> lock(stateMutex); std::lock_guard<std::mutex> lock(stateMutex);
// Ignore remote, error and transmit-echo frames; they are not input. // Ignore remote, error and transmit-echo frames; they are not input.
if(header.inf.sdf||header.inf.err||header.inf.tx)return; 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)) { if(channel>1||header.len>(fd?64:8)||header.id>(header.inf.sef?0x1fffffffu:0x7ffu)) {
++state.receive_errors;return; ++state.receive_errors;return;
} }
@@ -123,6 +136,7 @@ struct CanBus::Impl {
} }
for(unsigned i=0;i<count&&!stopped;++i) { for(unsigned i=0;i<count&&!stopped;++i) {
auto const& item=frames[i]; auto const& item=frames[i];
if(item.chnl>=liveCanChannelCount)continue;
if(item.dataType==ZCAN_DT_ZCAN_CAN_DATA)receiveFrame(item.data.zcanCANData.hdr,item.data.zcanCANData.dat,false,item.chnl); 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_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<std::mutex> lock(stateMutex);++state.receive_errors;} else if(item.dataType==ZCAN_DT_ZCAN_ERROR_DATA){std::lock_guard<std::mutex> lock(stateMutex);++state.receive_errors;}
@@ -139,6 +153,29 @@ struct CanBus::Impl {
} }
return result; return result;
} }
bool queueRisk(std::string_view level) {
std::lock_guard<std::mutex> lock(riskMutex);
{std::lock_guard<std::mutex> 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<std::mutex> 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<liveCanChannelCount&&!stopped;++channel) {
frame.channel=channel;send(frame);
}
}
}catch(...) {setStatus("ERROR");}
}
bool send(CanFrame const& f) { bool send(CanFrame const& f) {
validate(f); validate(f);
std::lock_guard<std::mutex> lock(lifecycleMutex); std::lock_guard<std::mutex> lock(lifecycleMutex);
@@ -168,4 +205,14 @@ void CanBus::requestStop()noexcept{impl_->requestStop();}
void CanBus::join()noexcept{impl_->join();} void CanBus::join()noexcept{impl_->join();}
CanSnapshot CanBus::snapshot()const{return impl_->snapshot();} CanSnapshot CanBus::snapshot()const{return impl_->snapshot();}
bool CanBus::send(CanFrame const& f){return impl_->send(f);} bool CanBus::send(CanFrame const& f){return impl_->send(f);}
bool CanBus::queueRisk(std::string_view level){return impl_->queueRisk(level);}
std::array<bool,2> 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<bool,2> sent{};
for(unsigned channel=0;channel<liveCanChannelCount;++channel) {
frame.channel=channel;sent[channel]=send(frame);
}
return sent;
}
} }
+1 -1
View File
@@ -23,7 +23,7 @@ int main(int argc,char** argv)try {
{"--visualize",[&](int&){visualize=true;}}, {"--visualize",[&](int&){visualize=true;}},
{"--no-visualize",[&](int&){visualize=false;}}, {"--no-visualize",[&](int&){visualize=false;}},
{"--config",[&](int&i){if(++i>=argc)throw std::invalid_argument("--config needs a file");configPath=argv[i];}}, {"--config",[&](int&i){if(++i>=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;i<argc;++i){auto handler=arguments.find(argv[i]);if(handler==arguments.end())throw std::invalid_argument("Unknown option: "+std::string(argv[i]));handler->second(i);} for(int i=1;i<argc;++i){auto handler=arguments.find(argv[i]);if(handler==arguments.end())throw std::invalid_argument("Unknown option: "+std::string(argv[i]));handler->second(i);}
+3 -2
View File
@@ -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) { void renderCanOverlay(cv::Mat& canvas,CanSnapshot const& can) {
cv::rectangle(canvas,{0,148},{canvas.cols,262},{20,24,30},-1); cv::rectangle(canvas,{0,148},{canvas.cols,262},{20,24,30},-1);
std::ostringstream header; std::ostringstream header;
header<<"CAN 0x391 | "<<can.source<<" "<<can.status<<" | RX "<<can.received header<<"CAN 0x391 ACTUAL POSITION | "<<can.source<<" "<<can.status<<" | RX "<<can.received
<<" TX "<<can.transmitted<<" | errors RX "<<can.receive_errors<<" TX "<<can.transmit_errors; <<" TX "<<can.transmitted<<" | errors RX "<<can.receive_errors<<" TX "<<can.transmit_errors;
cv::putText(canvas,header.str(),{24,174},cv::FONT_HERSHEY_SIMPLEX,.65,{235,235,235},2,cv::LINE_AA); cv::putText(canvas,header.str(),{24,174},cv::FONT_HERSHEY_SIMPLEX,.65,{235,235,235},2,cv::LINE_AA);
constexpr char const* labels[]={"LF","RF","LR","RR"}; constexpr char const* labels[]={"LF","RF","LR","RR"};
for(std::size_t channel=0;channel<can.channels.size();++channel) { auto channels=can.source=="replay"?can.channels.size():liveCanChannelCount;
for(std::size_t channel=0;channel<channels;++channel) {
auto const& s=can.channels[channel];std::ostringstream line; auto const& s=can.channels[channel];std::ostringstream line;
line<<"CH"<<channel<<" "<<(s.valid?"LIVE":s.received?"STALE":"WAIT")<<" "; line<<"CH"<<channel<<" "<<(s.valid?"LIVE":s.received?"STALE":"WAIT")<<" ";
for(std::size_t i=0;i<4;++i) { for(std::size_t i=0;i<4;++i) {
+33
View File
@@ -0,0 +1,33 @@
# 0x391 实际位置反馈与 CH0 单通道修复(2026-09-28)
用户要求显示四门实际位置反馈,并明确不使用 CH1。本次保留 `0x391` 第 0/1/2/3 字节到 LF/RF/LR/RR 的无符号原始值映射,不改为门状态、不引入未经确认的百分比或角度换算。
实时模式仅初始化、启动、接收和发送 CH0;拒绝向 CH1 调用发送 API,忽略合并接收中的 CH1 数据,画面仅显示 CH0,并标明 ACTUAL POSITION。历史会话的双通道快照结构与回灌显示保持兼容,`sendRisk()` 返回值第二槽固定为 false。
## 实机现象与修复结果
用户第一次确认左前门打开后,原始接收工具取得 CH0、CANFD、64 字节的 `0x391`,前四字节为 `1F 00 00 00`,即左前门反馈 31。由此确认实际位置字段映射有真实报文依据。
修复前,真实摄像头/TensorRT 与原双通道风险发送同时运行时,接收计数长时间停顿;反馈年龄最高观察到约 5.84 秒。CH0 发送成功、CH1 发送失败,接收在发送等待期间停顿。仅将发送移到后台线程不能消除适配器发送等待对接收的影响。
最终仅 CH0 版本在同一硬件上运行:10 秒采样内 RX 持续增长,TX 到 299、TX 错误为 0,各采样点保持 LIVE。用户再次确认左前门打开后,最终版同一进程显示实际位置 `26,0,0,0`,末次采样更新为 `25,0,0,0`,0x391 计数从 4 增至 33。显示层由相同 `renderResult()` 绘制,已检查 [CAN 画面裁剪](orin_391_overlay.png),可见 LF 26,CH1 未显示。
详细结果与运行二进制校验见 [实机结果](orin_391_results.json)。原始日志为 `/tmp/vap_391_open.log`,对照采样为 `/tmp/vap_391_after.log`,采样源码为 `/tmp/vap_391_pipeline.cpp`。
## 本机模拟、板端故障注入、硬件覆盖
- 本机模拟:`tests/run_can.sh`(UBSan、模拟 SDK)通过。检查 CH1 不初始化/配置/启动/重置/发送、CH1 输入不覆盖 CH0、实际位置更新、CAN/CANFD、过期恢复、单通道风险报文、发送失败与恢复、后台最新值覆盖、停止清理。
- 本机模拟:`tests/run_session.sh` 通过,保持已有会话 CAN 快照兼容;构建与 `git diff --check` 通过。
- 板端故障注入:未执行。本次未修改共享摄像头采集、停止或恢复逻辑。
- 真实硬件:Orin 上使用既有 `/dev/video5`、TensorRT 引擎、USBCANFD-200U 与配置;用户操作左前门,验证实际位置进入推理期间的实时显示。采样程序调用画面绘制,未运行 GUI 事件循环;正式程序另按原命令 `./run_camera.sh` 启动验证。
- 未覆盖:其余三扇门逐一动作、长期运行、CH0 断开/缺失 ACK、独立总线分析仪验证风险报文。接收错误计数仍增长(画面记录 RX errors 5801),本次未确定该总线错误的原因,不能宣称总线无错误。
## 部署与版本
最终运行 `/proc/<pid>/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 远端尚未推送同步。以上二进制哈希标识已验证的本地部署产物。
+41
View File
@@ -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/<pid>/exe`、`build/camera_risk`、`orin/build/camera_risk` SHA256 均为:
`a84680513aa61fb2af353f97e6369bbef80c9f12521ecd88454e72fcecab447f`
独立包 SHA256SUMS 已刷新并完整校验。修改留在本地工作区,未提交/推送;版本引用详情见 [风险发送记录](RISK_CAN_REPORT.md)。
## 未覆盖项
物理 CAN 发送依然失败,本次只确认该等待已与推理解耦,未确认 ACK、线缆、接收端或总线配置的故障原因。未使用独立总线分析仪验收线上载荷,也未测试物理断连重连或长期耐久。当前场景约 30 FPS 不代表任意人数/场景都能达到同样吞吐。
+27
View File
@@ -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/<pid>/exe`、构建文件和独立包哈希一致,SIGTERM 约 1.267 秒正常退出,恢复测试前无常驻进程状态。
+93 -14
View File
@@ -18,6 +18,11 @@ std::deque<ZCANDataObj> incoming;
std::atomic<int> opens{0},closes{0},resets{0},mergeCalls{0},txCalls{0},rxCalls{0}; std::atomic<int> opens{0},closes{0},resets{0},mergeCalls{0},txCalls{0},rxCalls{0};
std::atomic<bool> unavailable{false},initFailure{false},receiveFailure{false},txFailure{false}; std::atomic<bool> unavailable{false},initFailure{false},receiveFailure{false},txFailure{false};
ZCAN_20_MSG sentCan{};ZCAN_FD_MSG sentFd{}; ZCAN_20_MSG sentCan{};ZCAN_FD_MSG sentFd{};
std::vector<ZCAN_20_MSG> sentCanFrames;
int failedTxChannel=-1;
std::mutex txMutex;
std::condition_variable txWake;
std::atomic<bool> blockTx{false},txBlocked{false};
void check(bool ok,char const* message){if(!ok)throw std::runtime_error(message);} void check(bool ok,char const* message){if(!ok)throw std::runtime_error(message);}
template<class F> void waitFor(F f) { template<class F> void waitFor(F f) {
auto deadline=std::chrono::steady_clock::now()+2s; auto deadline=std::chrono::steady_clock::now()+2s;
@@ -27,6 +32,8 @@ template<class F> void rejects(F f){bool failed=false;try{f();}catch(std::except
void reset() { void reset() {
opens=closes=resets=mergeCalls=txCalls=rxCalls=0; opens=closes=resets=mergeCalls=txCalls=rxCalls=0;
unavailable=initFailure=receiveFailure=txFailure=false; unavailable=initFailure=receiveFailure=txFailure=false;
sentCanFrames.clear();failedTxChannel=-1;
blockTx=false;txBlocked=false;
std::lock_guard<std::mutex> lock(mutex);incoming.clear(); std::lock_guard<std::mutex> lock(mutex);incoming.clear();
} }
void inject(unsigned channel,bool fd,std::uint32_t id,std::initializer_list<std::uint8_t> bytes,unsigned stamp=123) { void inject(unsigned channel,bool fd,std::uint32_t id,std::initializer_list<std::uint8_t> 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_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_CloseDevice(U32,U32){++closes;return 1;}
U32 VCI_InitCAN(U32,U32,U32 port,ZCAN_INIT* init){ 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"); 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){ 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<U32*>(data)==1,"Invalid merged receive configuration");++mergeCalls;}return 1; if(ref==ZCAN_CMD_SET_CHNL_RECV_MERGE){check(port==0&&*static_cast<U32*>(data)==1,"Invalid merged receive configuration");++mergeCalls;}return 1;
} }
U32 VCI_StartCAN(U32,U32,U32){return 1;} U32 VCI_StartCAN(U32,U32,U32 port){check(port==0,"CH1 started");return 1;}
U32 VCI_ResetCAN(U32,U32,U32){++resets;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){ U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){
check(port==0,"Multiple merged receive readers");++rxCalls; check(port==0,"Multiple merged receive readers");++rxCalls;
if(receiveFailure)return std::numeric_limits<U32>::max(); if(receiveFailure)return std::numeric_limits<U32>::max();
@@ -58,10 +67,18 @@ U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){
U32 count=0;while(!incoming.empty()&&count<capacity){out[count++]=incoming.front();incoming.pop_front();}return count; U32 count=0;while(!incoming.empty()&&count<capacity){out[count++]=incoming.front();incoming.pop_front();}return count;
} }
U32 VCI_Transmit(U32,U32,U32 port,ZCAN_20_MSG* message,U32 count){ U32 VCI_Transmit(U32,U32,U32 port,ZCAN_20_MSG* message,U32 count){
check(count==1&&port==message->hdr.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<std::mutex> 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){ 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 { int main()try {
@@ -75,18 +92,21 @@ int main()try {
}check(closes==0,"Closed unopened device"); }check(closes==0,"Closed unopened device");
reset();initFailure=true;{ reset();initFailure=true;{
CanBus bus({});bus.start();check(bus.snapshot().status=="UNAVAILABLE","Partial failure not reported"); 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();{ reset();{
CanConfig c;c.timeout_ms=150;CanBus bus(c);bus.start(); CanConfig c;c.timeout_ms=150;CanBus bus(c);bus.start();
check(mergeCalls==1,"Merged receive configured more than once"); check(mergeCalls==1,"Merged receive configured more than once");
CanBus duplicate(c);rejects([&]{duplicate.start();}); CanBus duplicate(c);rejects([&]{duplicate.start();});
inject(0,false,0x391,{1,37,128,255},123); 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<int,4>{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;}); waitFor([&]{return bus.snapshot().received==2;});
auto s=bus.snapshot(); auto s=bus.snapshot();
check(s.channels[0].positions==std::array<int,4>{1,37,128,255},"Four byte positions overlap"); check(s.channels[0].positions==std::array<int,4>{31,25,76,100},"Actual positions not refreshed");
check(s.channels[1].positions==std::array<int,4>{4,25,76,100},"Channels mixed"); check(s.channels[1].received==0&&!s.channels[1].valid&&s.channels[1].positions==std::array<int,4>{-1,-1,-1,-1},"CH1 input accepted");
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].valid&&s.channels[0].fd&&s.channels[0].device_timestamp_us==456,"Freshness/frame metadata wrong");
auto timestamp=s.channels[0].received_monotonic_ns; auto timestamp=s.channels[0].received_monotonic_ns;
inject(0,false,0x390,{9,9,9,9});inject(0,false,0x391,{99,99,99}); inject(0,false,0x390,{9,9,9,9});inject(0,false,0x391,{99,99,99});
waitFor([&]{return bus.snapshot().received==4;}); 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;}); 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(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"); 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<std::uint8_t>(64,0xa5)}; CanFrame fd{0,0x12345,true,true,true,std::vector<std::uint8_t>(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"); 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);}); 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,0x800,false,false,false,{}});});
rejects([&]{bus.send({0,0x391,false,false,true,{}});}); rejects([&]{bus.send({0,0x391,false,false,true,{}});});
txFailure=true;check(!bus.send({}),"SDK send failure ignored"); txFailure=true;check(!bus.send({}),"SDK send failure ignored");
check(bus.snapshot().transmitted==2&&bus.snapshot().transmit_errors==1,"TX counters wrong"); check(bus.snapshot().transmitted==2&&bus.snapshot().transmit_errors==1,"TX counters wrong");
receiveFailure=true;waitFor([&]{return bus.snapshot().status=="ERROR";}); receiveFailure=true;waitFor([&]{return bus.snapshot().status=="ERROR";});
check(!bus.snapshot().channels[0].valid&&!bus.send({}),"Receive error left bus live"); 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(!bus.send({}),"Transmission after stop");
}check(closes==1,"Device closed twice"); }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<bool,2>{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<sentCanFrames.size();++i) {
auto const& message=sentCanFrames[i];
check(message.hdr.id==0x1f6&&message.hdr.len==8&&message.hdr.chn==0,
"Risk ID, DLC or channel incorrect");
check(!message.hdr.inf.fmt&&!message.hdr.inf.sef&&!message.hdr.inf.brs&&!message.hdr.inf.sdf,
"Risk must use a standard classic CAN data frame");
check(message.dat[0]==(i==0||i==7?0x00:0x11),"Risk must fill both nibbles and reset both for clear");
for(int byte=1;byte<8;++byte)check(message.dat[byte]==0,"Risk reserved bits not zero");
}
failedTxChannel=0;
check(bus.sendRisk("danger")==std::array<bool,2>{false,false},"CH0 failure reported success");
failedTxChannel=-1;
check(bus.sendRisk("clear")==std::array<bool,2>{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<bool,2>{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<bool,2>{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<std::mutex> lock(txMutex);blockTx=false;}txWake.notify_all();
waitFor([&]{return bus.snapshot().transmitted==2;});
{
std::lock_guard<std::mutex> 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<std::mutex> 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<<e.what()<<'\n';return 1;} }catch(std::exception const& e){std::cerr<<e.what()<<'\n';return 1;}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+58
View File
@@ -0,0 +1,58 @@
{
"running_sha256": "418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f",
"build_sha256": "418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f",
"stop_seconds": 0.8675570619998325,
"exit_code": 0,
"command": "./run_camera.sh",
"first_sample": {
"seconds": 0.25,
"rx": 8833,
"tx": 5,
"tx_errors": 0,
"valid": true,
"received_391": 4,
"age_ms": 147.083,
"positions": [
26,
0,
0,
0
]
},
"last_sample": {
"seconds": 10.0,
"rx": 35669,
"tx": 299,
"tx_errors": 0,
"valid": true,
"received_391": 33,
"age_ms": 84.5888,
"positions": [
25,
0,
0,
0
]
},
"sample_count": 40,
"max_observed_age_ms": 913.11,
"observed_positions": [
[
25,
0,
0,
0
],
[
26,
0,
0,
0
]
],
"checks": "real camera/TensorRT/CH0/renderer; not CAN fault injection",
"local_head": "515a9f32d8c0ebf83503d4d240ffe917dc09950c",
"remote_inference_only": "0192fccbf06cb928eac098b3dfad32abf0fc370d",
"source_state": "uncommitted workspace at hardware verification",
"package_sha256": "418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f"
}