diff --git a/CMakeLists.txt b/CMakeLists.txt index 3de7c2f..7e7ea93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,11 +13,19 @@ find_path(TENSORRT_INCLUDE_DIR NvInfer.h PATH_SUFFIXES aarch64-linux-gnu x86_64- find_library(TENSORRT_LIBRARY nvinfer REQUIRED) add_library(camera_risk_core src/trt_engine.cpp src/pose_engine.cu src/gpu_preprocess.cu src/uyvy_converter.cu - src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/inference_session.cpp src/serialization.cpp src/output_writer.cpp src/result_renderer.cpp src/config.cpp) + src/can_bus.cpp src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/inference_session.cpp src/serialization.cpp src/output_writer.cpp src/result_renderer.cpp src/config.cpp) target_include_directories(camera_risk_core PUBLIC include third_party ${TENSORRT_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS}) target_link_libraries(camera_risk_core PUBLIC ${TENSORRT_LIBRARY} ${OpenCV_LIBS} Threads::Threads CUDA::cudart CUDA::nppc CUDA::nppig CUDA::nppidei CUDA::nppicc) +target_include_directories(camera_risk_core SYSTEM PRIVATE third_party/zlg) target_compile_options(camera_risk_core PRIVATE $<$:-Wall;-Wextra;-Wpedantic>) add_library(camera_risk_visualizer src/visualizer.cpp) target_link_libraries(camera_risk_visualizer PUBLIC camera_risk_core) add_executable(camera_risk src/main.cpp) target_link_libraries(camera_risk PRIVATE camera_risk_visualizer) + +# Reference USBCANFD SDK is shipped with the standalone Orin package. +target_link_libraries(camera_risk_core PUBLIC "${CMAKE_SOURCE_DIR}/orin/lib/libusbcanfd.so" + "-Wl,--no-as-needed" "${CMAKE_SOURCE_DIR}/orin/lib/libusb-1.0.so" "-Wl,--as-needed") +set_target_properties(camera_risk PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN/../orin/lib;/usr/local/cuda/lib64") +# Keep the bundled SDK/libusb pair together even with a desktop LD_LIBRARY_PATH. +target_link_options(camera_risk PRIVATE "-Wl,--disable-new-dtags") diff --git a/README.md b/README.md index d9c6598..5f5ead7 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,44 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回 编解码及会话回归:`./tests/run_session.sh`,无需摄像头或 TensorRT。真实摄像头与 TensorRT 验证脚本:`python3 tests/orin_session_test.py --binary build/camera_risk --config config/camera.json --output /tmp/new-session-test`;输出目录必须不存在,脚本不会停止已有部署进程。 本次实机结果与未覆盖范围见 [H.264 会话测试报告](tests/ORIN_SESSION_REPORT.md)。 + +## 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,需检查设备后重启程序。 + +首次使用 USB-CAN 时,若节点为 root:root 且普通用户不可写,可运行 `./scripts/enable_can_access.sh`(需要 sudo)。它只为 `3068:0009` 适配器安装 `plugdev` 组读写规则并刷新设备权限;运行用户须属于 `plugdev`,新加组后需重新登录。无需以 root 运行整个推理程序。 + +`0x391` 四个信号按参考工程注释中的四个连续字节解析: + +| 字节(从 0 开始) | 预览标签 | 信号 | +|---|---|---| +| 0 | LF | 左前门实际位置反馈 | +| 1 | RF | 右前门实际位置反馈 | +| 2 | LR | 左后门实际位置反馈 | +| 3 | RR | 右后门实际位置反馈 | + +显示值是无符号原始字节 0–255,尚无物理单位、缩放或无效值定义,因此不标为百分比、不把 255 擅自视为无效。接收 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 只提供状态显示和下游数据,本次没有新增基于门位置的推理启停或风险门控。 + +下游通过 `pipeline.canStatus()` 读取当前状态;`pipeline.latest()->can` 是该推理结果生成时的快照。按需发送使用线程安全的 `pipeline.sendCan()`: + +```cpp +cms::CanFrame frame; +frame.channel = 0; // CH0 或 CH1 +frame.id = 0x123; // 按实际协议设置 +frame.data = {0x01, 0x02}; // 示例数据,应替换为实际报文 +bool sent = pipeline.sendCan(frame); + +// CANFD:frame.fd = true;可按协议设置 extended、bitrate_switch。 +// CAN 数据长度 0–8;CANFD 长度 0–8、12、16、20、24、32、48、64。 +// 标准 ID 最大 0x7FF,扩展 ID 最大 0x1FFFFFFF。 +``` + +非法通道/ID/长度/标志抛出 `std::invalid_argument`;设备未就绪或 SDK 发送失败返回 false,发送计数以 SDK 返回为准。接口不自动填充/截断数据,不自动发送周期或风险控制报文。发送调用与设备关闭互斥;双通道接收保持独立线程运行。 + +结果 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 接收。 diff --git a/config/camera.json b/config/camera.json index 433c732..d2c6a98 100644 --- a/config/camera.json +++ b/config/camera.json @@ -18,5 +18,11 @@ "warning_relative": 0.3, "release_relative": 0.45, "calibration_verified": false + }, + "can": { + "enabled": true, + "device_index": 0, + "timeout_ms": 1000, + "termination": true } } diff --git a/include/camera_pipeline.hpp b/include/camera_pipeline.hpp index cf32bb4..2ba89af 100644 --- a/include/camera_pipeline.hpp +++ b/include/camera_pipeline.hpp @@ -9,6 +9,7 @@ namespace cms { struct PipelineConfig { static constexpr char const* session_variant="inference-only"; CameraConfig camera; + CanConfig can; OutputOptions output; std::filesystem::path record_session,replay_session; std::filesystem::path engines,door_mask; @@ -36,6 +37,8 @@ public: CameraPipeline& operator=(CameraPipeline const&)=delete; void start(); FrameResultPtr latest() const; + CanSnapshot canStatus() const; + bool sendCan(CanFrame const& frame); bool running() const noexcept; void requestStop() noexcept; void join(); diff --git a/include/can_bus.hpp b/include/can_bus.hpp new file mode 100644 index 0000000..cfcdd32 --- /dev/null +++ b/include/can_bus.hpp @@ -0,0 +1,26 @@ +#pragma once +#include "can_types.hpp" +#include + +namespace cms { +// One lifecycle per instance; only one owner per device index in a process. +// start() must run after StopSignals so the receiver inherits its signal mask. +// Missing hardware is reported by snapshot(), and does not stop inference. +class CanBus { +public: + explicit CanBus(CanConfig config); + ~CanBus(); + CanBus(CanBus const&)=delete; + CanBus& operator=(CanBus const&)=delete; + void start(); + void requestStop() noexcept; + void join() noexcept; + CanSnapshot snapshot() const; + // 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); +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/can_types.hpp b/include/can_types.hpp new file mode 100644 index 0000000..9b6625f --- /dev/null +++ b/include/can_types.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +namespace cms { +struct CanConfig { + bool enabled{true}; + int device_index{0}; + int timeout_ms{1000}; + bool termination{true}; +}; +struct CanDoorSignals { + // Raw unsigned bytes: left front, right front, left rear, right rear. + // No scale/unit is specified in the reference protocol. + std::array positions{{-1,-1,-1,-1}}; + bool valid{},fd{},extended{}; + std::uint64_t received{},received_monotonic_ns{}; + std::uint32_t device_timestamp_us{}; + double age_ms{-1}; +}; +struct CanSnapshot { + std::string source{"live"},status{"DISABLED"}; + std::array channels; + std::uint64_t received{},transmitted{},transmit_errors{},receive_errors{}; +}; +struct CanFrame { + int channel{}; + std::uint32_t id{}; + bool fd{},extended{},bitrate_switch{}; + std::vector data; +}; +} diff --git a/include/frame_result.hpp b/include/frame_result.hpp index 7999cce..3bc16dd 100644 --- a/include/frame_result.hpp +++ b/include/frame_result.hpp @@ -1,6 +1,7 @@ #pragma once // Public downstream contract. Owns its data; no GPU, JSON or network handles. #include "image.hpp" +#include "can_types.hpp" #include #include #include @@ -67,6 +68,7 @@ struct FrameResult { std::vector track_ids; std::vector> boxes; RiskResult risk; + CanSnapshot can; CaptureMetadata capture; StageTiming timing; std::vector persons; diff --git a/include/serialization.hpp b/include/serialization.hpp index d6e7fc2..d44f4d4 100644 --- a/include/serialization.hpp +++ b/include/serialization.hpp @@ -1,4 +1,8 @@ #pragma once #include "frame_result.hpp" #include "door_risk.hpp" -namespace cms { Json serializeResult(FrameResult const& result); } +namespace cms { +Json serializeCan(CanSnapshot const& can); +CanSnapshot deserializeCan(Json const& value); +Json serializeResult(FrameResult const& result); +} diff --git a/include/visualizer.hpp b/include/visualizer.hpp index 59494b2..8a6474e 100644 --- a/include/visualizer.hpp +++ b/include/visualizer.hpp @@ -5,7 +5,8 @@ namespace cms { class CameraPipeline; // Pure drawing from the immutable result. Never re-runs inference. -cv::Mat renderResult(FrameResult const& result); +cv::Mat renderResult(FrameResult const& result,CanSnapshot const* liveCan=nullptr); +void renderCanOverlay(cv::Mat& image,CanSnapshot const& can); // Optional observer: closing the window returns without stopping the pipeline. void runLocalVisualization(CameraPipeline const& pipeline); } diff --git a/orin/README.md b/orin/README.md index 87d47ec..9820d90 100644 --- a/orin/README.md +++ b/orin/README.md @@ -51,3 +51,11 @@ bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查 ``` 录制目录必须不存在。会话保存一份 H.264 视频、逐帧过程 JSON、推理配置与门区副本;回灌解码视频并重新执行 TensorRT 推理,不打开摄像头,结束后自动退出。使用当前配置指向的引擎;有损压缩会引起推理结果差异。需要 OpenCV FFmpeg 的 H.264 编码支持。强制退出或断电导致未封装完成的会话不可回灌;正常 Ctrl+C / SIGTERM 会完成封装。详细格式与限制见工程 README。 + +## CAN 状态与发送接口 + +实时运行默认接收 USBCANFD 双通道(500 kbps / 2 Mbps)。预览新增 `0x391` 的 LF/RF/LR/RR 四门原始位置值,每通道分别显示,并标记新鲜度和收发计数。`--no-can` 可关闭;设备缺失不停止推理。`config/camera.json` 的 `can.termination` 控制终端电阻,默认沿用参考工程的开启设置。 + +`lib/` 包含 SDK 与 libusb 及其 SONAME 链接。回灌使用录制的 CAN 快照,不访问总线;旧会话显示 NOT_RECORDED。按需发送通过 C++ `CameraPipeline::sendCan()`,参数与完整示例见工程 README。 + +USB-CAN 首次接入如出现权限不足,运行 `./scripts/enable_can_access.sh`(需要 sudo,运行用户属于 plugdev),仅为 `3068:0009` 适配器安装设备访问规则。 diff --git a/orin/SHA256SUMS b/orin/SHA256SUMS index 12b8838..9b353ff 100644 --- a/orin/SHA256SUMS +++ b/orin/SHA256SUMS @@ -1,11 +1,17 @@ 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko -cd9711d71c6d04f40adf9434f893abe1744e9e2013f1459ddf736a80dcbbfd64 ./build/camera_risk -8a572f661e056e3916758a442e73212842bfe89361dd1b07b87de8f6ae523cf8 ./config/camera.json +e8788d22fd6db0e3b9edc95a02322a51ca7951e1df81fea10f34db7da3143461 ./build/camera_risk +d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json 9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo11s.engine -011e8d1e9b735536630a26cb0ee9930f4f6ae52052e26427156fb4d9a6f96e83 ./README.md +8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so +8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so.0 +ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so +ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so.1.0.12 +bb61f669d8b471c5ca42045b31a0e6fd300235da37a80893ae229f37deb1b7b9 ./README.md 7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh +41ebe2dcf57674232ca137d024e99447b467049cb1c63a284daae274296cb78c ./scripts/99-usbcanfd.rules +920102fc5e91bde9b78f6f7c5cb2ff8761a0c39b63c7a28b3347d5de5fe7c60a ./scripts/enable_can_access.sh 54e1d63e3aa72ad9b056a224d55968d4947ca6703a8d1c405570de5707a7e7ef ./scripts/initialize_camera.sh diff --git a/orin/build/camera_risk b/orin/build/camera_risk index c7896d7..5bff899 100755 Binary files a/orin/build/camera_risk and b/orin/build/camera_risk differ diff --git a/orin/config/camera.json b/orin/config/camera.json index 7c5d197..f036a4e 100644 --- a/orin/config/camera.json +++ b/orin/config/camera.json @@ -18,5 +18,11 @@ "warning_relative": 0.3, "release_relative": 0.45, "calibration_verified": false + }, + "can": { + "enabled": true, + "device_index": 0, + "timeout_ms": 1000, + "termination": true } } diff --git a/orin/lib/libusb-1.0.so b/orin/lib/libusb-1.0.so new file mode 100644 index 0000000..0824ee0 Binary files /dev/null and b/orin/lib/libusb-1.0.so differ diff --git a/orin/lib/libusb-1.0.so.0 b/orin/lib/libusb-1.0.so.0 new file mode 120000 index 0000000..4970d7f --- /dev/null +++ b/orin/lib/libusb-1.0.so.0 @@ -0,0 +1 @@ +libusb-1.0.so \ No newline at end of file diff --git a/orin/lib/libusbcanfd.so b/orin/lib/libusbcanfd.so new file mode 100644 index 0000000..72b9370 Binary files /dev/null and b/orin/lib/libusbcanfd.so differ diff --git a/orin/lib/libusbcanfd.so.1.0.12 b/orin/lib/libusbcanfd.so.1.0.12 new file mode 120000 index 0000000..2ee2cf4 --- /dev/null +++ b/orin/lib/libusbcanfd.so.1.0.12 @@ -0,0 +1 @@ +libusbcanfd.so \ No newline at end of file diff --git a/orin/scripts/99-usbcanfd.rules b/orin/scripts/99-usbcanfd.rules new file mode 100644 index 0000000..625f503 --- /dev/null +++ b/orin/scripts/99-usbcanfd.rules @@ -0,0 +1,2 @@ +# USBCANFD adapter used by the reference project. Grant the plugdev group access. +SUBSYSTEM=="usb", ATTR{idVendor}=="3068", ATTR{idProduct}=="0009", MODE="0660", GROUP="plugdev", TAG+="uaccess" diff --git a/orin/scripts/enable_can_access.sh b/orin/scripts/enable_can_access.sh new file mode 100755 index 0000000..b09aea3 --- /dev/null +++ b/orin/scripts/enable_can_access.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +root_command() { if [[ $EUID -eq 0 ]]; then "$@"; else sudo -n "$@"; fi; } +getent group plugdev >/dev/null || { echo 'Create a plugdev group and add the runtime user before running this script.' >&2; exit 1; } +root_command install -m 0644 "$root/99-usbcanfd.rules" /etc/udev/rules.d/99-usbcanfd.rules +root_command udevadm control --reload-rules +root_command udevadm trigger --subsystem-match=usb --attr-match=idVendor=3068 --attr-match=idProduct=0009 --action=add +root_command udevadm settle +printf '%s\n' 'USBCANFD 3068:0009 access enabled for plugdev. Newly added group members must log in again.' diff --git a/scripts/enable_can_access.sh b/scripts/enable_can_access.sh new file mode 100755 index 0000000..55b88f8 --- /dev/null +++ b/scripts/enable_can_access.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$root/orin/scripts/enable_can_access.sh" "$@" diff --git a/scripts/package_orin.sh b/scripts/package_orin.sh index ba0280c..75fd56b 100755 --- a/scripts/package_orin.sh +++ b/scripts/package_orin.sh @@ -7,6 +7,6 @@ cp -p "$root/config/door_mask.json" "$root/orin/config/" sed 's|../orin/engines|../engines|' "$root/config/camera.json" > "$root/orin/config/camera.json" ( cd "$root/orin" - find . -type f ! -path './output/*' ! -path './outputs/*' ! -path './logs/*' ! -name SHA256SUMS -print0 | + find . \( -type f -o -type l \) ! -path './output/*' ! -path './outputs/*' ! -path './logs/*' ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS ) diff --git a/src/camera_pipeline.cpp b/src/camera_pipeline.cpp index 37c97b7..d7f1b2f 100644 --- a/src/camera_pipeline.cpp +++ b/src/camera_pipeline.cpp @@ -5,6 +5,8 @@ #include "door_risk.hpp" #include "records.hpp" #include "inference_session.hpp" +#include "can_bus.hpp" +#include "serialization.hpp" #include "result_conversion.hpp" #include #include @@ -60,6 +62,7 @@ struct CameraPipeline::Impl { std::unique_ptr risk; std::shared_ptr doorPixels; std::unique_ptr camera; + std::unique_ptr can; std::thread captureThread,detectorThread,inferenceThread; mutable std::mutex stateMutex; std::exception_ptr failure; @@ -70,6 +73,8 @@ struct CameraPipeline::Impl { explicit Impl(PipelineConfig c):config(std::move(c)),captures(config.queue_capacity),detections(config.queue_capacity) { if(!config.replay_session.empty())sessionReader=std::make_unique(config); + auto canConfig=config.can;if(sessionReader)canConfig.enabled=false; + can=std::make_unique(canConfig); if(config.queue_capacity>8)throw std::invalid_argument("Queue capacity must be between 1 and 8"); if(config.max_people<1||config.max_people>16||!std::isfinite(config.detector_confidence)||config.detector_confidence<=0||config.detector_confidence>=1) throw std::invalid_argument("Invalid detector configuration"); @@ -80,9 +85,11 @@ struct CameraPipeline::Impl { if(captureThread.joinable())captureThread.join(); if(detectorThread.joinable())detectorThread.join(); if(inferenceThread.joinable())inferenceThread.join(); + can->join(); } void stop() noexcept { stopped=true;active=false; + can->requestStop(); // Camera stays alive until all threads have joined. start()/destruction // are owner-thread operations; requestStop() only interrupts existing work. {std::lock_guard lock(stateMutex);if(camera)camera->requestStop();} @@ -98,6 +105,7 @@ struct CameraPipeline::Impl { void start() { {std::lock_guard lock(stateMutex);if(started||stopped)throw std::logic_error("CameraPipeline can only be started once");started=true;} try { + can->start(); outputWriter=std::make_unique(config.output); sessionWriter=std::make_unique(config); detector=std::make_unique(config.engines/"yolo11s.engine",logger,config.detector_confidence,config.max_people); @@ -160,6 +168,12 @@ struct CameraPipeline::Impl { auto record=makeRecord(index,raw,isolated,fused,!config.record_session.empty(),false); auto riskData=risk->evaluate(index,record); auto result=std::make_shared(makeFrameResult(record,riskData,detected->frame.metadata)); + if(detected->frame.replay_record) { + auto const& recorded=detected->frame.replay_record->at("result"); + 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(); 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; @@ -178,6 +192,17 @@ CameraPipeline::CameraPipeline(PipelineConfig config):impl_(std::make_uniquestart();} FrameResultPtr CameraPipeline::latest()const{return impl_->results.load();} +CanSnapshot CameraPipeline::canStatus()const { + if(impl_->sessionReader) { + auto result=impl_->results.load();if(result)return result->can; + CanSnapshot s;s.source="replay";s.status="WAITING";return s; + } + return impl_->can->snapshot(); +} +bool CameraPipeline::sendCan(CanFrame const& frame) { + if(impl_->sessionReader)throw std::logic_error("CAN transmission is disabled during replay"); + return impl_->can->send(frame); +} bool CameraPipeline::running()const noexcept{return impl_->active.load();} void CameraPipeline::requestStop()noexcept{impl_->stop();} void CameraPipeline::join(){impl_->joinWorkers();{std::lock_guard lock(impl_->stateMutex);impl_->camera.reset();}impl_->rethrowFailure();} diff --git a/src/can_bus.cpp b/src/can_bus.cpp new file mode 100644 index 0000000..1cfdd53 --- /dev/null +++ b/src/can_bus.cpp @@ -0,0 +1,171 @@ +#include "can_bus.hpp" +#include "zcan.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cms { +namespace { +constexpr unsigned deviceType=33,receiveCapacity=128; +std::mutex ownersMutex; +std::set owners; +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)) + 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))) + throw std::invalid_argument("Invalid CAN/CANFD payload length"); +} +} +struct CanBus::Impl { + CanConfig config; + mutable std::mutex stateMutex; + std::mutex lifecycleMutex,waitMutex; + std::condition_variable wake; + CanSnapshot state; + std::atomic stopped{false}; + bool started{},opened{},owned{}; + std::array initialized{}; + std::thread receiver; + 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"; + } + ~Impl(){requestStop();join();} + 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); + VCI_CloseDevice(deviceType,config.device_index); + opened=false;initialized.fill(false); + } + if(owned){std::lock_guard lock(ownersMutex);owners.erase(config.device_index);owned=false;} + } + void start() { + std::lock_guard lock(lifecycleMutex); + if(started)throw std::logic_error("CAN bus can only be started once"); + started=true; + if(!config.enabled||stopped)return; + { + std::lock_guard ownerLock(ownersMutex); + if(!owners.insert(config.device_index).second)throw std::logic_error("CAN device already owned by another pipeline"); + owned=true; + } + try { + if(VCI_OpenDevice(deviceType,config.device_index,0)!=1)throw std::runtime_error("VCI_OpenDevice failed"); + opened=true; + // Reference USBCANFD: 60 MHz clock, arbitration 500 kbps, data 2 Mbps. + for(unsigned i=0;i<2;++i) { + 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.dset.tseg1=10;init.dset.tseg2=2;init.dset.sjw=2;init.dset.brp=1; + if(VCI_InitCAN(deviceType,config.device_index,i,&init)!=1)throw std::runtime_error("VCI_InitCAN failed"); + initialized[i]=true; + U32 termination=config.termination?1:0; + if(VCI_SetReference(deviceType,config.device_index,i,CMD_CAN_TRES,&termination)!=1) + throw std::runtime_error("CAN termination configuration failed"); + } + U32 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"); + for(unsigned i=0;i<2;++i)if(VCI_StartCAN(deviceType,config.device_index,i)!=1) + throw std::runtime_error("VCI_StartCAN failed"); + setStatus("OPEN"); + // Exactly one merged reader for both channels; no competing readers. + receiver=std::thread([this]{receiveLoop();}); + std::cout<<"CAN device "< lock(lifecycleMutex); + if(receiver.joinable())receiver.join(); + releaseDevice(); + if(config.enabled&&started)setStatus("STOPPED"); + } + void receiveFrame(ZCAN_MSG_HDR const& header,std::uint8_t const* data,bool fd,unsigned channel) { + 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>1||header.len>(fd?64:8)||header.id>(header.inf.sef?0x1fffffffu:0x7ffu)) { + ++state.receive_errors;return; + } + ++state.received; + if(header.id!=0x391)return; + if(header.len<4){++state.receive_errors;return;} + auto& s=state.channels[channel]; + for(unsigned i=0;i<4;++i)s.positions[i]=data[i]; + ++s.received;s.received_monotonic_ns=nowNs();s.device_timestamp_us=header.ts; + s.fd=fd;s.extended=header.inf.sef; + } + void receiveLoop() noexcept { + try { + std::array frames{}; + while(!stopped) { + auto count=VCI_ReceiveData(deviceType,config.device_index,0,frames.data(),receiveCapacity,100); + if(count>receiveCapacity) { + {std::lock_guard lock(stateMutex);++state.receive_errors;state.status="ERROR";} + std::cerr<<"CAN receive failed; inference continues without live CAN"< lock(stateMutex);++state.receive_errors;} + } + if(!count){std::unique_lock lock(waitMutex);wake.wait_for(lock,std::chrono::milliseconds(5),[this]{return stopped.load();});} + } + }catch(...) {setStatus("ERROR");} + } + CanSnapshot snapshot() const { + std::lock_guard lock(stateMutex);auto result=state;auto now=nowNs(); + for(auto& c:result.channels) { + c.age_ms=c.received?(now-c.received_monotonic_ns)*1e-6:-1; + c.valid=state.status=="OPEN"&&!stopped&&c.received&&c.age_ms<=config.timeout_ms; + } + return result; + } + bool send(CanFrame const& f) { + validate(f); + std::lock_guard lock(lifecycleMutex); + bool ready; + {std::lock_guard stateLock(stateMutex);ready=state.status=="OPEN";} + unsigned sent=0; + if(opened&&!stopped&&ready) { + ZCAN_MSG_HDR header{};header.id=f.id;header.chn=f.channel;header.len=f.data.size(); + header.inf.fmt=f.fd?1:0;header.inf.sef=f.extended;header.inf.brs=f.bitrate_switch; + if(f.fd) { + ZCAN_FD_MSG message{};message.hdr=header;std::copy(f.data.begin(),f.data.end(),message.dat); + sent=VCI_TransmitFD(deviceType,config.device_index,f.channel,&message,1); + }else { + ZCAN_20_MSG message{};message.hdr=header;std::copy(f.data.begin(),f.data.end(),message.dat); + sent=VCI_Transmit(deviceType,config.device_index,f.channel,&message,1); + } + } + std::lock_guard stateLock(stateMutex); + if(sent==1)++state.transmitted;else ++state.transmit_errors; + return sent==1; + } +}; +CanBus::CanBus(CanConfig c):impl_(std::make_unique(c)){} +CanBus::~CanBus()=default; +void CanBus::start(){impl_->start();} +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);} +} diff --git a/src/config.cpp b/src/config.cpp index d67f24e..3b983ab 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -19,6 +19,11 @@ PipelineConfig loadConfig(std::filesystem::path const& path) { auto const& door=j.at("door");config.door_mask=resolve(door.at("mask")); config.warning_relative=door.at("warning_relative");config.release_relative=door.at("release_relative"); config.door_mask_calibration_verified=door.at("calibration_verified"); + if(j.contains("can")) { + auto const& can=j.at("can");config.can.enabled=can.value("enabled",true); + config.can.device_index=can.value("device_index",0);config.can.timeout_ms=can.value("timeout_ms",1000); + config.can.termination=can.value("termination",true); + } return config; } } diff --git a/src/inference_session.cpp b/src/inference_session.cpp index 76a537d..1668e7f 100644 --- a/src/inference_session.cpp +++ b/src/inference_session.cpp @@ -48,7 +48,8 @@ SessionWriter::SessionWriter(PipelineConfig const& c):directory_(c.record_sessio Json config={{"width",c.camera.width},{"height",c.camera.height},{"fps",c.camera.fps}, {"detector_confidence",c.detector_confidence},{"max_people",c.max_people}, {"warning_relative",c.warning_relative},{"release_relative",c.release_relative}, - {"calibration_verified",c.door_mask_calibration_verified}}; + {"calibration_verified",c.door_mask_calibration_verified}, + {"can",{{"enabled",c.can.enabled},{"device_index",c.can.device_index},{"timeout_ms",c.can.timeout_ms},{"termination",c.can.termination}}}}; Json models=Json::object(); for(auto name:{"yolo11s.engine","rtmw_l_384.engine"}) models[name]={{"path",(c.engines/name).string()},{"bytes",std::filesystem::file_size(c.engines/name)}}; diff --git a/src/main.cpp b/src/main.cpp index 8f81f5b..a5c7910 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,7 +11,7 @@ int main(int argc,char** argv)try { auto root=std::filesystem::canonical("/proc/self/exe").parent_path().parent_path(); auto configPath=root/"config/camera.json"; cms::OutputOptions output;output.directory=root/"output"; - bool visualize=true; + bool visualize=true,disableCan=false; std::filesystem::path recordSession,replaySession; std::map> arguments{ {"--record-session",[&](int&i){if(++i>=argc)throw std::invalid_argument("--record-session needs a directory");recordSession=argv[i];}}, @@ -19,14 +19,16 @@ int main(int argc,char** argv)try { {"--save-original",[&](int&){output.original=true;}}, {"--save-annotated",[&](int&){output.annotated=true;}}, {"--save-results",[&](int&){output.results=true;}}, + {"--no-can",[&](int&){disableCan=true;}}, {"--visualize",[&](int&){visualize=true;}}, {"--no-visualize",[&](int&){visualize=false;}}, {"--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] [--visualize|--no-visualize]\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 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);}} }; for(int i=1;isecond(i);} auto config=cms::loadConfig(configPath);config.output=output;config.record_session=recordSession;config.replay_session=replaySession; + if(disableCan)config.can.enabled=false; cms::CameraPipeline pipeline(config); cms::StopSignals signals(pipeline); pipeline.start(); diff --git a/src/result_renderer.cpp b/src/result_renderer.cpp index a315abf..5fd15ad 100644 --- a/src/result_renderer.cpp +++ b/src/result_renderer.cpp @@ -22,7 +22,25 @@ void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,b for(double x=0;x>(); + for(auto raw:c.positions)if(raw < -1||raw>255)throw std::runtime_error("Recorded CAN position out of range"); + c.valid=v.at("valid");c.received=v.at("received");c.received_monotonic_ns=v.at("received_monotonic_ns"); + c.device_timestamp_us=v.at("device_timestamp_us");c.age_ms=v.at("age_ms");c.fd=v.at("fd");c.extended=v.at("extended"); + } + return s; +} Json serializeResult(FrameResult const& r) { Json out={{"frame_index",r.frame_index},{"track_ids",r.track_ids},{"boxes",r.boxes},{"skeletons",Json::array()},{"risk",riskJson(r.risk)}}; for(std::size_t i=0;i #include #include +#include namespace cms { namespace { constexpr char windowName[]="CMS Camera Risk"; } void runLocalVisualization(CameraPipeline const& pipeline) { @@ -11,9 +12,21 @@ void runLocalVisualization(CameraPipeline const& pipeline) { ~Window(){try{if(cvGetWindowHandle(windowName))cv::destroyWindow(windowName);}catch(...) {}} } window; FrameResultPtr shown; + auto nextRefresh=std::chrono::steady_clock::now(); while(pipeline.running()) { auto result=pipeline.latest(); - if(result&&result!=shown){cv::imshow(windowName,renderResult(*result));shown=std::move(result);} + auto now=std::chrono::steady_clock::now(); + if(result!=shown||now>=nextRefresh) { + auto can=pipeline.canStatus();cv::Mat image; + if(result)image=renderResult(*result,&can); + else { + image=cv::Mat(800,1000,CV_8UC3,cv::Scalar(20,24,30)); + cv::putText(image,"Waiting for inference...",{24,64},cv::FONT_HERSHEY_SIMPLEX,.8,{235,235,235},2); + renderCanOverlay(image,can); + } + cv::imshow(windowName,image);shown=std::move(result); + nextRefresh=now+std::chrono::milliseconds(100); + } int key=cv::waitKey(16)&0xff; if(key==27||key=='q'||key=='Q'||cvGetWindowHandle(windowName)==nullptr)return; } diff --git a/tests/ORIN_CAN_REPORT.md b/tests/ORIN_CAN_REPORT.md new file mode 100644 index 0000000..c042830 --- /dev/null +++ b/tests/ORIN_CAN_REPORT.md @@ -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//exe` 与源码构建文件一致。更新独立包时核对源码构建与 `orin/build/camera_risk` 哈希一致,并刷新 `orin/SHA256SUMS`(包含 SDK 库及 SONAME 链接)。用户要求暂不验证接收后,仅做文件和依赖校验,不再启动 CAN 测试进程。测试前没有常驻推理进程,没有修改既有启动参数。 diff --git a/tests/can_bus_test.cpp b/tests/can_bus_test.cpp new file mode 100644 index 0000000..d847a5c --- /dev/null +++ b/tests/can_bus_test.cpp @@ -0,0 +1,112 @@ +#include "can_bus.hpp" +#include "zcan.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using namespace std::chrono_literals; +std::mutex mutex; +std::condition_variable changed; +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{}; +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; + while(!f()){check(std::chrono::steady_clock::now() 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 lock(mutex);incoming.clear(); +} +void inject(unsigned channel,bool fd,std::uint32_t id,std::initializer_list 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 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(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::max(); + std::unique_lock lock(mutex); + changed.wait_for(lock,10ms,[]{return !incoming.empty();}); + U32 count=0;while(!incoming.empty()&&counthdr.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{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"); + 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(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< #include #include @@ -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"); diff --git a/third_party/zlg/README.md b/third_party/zlg/README.md new file mode 100644 index 0000000..3336687 --- /dev/null +++ b/third_party/zlg/README.md @@ -0,0 +1,5 @@ +# USBCANFD SDK + +`zcan.h` and the runtime libraries in `orin/lib/` were copied unchanged from the user-specified reference project `/home/orin/vision-anti-pinch` on 2026-09-24. The runtime libraries are aarch64 `libusbcanfd.so` (SONAME `libusbcanfd.so.1.0.12`) and `libusb-1.0.so` (SONAME `libusb-1.0.so.0`). Symlinks retain these SONAMEs in the standalone package. + +`src/can_bus.cpp` uses the reference device type and 500 kbps / 2 Mbps initialization settings. It implements one merged receive worker, per-channel state, explicit data-frame transmission, and lifecycle cleanup instead of importing the reference global callback/receive-thread implementation. diff --git a/third_party/zlg/zcan.h b/third_party/zlg/zcan.h new file mode 100644 index 0000000..9f9d935 --- /dev/null +++ b/third_party/zlg/zcan.h @@ -0,0 +1,443 @@ +#pragma once + +#ifdef __cplusplus +#define EXTERN_C extern "C" +#else +#define EXTERN_C +#endif + +#define ZCAN_API +#define INVALID_HANDLE_VALUE ((HANDLE)(-1)) + +#define CMD_CAN_FILTER 0x14 // 滤波 +#define CMD_CAN_TTX 0x16 // 定时发送 +#define CMD_CAN_TTX_CTL 0x17 // 使能定时发送 +#define CMD_CAN_TRES 0x18 // CAN终端电阻 + +#define ZCAN_CMD_SET_CHNL_RECV_MERGE 0x32 // 设置合并接收 0:不合并接收;1:合并接收 +#define ZCAN_CMD_GET_CHNL_RECV_MERGE 0x33 // 获取是否开启合并接收 0:不合并接收;1:合并接收 + +#define CMD_SET_SN 0x42 // 获取SN号 +#define CMD_GET_SN 0x43 // 设置SN号 +#define CMD_CAN_TX_TIMEOUT 0x44 // 发送超时 + +#define ZCAN_CMD_GET_SEND_QUEUE_SIZE 0x100 // 获取队列大小,uint32_t +#define ZCAN_CMD_GET_SEND_QUEUE_SPACE 0x101 // 获取队列剩余空间, uint32_t +#define ZCAN_CMD_SET_SEND_QUEUE_CLR 0x102 // 清空发送队列,1:清空 +#define ZCAN_CMD_SET_SEND_QUEUE_EN 0x103 // 开启发送队列,1:使能 + +typedef unsigned char U8; +typedef unsigned short U16; +typedef unsigned int U32; +typedef unsigned long long U64; + +enum eZCANDataDEF +{ + //数据类型 + ZCAN_DT_ZCAN_CAN_DATA = 1, // CAN/CANFD数据 + ZCAN_DT_ZCAN_CANFD_DATA = 2, // CAN/CANFD数据 + ZCAN_DT_ZCAN_ERROR_DATA = 3, // CAN/CANFD错误数据 + ZCAN_DT_ZCAN_LIN_DATA = 4, // LIN数据 + ZCAN_DT_ZCAN_LIN_ERROR_DATA = 5, // LIN错误数据 +}; +#pragma pack(push, 1) + +typedef enum { + ZCAN_TX_NORM = 0, /**< normal transmission */ + ZCAN_TX_ONCE = 1, /**< single-shot transmission */ + ZCAN_SR_NORM = 2, /**< self reception */ + ZCAN_SR_ONCE = 3, /**< single-shot transmission & self reception */ +} ZCAN_TX_MODE; + +/** CAN filter configuration */ +typedef struct { + U8 type; /**< 0-std_frame, 1-ext_frame */ + U8 pad[3]; + U32 sid; /**< start-id */ + U32 eid; /**< end-id */ +} ZCAN_FILTER; + +typedef struct { + U32 size; + ZCAN_FILTER table[64]; +} ZCAN_FILTER_TABLE; + +/** controller initialization */ +typedef struct { + U32 clk; // 时钟频率 + U32 mode; /**< bit0-normal/listen_only, bit1-ISO/BOSCH */ + struct { + U8 tseg1; + U8 tseg2; + U8 sjw; + U8 smp; + U16 brp; + } aset; + struct { + U8 tseg1; + U8 tseg2; + U8 sjw; + U8 smp; + U16 brp; + } dset; +} ZCAN_INIT; + +/** CAN message info */ +typedef struct { + U32 txm : 4; /**< TX-mode, @see ZCAN_TX_MODE */ + U32 fmt : 4; /**< 0-CAN2.0, 1-CANFD */ + U32 sdf : 1; /**< 0-data_frame, 1-remote_frame */ + U32 sef : 1; /**< 0-std_frame, 1-ext_frame */ + U32 err : 1; /**< error flag */ + U32 brs : 1; /**< bit-rate switch */ + U32 est : 1; /**< error state */ + U32 tx : 1; /**< received valid, tx frame */ + U32 echo : 1; /**< tx valid, echo frame */ + U32 qsend_100us:1; /**< queue send delay unit, 1-100us, 0-ms */ + U32 qsend:1; /**< send valid, queue send frame */ + U32 pad : 15; +} ZCAN_MSG_INF; + +/** CAN message header */ +typedef struct { + U32 ts; /**< timestamp */ + U32 id; /**< CAN-ID */ + ZCAN_MSG_INF inf; /**< @see ZCAN_MSG_INF */ + U16 pad; /**< send queue delay */ + U8 chn; /**< channel */ + U8 len; /**< data length */ +} ZCAN_MSG_HDR; + +/** CAN2.0-frame */ +typedef struct { + ZCAN_MSG_HDR hdr; + U8 dat[8]; +} ZCAN_20_MSG; + +/** CANFD-frame */ +typedef struct { + ZCAN_MSG_HDR hdr; + U8 dat[64]; +} ZCAN_FD_MSG; + +// 错误帧 +typedef struct { + ZCAN_MSG_HDR hdr; + U8 dat[8]; +} ZCAN_ERR_MSG; + +// 定时发送结构体 +typedef struct { + U32 interval; // 定时发送周期,单位 100us + U16 repeat; // 发送次数,0等于循环发 + U8 index; // 定时发送列表的帧索引号,也就是第几条定时发送报文 + U8 flags; // 0-此帧禁用定时发送,1-此帧使能定时发送 + ZCAN_FD_MSG msg; // CANFD帧结构体 +} ZCAN_TTX; + +typedef struct { + U32 size; // 实际生效的数组的长度(用来控制实际生效多少条定时发送帧结构体) + ZCAN_TTX table[8]; // 最大设置8条 +} ZCAN_TTX_CFG; + +// 设备信息 +typedef struct { + U16 hwv; /**< hardware version */ + U16 fwv; /**< firmware version */ + U16 drv; /**< driver version */ + U16 api; /**< API version */ + U16 irq; /**< IRQ */ + U8 chn; /**< channels */ + U8 sn[20]; /**< serial number */ + U8 id[40]; /**< card id */ + U16 pad[4]; +} ZCAN_DEV_INF; + +/** controller status */ +typedef struct { + U8 IR; /**< not used(for backward compatibility) */ + U8 MOD; /**< not used */ + U8 SR; /**< not used */ + U8 ALC; /**< not used */ + U8 ECC; /**< not used */ + U8 EWL; /**< not used */ + U8 RXE; /**< RX errors */ + U8 TXE; /**< TX errors */ + U32 PAD; +} ZCAN_STAT; + +//lin begin +typedef struct _VCI_LIN_INIT_CONFIG +{ + U8 linMode; // 是否作为主机,0-从机,1-主机 + U8 chkSumMode; // 校验方式,1-经典校验 2-增强校验 3-自动(对应eZLINChkSumMode的模式) + U16 reserved; // 保留 + U32 linBaud; // 波特率,取值1000~20000 +}ZCAN_LIN_INIT_CONFIG, *PZCAN_LIN_INIT_CONFIG; + +typedef struct tagZCANLINData +{ + union{ + struct { + U8 ID : 6; // 帧ID + U8 Parity : 2; // 帧ID校验 + }unionVal; + U8 rawVal; // 受保护的ID原始值 + } PID; // 受保护的ID + struct + { + U64 timeStamp; // 时间戳,单位微秒(us) + U8 dataLen; // 数据长度 + U8 dir; // 传输方向,0-接收 1-发送 + U8 chkSum; // 数据校验, 部分设备不支持校验数据的获取 + U8 reserved[13]; // 保留 + U8 data[8]; // 数据 + }RxData; // 仅接收数据时有效 + U8 reserved[7]; // 保留 +}ZCANLINData; + +typedef struct tagZCANLINErrData +{ + U64 timeStamp; // 时间戳, 单位微秒(us) + union { + struct { + U8 ID : 6; // 帧ID + U8 Parity : 2; // 帧ID校验 + }unionVal; + U8 rawVal; // 受保护的ID原始值 + } PID; // 受保护的ID + U8 dataLen; + U8 data[8]; + union + { + struct + { + U16 errStage : 4; // 错误阶段 + U16 errReason : 4; // 错误原因 + U16 reserved : 8; // 保留 + }; + U16 unionErrData; + }errData; + U8 dir; // 传输方向 + U8 chkSum; // 数据校验, 部分设备不支持校验数据的获取 + U8 reserved[10]; // 保留 +}ZCANLINErrData; + +typedef struct _VCI_LIN_MSG{ + U8 chnl; // 数据通道 + U8 dataType; // 数据类型, 0-LIN数据 1-LIN错误数据 + union + { + ZCANLINData zcanLINData; // LIN数据 + ZCANLINErrData zcanLINErrData; // LIN错误数据 + U8 raw[46]; // RAW数据 + } data; // 实际数据, 联合体,有效成员根据 dataType 字段而定 +}ZCAN_LIN_MSG, *PZCAN_LIN_MSG; + +enum eZLINChkSumMode +{ + DEFAULT = 0, // 默认,启动时配置 + CLASSIC_CHKSUM, // 经典校验 + ENHANCE_CHKSUM, // 增强校验 + AUTOMATIC, // 自动,设备自动识别校验方式(仅ZCAN_SetLINSubscribe时有效) +}; +enum +{ + MAX_LIN_ID_COUNT = 63, + MIN_LIN_DLC = 1, + MAX_LIN_DLC = 8, + AUTO_LIN_DLC = 255, +}; +typedef struct _VCI_LIN_PUBLISH_CFG +{ + U8 ID; // 受保护的ID(ID取值范围为0-63) + U8 dataLen; // dataLen范围为1-8 + U8 data[8]; + U8 chkSumMode; // 校验方式,0-默认,启动时配置 1-经典校验 2-增强校验(对应eZLINChkSumMode的模式) + U8 reserved[5]; // 保留 +}ZCAN_LIN_PUBLISH_CFG, *PZCAN_LIN_PUBLISH_CFG; + +typedef struct _VCI_LIN_SUBSCIBE_CFG +{ + U8 ID; // 受保护的ID(ID取值范围为0-63) + U8 dataLen; // dataLen范围为1-8 当为255(0xff)则表示设备自动识别报文长度 + U8 chkSumMode; // 校验方式,0-默认,启动时配置 1-经典校验 2-增强校验 3-自动(对应eZLINChkSumMode的模式) + U8 reserved[5]; // 保留 +}ZCAN_LIN_SUBSCIBE_CFG, *PZCAN_LIN_SUBSCIBE_CFG; + +// 合并接收数据数据结构,支持CAN/CANFD/LIN/错误等不同类型数据 +typedef struct tagZCANDataObj +{ + U8 dataType; // 数据类型, 参考eZCANDataDEF中 数据类型 部分定义 + U8 chnl; // 数据通道 + union{ + struct{ + U16 reserved : 16; // 保留 + }unionVal; + U16 rawVal; + }flag; // 标志信息, 暂未使用 + U8 extraData[4]; // 额外数据, 暂未使用 + union + { + ZCAN_20_MSG zcanCANData; // CAN数据 + ZCAN_FD_MSG zcanCANFDData; // CANFD数据 + ZCAN_ERR_MSG zcanErrData; // 错误数据 + ZCANLINData zcanLINData; // LIN数据 + ZCANLINErrData zcanLINErrData; // LIN错误数据 + U8 raw[92]; // RAW数据 + } data; // 实际数据, 联合体,有效成员根据 dataType 字段而定 +}ZCANDataObj; + + +// UDS传输协议版本 +typedef U8 ZCAN_UDS_TRANS_VER; +#define ZCAN_UDS_TRANS_VER_0 0 // ISO15765-2(2004版本) +#define ZCAN_UDS_TRANS_VER_1 1 // ISO15765-2(2016版本) + +// 帧类型 +typedef U8 ZCAN_UDS_FRAME_TYPE; +#define ZCAN_UDS_FRAME_CAN 0 // CAN帧 +#define ZCAN_UDS_FRAME_CANFD 1 // CANFD帧 +#define ZCAN_UDS_FRAME_CANFD_BRS 2 // CANFD加速帧 + +// UDS填充模式 +#define FILL_MODE_SHORT 0 // 小于8字节填充至8字节,大于8字节时按DLC就近填充 +#define FILL_MODE_NONE 1 // 不填充 +#define FILL_MODE_MAX 2 // 填充至最大数据长度 (不建议) + +// CAN UDS请求数据 +typedef struct _ZCAN_UDS_REQUEST +{ + U32 req_id; // 请求事务ID,范围0~65535,本次请求的唯一标识 + U8 channel; // 设备通道索引 0~255 + ZCAN_UDS_FRAME_TYPE frame_type; // 帧类型 + U8 reserved0[2]; // 保留 + U32 src_addr; // 请求地址 + U32 dst_addr; // 响应地址 + U8 suppress_response; // 1:抑制响应 + U8 sid; // 请求服务id + U8 reserved1[6]; // 保留 + struct { + U32 timeout; // 响应超时时间(ms)。因PC定时器误差,建议设置不小于200ms + U32 enhanced_timeout; // 收到消极响应错误码为0x78后的超时时间(ms)。因PC定时器误差,建议设置不小于200ms + U8 check_any_negative_response:1; // 接收到非本次请求服务的消极响应时是否需要判定为响应错误 + U8 wait_if_suppress_response:1; // 抑制响应时是否需要等待消极响应,等待时长为响应超时时间 + U8 flag:6; // 保留 + U8 reserved0[7]; // 保留 + } session_param; // 会话层参数 + struct { + ZCAN_UDS_TRANS_VER version; // 传输协议版本, VERSION_0, VERSION_1 + U8 max_data_len; // 单帧最大数据长度, can:8, canfd:64 + U8 local_st_min; // 本程序发送流控时用,连续帧之间的最小间隔, 0x00-0x7F(0ms~127ms), 0xF1-0xF9(100us~900us) + U8 block_size; // 流控帧的块大小 + U8 fill_byte; // 无效字节的填充数据 + U8 ext_frame; // 0:标准帧 1:扩展帧 + U8 is_modify_ecu_st_min; // 是否忽略ECU返回流控的STmin,强制使用本程序设置的 remote_st_min + U8 remote_st_min; // 发送多帧时用, is_ignore_ecu_st_min = 1 时有效, 0x00-0x7F(0ms~127ms), 0xF1-0xF9(100us~900us) + U32 fc_timeout; // 接收流控超时时间(ms), 如发送首帧后需要等待回应流控帧 + U8 fill_mode; // 数据长度填充模式, FILL_MODE_SHORT, FILL_MODE_NONE, FILL_MODE_MAX + U8 reserved0[3]; // 保留 + } trans_param; // 传输层参数 + U8 *data; // 数据数组(不包含SID) + U32 data_len; // 数据数组的长度 + U32 reserved2; // 保留 +} ZCAN_UDS_REQUEST; + +// UDS错误码 +typedef U8 ZCAN_UDS_ERROR; +#define ZCAN_UDS_ERROR_OK 0 // 没错误 +#define ZCAN_UDS_ERROR_TIMEOUT 1 // 响应超时 +#define ZCAN_UDS_ERROR_TRANSPORT 2 // 发送数据失败 +#define ZCAN_UDS_ERROR_CANCEL 3 // 取消请求 +#define ZCAN_UDS_ERROR_SUPPRESS_RESPONSE 4 // 抑制响应 +#define ZCAN_UDS_ERROR_BUSY 5 // 忙碌中 +#define ZCAN_UDS_ERROR_REQ_PARAM 6 // 请求参数错误 +#define ZCAN_UDS_ERROR_OTHTER 100 + +typedef U8 ZCAN_UDS_RESPONSE_TYPE; +#define ZCAN_UDS_RT_NEGATIVE 0 // 消极响应 +#define ZCAN_UDS_RT_POSITIVE 1 // 积极响应 + +// UDS响应数据 +typedef struct _ZCAN_UDS_RESPONSE +{ + ZCAN_UDS_ERROR status; // 响应状态 + U8 reserved[6]; // 保留 + ZCAN_UDS_RESPONSE_TYPE type; // 响应类型 + union { + struct { + U8 sid; // 响应服务id + U32 data_len; // 数据长度(不包含SID), 数据存放在接口传入的dataBuf中 + } positive; + struct { + U8 neg_code; // 固定为0x7F + U8 sid; // 请求服务id + U8 error_code; // 错误码 + } negative; + U8 raw[8]; + }; +} ZCAN_UDS_RESPONSE; + +// UDS控制类型 +typedef U32 ZCAN_UDS_CTRL_CODE; +#define ZCAN_UDS_CTRL_STOP_REQ 0 // 停止UDS请求 + +// UDS控制请求 +typedef struct _ZCAN_UDS_CTRL_REQ +{ + U32 reqID; // 请求事务ID,指明要操作哪一条请求 + ZCAN_UDS_CTRL_CODE cmd; // 控制类型 + U8 reserved[8]; // 保留 +} ZCAN_UDS_CTRL_REQ; + +// UDS控制结果 +typedef U32 ZCAN_UDS_CTRL_RESULT; +#define ZCAN_UDS_CTRL_RESULT_OK 0 // 成功 +#define ZCAN_UDS_CTRL_RESULT_ERR 1 // 失败 + +// UDS控制响应数据 +typedef struct _ZCAN_UDS_CTRL_RESP +{ + ZCAN_UDS_CTRL_RESULT result; // 操作结果 + U8 reserved[8]; // 保留 +} ZCAN_UDS_CTRL_RESP; + +#pragma pack(pop) + +// +EXTERN_C U32 ZCAN_API VCI_OpenDevice(U32 Type, U32 Card, U32 Reserved); +EXTERN_C U32 ZCAN_API VCI_CloseDevice(U32 Type, U32 Card); +EXTERN_C U32 ZCAN_API VCI_InitCAN(U32 Type, U32 Card, U32 Port, ZCAN_INIT *pInit); +EXTERN_C U32 ZCAN_API VCI_ReadBoardInfo(U32 Type, U32 Card, ZCAN_DEV_INF *pInfo); +EXTERN_C U32 ZCAN_API VCI_ReadErrInfo(U32 Type, U32 Card, U32 Port, ZCAN_ERR_MSG *pErr); +EXTERN_C U32 ZCAN_API VCI_ReadCANStatus(U32 Type, U32 Card, U32 Port, ZCAN_STAT *pStat); +EXTERN_C U32 ZCAN_API VCI_GetReference(U32 Type, U32 Card, U32 Port, U32 Ref, void *pData); +EXTERN_C U32 ZCAN_API VCI_SetReference(U32 Type, U32 Card, U32 Port, U32 Ref, void *pData); +EXTERN_C U32 ZCAN_API VCI_GetReceiveNum(U32 Type, U32 Card, U32 Port); +EXTERN_C U32 ZCAN_API VCI_ClearBuffer(U32 Type, U32 Card, U32 Port); +EXTERN_C U32 ZCAN_API VCI_StartCAN(U32 Type, U32 Card, U32 Port); +EXTERN_C U32 ZCAN_API VCI_ResetCAN(U32 Type, U32 Card, U32 Port); +EXTERN_C U32 ZCAN_API VCI_Transmit(U32 Type, U32 Card, U32 Port, ZCAN_20_MSG *pData, U32 Count); +EXTERN_C U32 ZCAN_API VCI_TransmitFD(U32 Type, U32 Card, U32 Port, ZCAN_FD_MSG *pData, U32 Count); +EXTERN_C U32 ZCAN_API VCI_Receive(U32 Type, U32 Card, U32 Port, ZCAN_20_MSG *pData, U32 Count, U32 Time); +EXTERN_C U32 ZCAN_API VCI_ReceiveFD(U32 Type, U32 Card, U32 Port, ZCAN_FD_MSG *pData, U32 Count, U32 Time); +EXTERN_C U32 ZCAN_API VCI_Debug(U32 Debug); + +// LIN +EXTERN_C U32 VCI_InitLIN(U32 Type, U32 Card, U32 LinChn, PZCAN_LIN_INIT_CONFIG pLINInitConfig); +EXTERN_C U32 VCI_StartLIN(U32 Type, U32 Card, U32 LinChn); +EXTERN_C U32 VCI_ResetLIN(U32 Type, U32 Card, U32 LinChn); +EXTERN_C U32 VCI_TransmitLIN(U32 Type, U32 Card, U32 LinChn, PZCAN_LIN_MSG pSend, U32 Len); +EXTERN_C U32 VCI_GetLINReceiveNum(U32 Type, U32 Card, U32 LinChn); +EXTERN_C U32 VCI_ClearLINBuffer(U32 Type, U32 Card, U32 LinChn); +EXTERN_C U32 VCI_ReceiveLIN(U32 Type, U32 Card, U32 LinChn, PZCAN_LIN_MSG pReceive, U32 Len,int WaitTime); +EXTERN_C U32 VCI_SetLINSubscribe(U32 Type, U32 Card, U32 LinChn, PZCAN_LIN_SUBSCIBE_CFG pSend, U32 nSubscribeCount); +EXTERN_C U32 VCI_SetLINPublish(U32 Type, U32 Card, U32 LinChn, PZCAN_LIN_PUBLISH_CFG pSend, U32 nPublishCount); + +// 合并接收 +EXTERN_C U32 VCI_TransmitData(unsigned Type, unsigned Card, unsigned Port, ZCANDataObj *pData, unsigned Count); +EXTERN_C U32 VCI_ReceiveData(unsigned Type, unsigned Card, unsigned Port, ZCANDataObj *pData, unsigned Count, unsigned Time); + +// UDS +EXTERN_C U32 VCI_UDS_Request(unsigned Type, unsigned Card, const ZCAN_UDS_REQUEST *req, ZCAN_UDS_RESPONSE *resp, U8 *dataBuf, U32 dataBufSize); +EXTERN_C U32 VCI_UDS_Control(unsigned Type, unsigned Card, const ZCAN_UDS_CTRL_REQ *ctrl, ZCAN_UDS_CTRL_RESP *resp);