Add USBCANFD transport and visualize 0x391 door feedback
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# CAN 收发与 0x391 显示实现记录(2026-09-24)
|
||||
|
||||
范围:仅当前 `inference-only` 分支。用户明确当前并非真实 CAN 环境,接收实测暂不验证;真实 CAN 接收和发送均未验收。
|
||||
|
||||
## 已完成的模拟和离线检查
|
||||
|
||||
- `tests/run_can.sh`:UBSan + 模拟 SDK,无硬件访问。覆盖四个连续字节独立解析、双通道隔离、CAN/CANFD 数据帧、过期与恢复、无关 ID / 短报文不刷新状态、按需发送编码、非法 ID/通道/长度/BRS、发送失败、接收错误、缺失适配器、部分初始化失败清理、设备单一所有权和停止后拒绝发送。
|
||||
- `tests/run_session.sh`:人工图像 H.264 往返与过程记录,新增 CAN 快照保存/读取检查,四门值、有效状态与年龄保持不变。
|
||||
- 静态可视化检查:人工构造 CH0 LIVE、CH1 STALE 快照,四门不同值均可见,显示收发计数和报文年龄,未读取任何 CAN 设备。临时图 `/tmp/vap_can_overlay.png`。
|
||||
- 源码构建通过,SDK 和 libusb 从独立包加载,配置和 `--no-can` 接入完成。
|
||||
|
||||
以上均不能代替真实 CAN 总线验证。
|
||||
|
||||
## 已执行的 Orin 摄像头 / TensorRT 检查
|
||||
|
||||
在用户要求暂停 CAN 实测前执行过 `tests/orin_session_test.py`,测试目录 `/tmp/vap-can-hardware-20260924/`。当时 USB 节点为 `root:root 0664`,SDK 打开失败,状态为 UNAVAILABLE,实际接收数为 0。
|
||||
|
||||
在这一缺失 CAN 的回退条件下,真实 `/dev/video5` 与 TensorRT 完成 13 帧 H.264 录制、13 帧完整回灌,CAN UNAVAILABLE 快照在回灌中保持一致并标记 `source=replay`。13 帧检测到人员;跟踪 ID 一致 13/13,风险决策一致 8/13。有损视频产生姿态/风险差异,不能把视频回灌解释为算法输出逐位相同。SIGTERM 退出 1.317 秒,回灌 SIGINT 退出 0.916 秒。完整原始结果在 `orin_can_results.json`。
|
||||
|
||||
此项仅证明真实摄像头/TensorRT、无 CAN 回退、状态录制/回灌及停止,不证明 CAN 收发。
|
||||
|
||||
## 设备访问准备与延期项目
|
||||
|
||||
已读取 USB 描述确认 `3068:0009` 为 ZLG USBCANFD-200U。经批准安装仅针对该 VID/PID 的 udev 规则,将设备设为 `root:plugdev 0660`;orin 原已属于 plugdev。规则及安装脚本均纳入独立包。权限修复后的接收测试被用户取消,未启动;后续没有再打开适配器接收或发送报文。
|
||||
|
||||
未验证实际总线 `0x391` 布局/物理单位、CAN/CANFD 真实发送或 ACK、双通道接线、波特率/终端电阻与实车匹配、硬件断开重连、长时间运行。没有新增板端故障注入或物理拔插操作。发送仅提供按需 C++ API,未定义或自动发送任何业务控制报文。
|
||||
|
||||
## 部署校验
|
||||
|
||||
本次程序 SHA256:`e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461`。
|
||||
|
||||
权限修复前的板端测试核对过 `/proc/<pid>/exe` 与源码构建文件一致。更新独立包时核对源码构建与 `orin/build/camera_risk` 哈希一致,并刷新 `orin/SHA256SUMS`(包含 SDK 库及 SONAME 链接)。用户要求暂不验证接收后,仅做文件和依赖校验,不再启动 CAN 测试进程。测试前没有常驻推理进程,没有修改既有启动参数。
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "can_bus.hpp"
|
||||
#include "zcan.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
std::deque<ZCANDataObj> incoming;
|
||||
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};
|
||||
ZCAN_20_MSG sentCan{};ZCAN_FD_MSG sentFd{};
|
||||
void check(bool ok,char const* message){if(!ok)throw std::runtime_error(message);}
|
||||
template<class F> void waitFor(F f) {
|
||||
auto deadline=std::chrono::steady_clock::now()+2s;
|
||||
while(!f()){check(std::chrono::steady_clock::now()<deadline,"Asynchronous CAN condition timed out");std::this_thread::sleep_for(1ms);}
|
||||
}
|
||||
template<class F> void rejects(F f){bool failed=false;try{f();}catch(std::exception const&){failed=true;}check(failed,"Expected rejection");}
|
||||
void reset() {
|
||||
opens=closes=resets=mergeCalls=txCalls=rxCalls=0;
|
||||
unavailable=initFailure=receiveFailure=txFailure=false;
|
||||
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) {
|
||||
ZCANDataObj obj{};obj.chnl=channel;obj.dataType=fd?ZCAN_DT_ZCAN_CANFD_DATA:ZCAN_DT_ZCAN_CAN_DATA;
|
||||
auto& hdr=fd?obj.data.zcanCANFDData.hdr:obj.data.zcanCANData.hdr;
|
||||
hdr.id=id;hdr.chn=channel;hdr.len=bytes.size();hdr.ts=stamp;
|
||||
auto* data=fd?obj.data.zcanCANFDData.dat:obj.data.zcanCANData.dat;
|
||||
std::copy(bytes.begin(),bytes.end(),data);
|
||||
{std::lock_guard<std::mutex> lock(mutex);incoming.push_back(obj);}changed.notify_all();
|
||||
}
|
||||
}
|
||||
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(init->clk==60000000&&init->mode==0&&init->aset.brp==5&&init->dset.brp==1,"Reference timing not retained");
|
||||
return initFailure&&port==1?0:1;
|
||||
}
|
||||
U32 VCI_SetReference(U32,U32,U32 port,U32 ref,void* data){
|
||||
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_ResetCAN(U32,U32,U32){++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<U32>::max();
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
changed.wait_for(lock,10ms,[]{return !incoming.empty();});
|
||||
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){
|
||||
check(count==1&&port==message->hdr.chn,"Invalid CAN send call");++txCalls;sentCan=*message;return txFailure?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;
|
||||
}
|
||||
}
|
||||
int main()try {
|
||||
using namespace cms;
|
||||
reset();{
|
||||
CanConfig c;c.enabled=false;CanBus bus(c);bus.start();
|
||||
check(bus.snapshot().status=="DISABLED"&&!bus.send({0,0x123,false,false,false,{1,2}}),"Disabled CAN used hardware");
|
||||
}check(opens==0,"Disabled CAN opened device");
|
||||
reset();unavailable=true;{
|
||||
CanBus bus({});bus.start();check(bus.snapshot().status=="UNAVAILABLE","Missing adapter not reported");
|
||||
}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");
|
||||
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==2;});
|
||||
auto s=bus.snapshot();
|
||||
check(s.channels[0].positions==std::array<int,4>{1,37,128,255},"Four byte positions overlap");
|
||||
check(s.channels[1].positions==std::array<int,4>{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");
|
||||
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;});
|
||||
s=bus.snapshot();check(s.channels[0].received_monotonic_ns==timestamp&&s.receive_errors==1,"Invalid input refreshed signal");
|
||||
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(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)};
|
||||
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({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");
|
||||
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";
|
||||
}catch(std::exception const& e){std::cerr<<e.what()<<'\n';return 1;}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"platform": "orin-desktop",
|
||||
"binary": "/home/orin/vision-anti-pinch-0923/build/camera_risk",
|
||||
"binary_sha256": "e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461",
|
||||
"running_binary_sha256": "e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461",
|
||||
"recorded_frames": 13,
|
||||
"replayed_frames": 13,
|
||||
"frames_with_people": 13,
|
||||
"max_raw_pose_error_px": 938.180541992,
|
||||
"matching_track_frames": 13,
|
||||
"matching_decision_frames": 8,
|
||||
"video_bytes": 322966,
|
||||
"uncompressed_bgr_bytes": 115015680,
|
||||
"sigterm_seconds": 1.3167834819996642,
|
||||
"replay_sigint_seconds": 0.9158224279999558,
|
||||
"interrupted_frames": 2,
|
||||
"can_statuses": [
|
||||
"UNAVAILABLE"
|
||||
],
|
||||
"can_received": 0,
|
||||
"can_0x391_by_channel": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
@@ -106,6 +106,9 @@ def main():
|
||||
for before, after, stem in zip(original, restored, rows(recorded)):
|
||||
assert before['frame_index'] == after['frame_index']
|
||||
assert before['capture'] == after['source_capture']
|
||||
if 'can' in before['result']:
|
||||
expected_can = dict(before['result']['can'], source='replay')
|
||||
assert expected_can == after['result']['can'], 'Recorded CAN state changed during replay'
|
||||
assert before['video_frame_index'] == after['video_frame_index']
|
||||
matching_tracks += before['result']['track_ids'] == after['result']['track_ids']
|
||||
matching_decisions += before['result']['risk']['decision'] == after['result']['risk']['decision']
|
||||
@@ -124,7 +127,10 @@ def main():
|
||||
'running_binary_sha256': running_hash, 'recorded_frames': len(original), 'replayed_frames': len(restored),
|
||||
'frames_with_people': sum(bool(row['result']['track_ids']) for row in original),
|
||||
'max_raw_pose_error_px': max_pose_error, 'matching_track_frames': matching_tracks, 'matching_decision_frames': matching_decisions,
|
||||
'video_bytes': (recorded/'frames.mp4').stat().st_size, 'uncompressed_bgr_bytes': len(original)*int(probe['width'])*int(probe['height'])*3, 'sigterm_seconds': term_seconds, 'replay_sigint_seconds': interrupt_seconds, 'interrupted_frames': interrupted_count}
|
||||
'video_bytes': (recorded/'frames.mp4').stat().st_size, 'uncompressed_bgr_bytes': len(original)*int(probe['width'])*int(probe['height'])*3, 'sigterm_seconds': term_seconds, 'replay_sigint_seconds': interrupt_seconds, 'interrupted_frames': interrupted_count,
|
||||
'can_statuses': sorted({r['result'].get('can', {}).get('status', 'NOT_RECORDED') for r in original}),
|
||||
'can_received': max(r['result'].get('can', {}).get('received', 0) for r in original),
|
||||
'can_0x391_by_channel': [max((r['result'].get('can', {}).get('channels', [{}, {}])[ch].get('received', 0) for r in original)) for ch in range(2)]}
|
||||
assert result['binary_sha256'] == running_hash
|
||||
(args.output/'report.json').write_text(json.dumps(result, indent=2)+'\n')
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
test_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
"${CXX:-g++}" -std=c++17 -O1 -g -Wall -Wextra -Wpedantic -pthread \
|
||||
-fsanitize=undefined -fno-sanitize-recover=all -I"$root/include" -isystem "$root/third_party/zlg" \
|
||||
"$root/src/can_bus.cpp" "$root/tests/can_bus_test.cpp" -o "$test_dir/can_bus_test"
|
||||
"$test_dir/can_bus_test"
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "inference_session.hpp"
|
||||
#include "camera_pipeline.hpp"
|
||||
#include "serialization.hpp"
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
@@ -24,6 +25,8 @@ int main(int argc,char** argv)try {
|
||||
im->pixels.resize(9600);
|
||||
for(int y=0;y<48;++y)for(int x=0;x<192;++x)im->pixels[y*200+x]=32+x/3+y%4;
|
||||
FrameResult r;r.image=im;r.frame_index=3;r.capture.frame_index=3;r.capture.driver_sequence=42;
|
||||
r.can.status="OPEN";r.can.received=7;r.can.channels[0].positions={0,25,100,255};
|
||||
r.can.channels[0].valid=true;r.can.channels[0].received=7;r.can.channels[0].age_ms=12.5;
|
||||
r.capture.capture_monotonic_ns=123;r.capture.received_monotonic_ns=124;
|
||||
{
|
||||
SessionWriter writer(c);writer.write(r,{{"raw_keypoints",{1,2,3}}});
|
||||
@@ -37,6 +40,8 @@ int main(int argc,char** argv)try {
|
||||
PipelineConfig replay;replay.replay_session=c.record_session;
|
||||
SessionReader reader(replay);check(replay.camera.width==64&&replay.detector_confidence==.42,"Settings not restored");
|
||||
auto first=reader.read();check(first&&first->metadata.frame_index==3,"First index mismatch");
|
||||
auto can=deserializeCan(first->replay_record->at("result").at("can"));
|
||||
check(can.channels[0].positions==r.can.channels[0].positions&&can.channels[0].valid&&can.channels[0].age_ms==12.5,"Recorded CAN snapshot changed");
|
||||
check(first->metadata.driver_sequence==42&&first->metadata.capture_monotonic_ns!=123,"Capture metadata mismatch");
|
||||
check(first->replay_record->at("capture").at("capture_monotonic_ns")==123,"Original clock lost");
|
||||
for(int y=0;y<48;++y)for(int x=0;x<192;++x)check(std::abs(int(first->image->pixels[y*192+x])-(32+x/3+y%4))<12,"Decoded first frame mismatch");
|
||||
|
||||
Reference in New Issue
Block a user