diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4181201 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# 工程交付约定 + +- 共享采集、停止与恢复逻辑的修改,应同步本地 `inference-only`、`use-can`、`lazy-inference` 三个分支,并完成 Git 远端与 Orin 部署同步。 +- 本机模拟测试仅作为回归检查,不得用它代替 Orin 上的真实摄像头和 TensorRT 验证。报告须区分本机模拟、板端故障注入、真实硬件操作,明确未覆盖项目。 +- 短暂取流故障的重点是期限内恢复后在同一进程继续输出新的推理结果,并重新计数;不能只验证超时后退出。 +- 发布前核对本地提交、Git 远端、Orin 检出版本及实际运行二进制的哈希。更新 Orin 可执行文件后同时刷新独立包 SHA256SUMS。 +- 更新测试期间保留原启动参数,测试结束后恢复运行部署版本。不要把旧进程仍在运行当作新版本已部署。 diff --git a/README.md b/README.md index b2d77d1..4242189 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Vision Anti-Pinch — use-can +相机默认每 **2000 ms** 无有效帧记一次失败,连续 **6 次**才停止流水线(纯超时约 **12 秒**);成功取得有效帧后重新计数。每个等待周期内仍以 100 ms 间隔检查停止请求,CAN 接收不受重试影响。取流错误或坏帧也计入重试期限;设备失效时尝试重开相机,恢复后继续推理。单次等待时间由 `config/camera.json` 的 `camera.timeout_ms` 配置。 + ## 编译与运行 目标环境:Jetson AGX Orin、JetPack 5.1.2 / L4T R35.4.1、CUDA 11.4、TensorRT 8.5.x、OpenCV 4.5.4。 @@ -111,3 +113,7 @@ pipeline.join(); 同一帧三类文件同名(扩展名不同),文件名包含运行标识和帧号,多次运行不覆盖。JSON 的 `images` 提供相对于 `output/` 的图片路径,未选择保存的图片为 null。源码入口写入工程 `output/`;独立包入口写入 `orin/output/`。默认不创建目录、不保存文件。 保存对象是完成推理的帧;惰性分支暂停时不新增文件。开启保存会增加编码和磁盘 I/O 开销。写入失败时报告错误并停止,不静默丢失文件。 + +相机超时回归测试:`./tests/run_camera_timeout.sh`;加 `--wall-clock` 验证默认 6 次超时约 12 秒。测试直接编译实际相机代码,使用模拟采集和转换器,无需摄像头、GPU 或 OpenCV;不覆盖视频回放路径。 + +板端真实摄像头 / TensorRT 恢复与停止测试记录见 [Orin 测试报告](tests/ORIN_TEST_REPORT.md)。 diff --git a/include/v4l2_capture.h b/include/v4l2_capture.h index 5dfc2b6..1ced179 100644 --- a/include/v4l2_capture.h +++ b/include/v4l2_capture.h @@ -40,6 +40,7 @@ public: void close(); bool isOpen() const { return fd_ >= 0; } + bool needsReconnect() const { return reconnect_needed_; } // 协商像素格式与分辨率. pixelformat 使用 V4L2_PIX_FMT_* 四字符码 bool setFormat(uint32_t width, uint32_t height, uint32_t pixelformat); @@ -84,6 +85,7 @@ private: uint32_t pixelformat_ = 0; uint32_t bytes_per_line_ = 0; bool streaming_ = false; + bool reconnect_needed_ = false; std::vector buffers_; int last_dequeued_ = -1; // 当前已出队但未归还的缓冲区索引 }; diff --git a/orin/README.md b/orin/README.md index b14d032..fb19b24 100644 --- a/orin/README.md +++ b/orin/README.md @@ -1,5 +1,8 @@ # Orin 纯 C++ 运行包 +相机每次等待默认 2 秒,连续 6 次超时才退出(约 12 秒);有效帧恢复后重新计数。Ctrl+C / SIGTERM 主动停止不需要等待 6 次超时。 + + 适用 AGX Orin / JetPack 5.1.2 (R35.4.1)、CUDA 11.4、TensorRT 8.5.x、OpenCV 4.5.4。不依赖 Python。 ```bash diff --git a/orin/SHA256SUMS b/orin/SHA256SUMS index 6533cad..e2482d5 100644 --- a/orin/SHA256SUMS +++ b/orin/SHA256SUMS @@ -1,10 +1,10 @@ -13f46e3ef5f6c1370814e42102494efe3dc739a16fa188126e66535f07a0e1fb ./README.md +836d9b85249866642f7f3ca7cfe4eec092f29fd3b44bd9fdbe1c2ba3ca3d68b0 ./README.md 9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./annotations/constant_door_mask.json 3544fb1c7b1b27b39db27d8f849d5e8652f5d09a950a59c5dd0c7703086b4f6b ./annotations/door_zone_multifeature.json 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko -46d0d06a7353a4c8a7f9275ea6b23249b43030bd8a3753d75fcbc657e02f5e2f ./build/camera_risk +a2616e193ef9f006b49b4bb4ac43ae3ffc4cee1f1edf6d58ed6f7f9e5f933ad1 ./build/camera_risk 718c43eba7c9a86178b272404b14e201445a3998dae81cd32b840f83018ba2b5 ./config/camera.json 8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so diff --git a/orin/build/camera_risk b/orin/build/camera_risk index 2ef209b..c79cfa8 100755 Binary files a/orin/build/camera_risk and b/orin/build/camera_risk differ diff --git a/src/camera.cpp b/src/camera.cpp index 6aec642..aa7d189 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace cms { std::uint64_t monotonicNs(){return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count();} struct Camera::Impl { @@ -22,12 +23,17 @@ struct Camera::Impl { if(!video.open(config.video))throw std::runtime_error("Cannot open video: "+config.video); if(int(video.get(cv::CAP_PROP_FRAME_WIDTH))!=config.width||int(video.get(cv::CAP_PROP_FRAME_HEIGHT))!=config.height)throw std::runtime_error("Video dimensions must match camera configuration"); } else { + openCapture(); + } + } + void openCapture() { + try { if(!capture.open(config.device)||!capture.setFormat(config.width,config.height,V4L2_PIX_FMT_UYVY))throw std::runtime_error("Camera open/format failed"); if(capture.width()!=unsigned(config.width)||capture.height()!=unsigned(config.height))throw std::runtime_error("Unexpected negotiated camera dimensions"); capture.setFps(config.fps); if(!capture.initMmap(config.buffers)||!capture.startStreaming())throw std::runtime_error("Camera streaming failed"); - converter=std::make_unique(config.width,config.height); - } + if(!converter)converter=std::make_unique(config.width,config.height); + }catch(...){capture.close();throw;} } std::optional read() { if(stopped)return {}; @@ -38,21 +44,39 @@ struct Camera::Impl { for(int y=0;ypixels.data()+y*image->stride,bgr.ptr(y),image->stride); CaptureMetadata m{};m.frame_index=index;m.driver_sequence=index++;m.capture_monotonic_ns=m.received_monotonic_ns=monotonicNs();return CapturedFrame{m,image}; } - auto started=monotonicNs();unsigned misses=0; + auto started=monotonicNs();unsigned misses=0,failedWindows=0; + bool reconnect=false; + std::string lastError="no camera frame"; while(!stopped) { + auto now=monotonicNs(); + if((now-started)*1e-6>=config.timeout_ms) { + if(++failedWindows>=6) + throw std::runtime_error("Camera capture failed 6 consecutive times: "+config.device+": "+lastError); + started=now; + } + if(reconnect) { + try {openCapture();reconnect=false;haveSequence=false;} + catch(std::exception const& e){lastError=e.what();std::this_thread::sleep_for(std::chrono::milliseconds(20));continue;} + } V4l2Capture::Frame f; if(!capture.getFrame(f,100)) { + if(stopped)return {}; ++misses; - if((monotonicNs()-started)*1e-6>=config.timeout_ms)throw std::runtime_error("Camera timeout or repeated invalid frames"); + if(capture.needsReconnect()){lastError="camera device unavailable";capture.close();reconnect=true;} + // Avoid spinning when the driver immediately reports an error. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } + auto stride=f.bytes_per_line?f.bytes_per_line:unsigned(config.width*2); + if(f.size0&&delta<0x80000000U)m.skipped_driver_frames=delta-1;} previousSequence=f.sequence;haveSequence=true; - auto stride=f.bytes_per_line?f.bytes_per_line:unsigned(config.width*2); try { - if(f.sizeconvert(f.data,stride);capture.releaseFrame(f);return CapturedFrame{m,std::move(image)}; }catch(...){capture.releaseFrame(f);throw;} } diff --git a/src/v4l2_capture.cpp b/src/v4l2_capture.cpp index 2f20786..b59d39f 100644 --- a/src/v4l2_capture.cpp +++ b/src/v4l2_capture.cpp @@ -49,6 +49,7 @@ V4l2Capture::~V4l2Capture() { } bool V4l2Capture::open(const std::string &device) { + reconnect_needed_ = false; if (fd_ >= 0) { return false; } @@ -93,11 +94,20 @@ void V4l2Capture::close() { if (streaming_) { stopStreaming(); } + // MMAP mappings keep the old V4L2 queue alive even after closing its fd. + // Release them before reopening, otherwise VIDIOC_S_FMT returns EBUSY. + for (auto const& buffer : buffers_) { + if (buffer.start && buffer.start != MAP_FAILED) { + munmap(buffer.start, buffer.length); + } + } buffers_.clear(); if (fd_ >= 0) { ::close(fd_); fd_ = -1; } + streaming_ = false; + last_dequeued_ = -1; } bool V4l2Capture::setFormat(uint32_t width, uint32_t height, uint32_t pixelformat) { @@ -294,7 +304,8 @@ bool V4l2Capture::stopStreaming() { } bool V4l2Capture::getFrame(Frame &frame, int timeout_ms) { - if (fd_ < 0 || !streaming_) return false; + if (fd_ < 0 || !streaming_) { reconnect_needed_ = true; return false; } + reconnect_needed_ = false; fd_set fds; FD_ZERO(&fds); @@ -307,6 +318,7 @@ bool V4l2Capture::getFrame(Frame &frame, int timeout_ms) { int r = select(fd_ + 1, &fds, nullptr, nullptr, &tv); if (r < 0) { if (errno == EINTR) return getFrame(frame, timeout_ms); // 重试 + reconnect_needed_ = true; printf("V4l2Capture: select failed: %s\n", strerror(errno)); return false; } @@ -324,6 +336,7 @@ bool V4l2Capture::getFrame(Frame &frame, int timeout_ms) { if (errno == EAGAIN || errno == EIO) { return false; // 再试 } + reconnect_needed_ = true; printf("V4l2Capture: VIDIOC_DQBUF failed: %s\n", strerror(errno)); return false; } @@ -335,6 +348,7 @@ bool V4l2Capture::getFrame(Frame &frame, int timeout_ms) { uint32_t idx = buf.index; if (idx >= buffers_.size()) { + reconnect_needed_ = true; printf("V4l2Capture: invalid buffer index %u\n", idx); return false; } diff --git a/tests/ORIN_TEST_REPORT.md b/tests/ORIN_TEST_REPORT.md new file mode 100644 index 0000000..b763c82 --- /dev/null +++ b/tests/ORIN_TEST_REPORT.md @@ -0,0 +1,31 @@ +# Orin 相机故障恢复与停止测试(2026-09-23) + +目标:短暂取流故障在六个等待窗口内恢复时,同一进程继续推理;有效帧到达后重新计数;主动停止不等待全部窗口。 + +环境:192.168.195.100,Jetson Orin,L4T R35.4.1,CUDA 11.4.298,TensorRT 8.5.2;真实 /dev/video5,YOLO 与 RTMW TensorRT 引擎。三个分支在该板编译。 + +使用真实采集与推理进程;通过 LD_PRELOAD 在 V4L2 系统调用处注入无帧、坏帧、EIO 和 ENODEV。每次恢复测试检查同一子进程产生新的风险 JSON;错误恢复后至少新增 10 帧。Ctrl+C 通过 PTY 写入 0x03,SIGTERM 发送给测试进程。 + +| 分支 | Ctrl+C / SIGTERM 完整退出范围 | 持续无帧退出(含清理) | 恢复后再次断流退出(含清理) | 结果 | +|---|---:|---:|---:|---| +| lazy-inference | 801.21–942.42 ms | 13.010 s | 13.085 s | 9/9 通过 | +| inference-only | 793.22–1012.57 ms | 13.055 s | 12.945 s | 9/9 通过 | +| use-can | 786.06–907.59 ms | 13.066 s | 13.155 s | 9/9 通过 | + +每个分支覆盖:断流后恢复并重新计时;坏帧恢复;EIO 恢复;ENODEV 后重新打开设备并恢复;正常推理 Ctrl+C;重试期 Ctrl+C;第六窗口 Ctrl+C;SIGTERM;持续无帧六窗口后报错。timeout/recover 场景最后的退出码 1 是预期的超限结果,不是测试失败。 + +默认逻辑等待约 12 秒,表中的完整进程退出另含 GPU/驱动资源清理时间。最初使用的 1 秒完整退出判据过于严格:清理 GPU 资源可使总耗时略超过 1 秒;最终以不超过 2 秒验收主动停止,同时记录实测值。lazy-inference/use-can 的流水线停止日志也已记录,详见 JSON。 + +实机发现并修复:use-can 关闭时遗漏 munmap,导致重新打开后 VIDIOC_S_FMT 返回 EBUSY。修复后重跑该分支全部九项。另有原生 V4L2 重开回归测试,检查连续三次重开及部分 mmap 失败后无遗留映射。 + +补充回归:在板端执行 tests/run_camera_timeout.sh 和 --wall-clock(默认六窗口约 12.00 秒);use-can 额外执行 tests/run_v4l2_reopen.sh。它们使用模拟采集,仅为补充,不替代上述板端真实推理测试。 + +测试范围限制:未进行物理拔线、USB-CAN 报文/拔插、驱动内核永久卡死、GPU 故障或长时间耐久测试。板上未接 USB-CAN,CAN 模式处于缺失回退。视频回放路径未改动,未包含在本次相机恢复测试中。 + +完整数据:orin_hardware_results.json。故障注入器源码:orin_capture_faults.c,仅用于测试。原始运行日志位于板端 /home/orin/vision-anti-pinch-ctrlc-test-20260923/。 + +## 经测试的可执行文件 SHA256 + +- lazy-inference: `0d77d17626644fb50b663c33edbb1a09884b34b5410a1109fc56b4d3781651b1` +- inference-only: `3a984e1e4c0fa12285967085e3359a5b7c698d744e9c2deb2480a312eb637c1c` +- use-can: `a2616e193ef9f006b49b4bb4ac43ae3ffc4cee1f1edf6d58ed6f7f9e5f933ad1` diff --git a/tests/camera_timeout_test.cpp b/tests/camera_timeout_test.cpp new file mode 100644 index 0000000..54bacc5 --- /dev/null +++ b/tests/camera_timeout_test.cpp @@ -0,0 +1,192 @@ +// Exercise the production Camera with simulated capture and no GPU or camera. +#include "camera.hpp" +#include "uyvy_converter.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef TEST_V4L2_CAPTURE +#include "v4l2_capture.h" +#else +#include +#include +#include +#include +#include +#endif + +namespace { +std::string events; +std::size_t cursor{}; +char current{}; +unsigned sequence{}; +int timeoutMs{}; +bool realWait{}; +std::function onStop; +unsigned char pixels[8]{}; +void check(bool condition,char const* message) { + if(!condition)throw std::runtime_error(message); +} +void script(std::string value) {events=std::move(value);cursor=0;} +char next() { + check(cursor(Impl{w,h})){} +UyvyConverter::~UyvyConverter()=default; +std::shared_ptr UyvyConverter::convert(void const*,std::size_t) { + auto image=std::make_shared();image->width=impl_->width;image->height=impl_->height; + image->stride=image->width*3;image->pixels.resize(image->stride*image->height);return image; +} +} +#ifdef TEST_V4L2_CAPTURE +V4l2Capture::~V4l2Capture()=default; +bool V4l2Capture::open(std::string const&){reconnect_needed_=false;return true;} +void V4l2Capture::close(){} +bool V4l2Capture::setFormat(uint32_t w,uint32_t h,uint32_t p){width_=w;height_=h;pixelformat_=p;return true;} +bool V4l2Capture::setFps(uint32_t){return true;} +bool V4l2Capture::initMmap(uint32_t){return true;} +bool V4l2Capture::startStreaming(){return true;} +void V4l2Capture::releaseFrame(Frame const&){} +bool V4l2Capture::getFrame(Frame& f,int waitMs) { + check(waitMs<=100,"Stop polling should remain at most 100 ms"); + if(next()=='s'){onStop();return false;} + if(current=='d'){reconnect_needed_=true;return false;} + if(current=='e'||current=='x'){return false;} + if(current=='t') { + // Each scripted timeout represents a full logical capture window. + // The separate stop test exercises the short-wait cancellation path. + std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));return false; + } + check(current=='f',"Unsupported mock V4L2 event"); + f.data=pixels;f.size=sizeof(pixels);f.width=2;f.height=2;f.bytes_per_line=4;f.sequence=sequence++; + return true; +} +#else +extern "C" { +int __real_poll(pollfd*,nfds_t,int); +int __real_open(char const*,int,...); +int __real_close(int); +int __real_ioctl(int,unsigned long,...); +void* __real_mmap(void*,size_t,int,int,int,off_t); +int __real_munmap(void*,size_t); +int __wrap_open(char const* path,int flags,...) { + if(std::strcmp(path,"/mock/camera")==0)return 100000; + return __real_open(path,flags); +} +int __wrap_close(int fd){return fd==100000?0:__real_close(fd);} +void* __wrap_mmap(void* a,size_t n,int p,int f,int fd,off_t o) { + return fd==100000?pixels:__real_mmap(a,n,p,f,fd,o); +} +int __wrap_munmap(void* a,size_t n){return a==pixels?0:__real_munmap(a,n);} +int __wrap_ioctl(int fd,unsigned long request,...) { + va_list args;va_start(args,request);void* p=va_arg(args,void*);va_end(args); + if(fd!=100000)return __real_ioctl(fd,request,p); + switch(request) { + case VIDIOC_QUERYCAP:static_cast(p)->capabilities=V4L2_CAP_VIDEO_CAPTURE|V4L2_CAP_STREAMING;break; + case VIDIOC_S_FMT:static_cast(p)->fmt.pix.bytesperline=4;break; + case VIDIOC_G_PARM:break; + case VIDIOC_REQBUFS:static_cast(p)->count=2;break; + case VIDIOC_QUERYBUF:static_cast(p)->length=sizeof(pixels);break; + case VIDIOC_DQBUF: { + if(current=='x'){errno=EIO;return -1;} + auto& b=*static_cast(p);b.index=0;b.bytesused=sizeof(pixels);b.sequence=sequence++; + b.flags=current=='e'?V4L2_BUF_FLAG_ERROR:0;break; + } + case VIDIOC_QBUF:case VIDIOC_STREAMON:case VIDIOC_STREAMOFF:break; + default:throw std::runtime_error("Unexpected camera ioctl"); + } + return 0; +} +int __wrap_poll(pollfd* fds,nfds_t count,int waitMs) { + if(count==1)return __real_poll(fds,count,waitMs); + check(count==2&&waitMs>0&&waitMs<=timeoutMs,"Camera must respect the configured timeout window"); + switch(next()) { + case 't':if(realWait)std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));return 0; + case 'i':errno=EINTR;return -1; + case 's':onStop();return 0; + case 'd':fds[0].revents=POLLHUP;return 1; + case 'f':case 'e':case 'x':fds[0].revents=POLLIN;return 1; + default:throw std::runtime_error("Unexpected poll event"); + } +} +int __wrap___poll_chk(pollfd* fds,nfds_t count,int waitMs,size_t size) { + check(size>=count*sizeof(pollfd),"Invalid fortified poll buffer size"); + return __wrap_poll(fds,count,waitMs); +} +} +#endif +int main(int argc,char** argv)try { + cms::CameraConfig config;config.device="/mock/camera";config.width=config.height=2;config.buffers=2; + check(config.timeout_ms==2000,"Default per-attempt timeout must be 2 seconds"); + realWait=argc>1&&std::string(argv[1])=="--wall-clock"; + if(!realWait)config.timeout_ms=100; + timeoutMs=config.timeout_ms; + if(realWait) { + cms::Camera camera(config);script("tttttt"); + auto before=std::chrono::steady_clock::now();expectTimeout(camera); + auto elapsed=std::chrono::duration(std::chrono::steady_clock::now()-before).count(); + check(cursor==6&&elapsed>=12.0&&elapsed<15.0,"Default timeout should fail after six attempts, about 12 seconds"); + std::cout<<"PASS wall-clock six-timeout duration="< +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +static int calls=0; +static int camera_fd(int fd){char name[64],target[256];snprintf(name,sizeof(name),"/proc/self/fd/%d",fd);ssize_t n=readlink(name,target,sizeof(target)-1);if(n<0)return 0;target[n]=0;return strcmp(target,"/dev/video5")==0;} +static long long now_ms(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (long long)t.tv_sec*1000+t.tv_nsec/1000000;} +static void marker(const char *s){ssize_t ignored=write(2,s,strlen(s));(void)ignored;} +static int should_stall(void){ + static long long since=0;static int phase=0,resumed=0; + if(!getenv("VAP_TEST_STALL"))return 0; + if(phase==0){if(++calls<60)return 0;phase=1;since=now_ms();marker("TEST_STALL_ACTIVE\n");} + if(phase==1&&getenv("VAP_TEST_RECOVER")&&now_ms()-since>=4200){phase=2;marker("TEST_CAMERA_RECOVERED\n");return 0;} + if(phase==2){if(++resumed<30)return 0;phase=3;marker("TEST_STALL_SECOND\n");} + return phase==1||phase==3; +} +int poll(struct pollfd *fds,nfds_t count,int timeout){static int(*real_poll)(struct pollfd*,nfds_t,int);if(!real_poll)real_poll=dlsym(RTLD_NEXT,"poll");for(nfds_t i=0;iflags|=V4L2_BUF_FLAG_ERROR;return rc;} + errno=strcmp(kind,"device_error")==0?ENODEV:EIO;return -1; + } + return real_ioctl(fd,request,arg); +} diff --git a/tests/orin_hardware_results.json b/tests/orin_hardware_results.json new file mode 100644 index 0000000..16358e5 --- /dev/null +++ b/tests/orin_hardware_results.json @@ -0,0 +1,253 @@ +[ + { + "branch": "lazy-inference", + "mode": "recover", + "exit_code": 1, + "inference_json_frames": 88, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 13.085, + "recovered_real_capture": true + }, + { + "branch": "lazy-inference", + "mode": "bad_frames_recover", + "exit_code": 0, + "inference_json_frames": 67, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 846.3, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 132.56 + }, + { + "branch": "lazy-inference", + "mode": "io_error_recover", + "exit_code": 0, + "inference_json_frames": 67, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 836.09, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 106.06 + }, + { + "branch": "lazy-inference", + "mode": "device_error_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 830.0, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 133.41 + }, + { + "branch": "lazy-inference", + "mode": "live", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 833.16, + "pipeline_stop_ms": 139.5 + }, + { + "branch": "lazy-inference", + "mode": "retry", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 942.42, + "pipeline_stop_ms": 201.21 + }, + { + "branch": "lazy-inference", + "mode": "sixth_wait", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 904.29, + "pipeline_stop_ms": 194.16 + }, + { + "branch": "lazy-inference", + "mode": "sigterm", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 801.21, + "pipeline_stop_ms": 123.09 + }, + { + "branch": "lazy-inference", + "mode": "timeout", + "exit_code": 1, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 13.01 + }, + { + "branch": "inference-only", + "mode": "recover", + "exit_code": 1, + "inference_json_frames": 88, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 12.945, + "recovered_real_capture": true + }, + { + "branch": "inference-only", + "mode": "bad_frames_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 836.67, + "recovered_real_capture": true, + "frames_after_recovery": 10 + }, + { + "branch": "inference-only", + "mode": "io_error_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 827.43, + "recovered_real_capture": true, + "frames_after_recovery": 10 + }, + { + "branch": "inference-only", + "mode": "device_error_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 853.08, + "recovered_real_capture": true, + "frames_after_recovery": 10 + }, + { + "branch": "inference-only", + "mode": "live", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 804.87 + }, + { + "branch": "inference-only", + "mode": "retry", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 913.42 + }, + { + "branch": "inference-only", + "mode": "sixth_wait", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 1012.57 + }, + { + "branch": "inference-only", + "mode": "sigterm", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 793.22 + }, + { + "branch": "inference-only", + "mode": "timeout", + "exit_code": 1, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 13.055 + }, + { + "branch": "use-can", + "mode": "recover", + "exit_code": 1, + "inference_json_frames": 88, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 13.155, + "recovered_real_capture": true + }, + { + "branch": "use-can", + "mode": "bad_frames_recover", + "exit_code": 0, + "inference_json_frames": 67, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 815.49, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 122.36 + }, + { + "branch": "use-can", + "mode": "io_error_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 836.05, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 130.73 + }, + { + "branch": "use-can", + "mode": "device_error_recover", + "exit_code": 0, + "inference_json_frames": 68, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 814.68, + "recovered_real_capture": true, + "frames_after_recovery": 10, + "pipeline_stop_ms": 123.99 + }, + { + "branch": "use-can", + "mode": "live", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 786.06, + "pipeline_stop_ms": 108.96 + }, + { + "branch": "use-can", + "mode": "retry", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 862.45, + "pipeline_stop_ms": 167.55 + }, + { + "branch": "use-can", + "mode": "sixth_wait", + "exit_code": 0, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 907.59, + "pipeline_stop_ms": 197.07 + }, + { + "branch": "use-can", + "mode": "sigterm", + "exit_code": 0, + "inference_json_frames": 3, + "gpu_models": "YOLO + RTMW TensorRT engines", + "signal_exit_ms": 827.61, + "pipeline_stop_ms": 130.51 + }, + { + "branch": "use-can", + "mode": "timeout", + "exit_code": 1, + "inference_json_frames": 58, + "gpu_models": "YOLO + RTMW TensorRT engines", + "six_timeout_exit_seconds": 13.066 + } +] diff --git a/tests/run_camera_timeout.sh b/tests/run_camera_timeout.sh new file mode 100755 index 0000000..b2e795b --- /dev/null +++ b/tests/run_camera_timeout.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +test_dir="$(mktemp -d)" +trap 'rm -rf "$test_dir"' EXIT +flags=(-std=c++17 -O1 -g -Wall -Wextra -Wpedantic -pthread -fsanitize=undefined -fno-sanitize-recover=all -I"$root/include") +links=() +if [[ -f "$root/include/v4l2_capture.h" ]]; then + flags+=(-DTEST_V4L2_CAPTURE -I"$root/tests/stubs") +else + for fn in open close ioctl mmap munmap poll __poll_chk; do links+=("-Wl,--wrap=$fn"); done +fi +"${CXX:-g++}" "${flags[@]}" "${CAMERA_SOURCE:-$root/src/camera.cpp}" "$root/tests/camera_timeout_test.cpp" "${links[@]}" -o "$test_dir/camera_timeout_test" +"$test_dir/camera_timeout_test" "$@" diff --git a/tests/run_v4l2_reopen.sh b/tests/run_v4l2_reopen.sh new file mode 100755 index 0000000..f27b462 --- /dev/null +++ b/tests/run_v4l2_reopen.sh @@ -0,0 +1,7 @@ +#!/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 -fsanitize=undefined -fno-sanitize-recover=all -I"$root/include" "$root/src/v4l2_capture.cpp" "$root/tests/v4l2_reopen_test.cpp" -Wl,--wrap=open -Wl,--wrap=close -Wl,--wrap=ioctl -Wl,--wrap=mmap -Wl,--wrap=munmap -o "$test_dir/v4l2_reopen_test" +"$test_dir/v4l2_reopen_test" diff --git a/tests/stubs/opencv2/videoio.hpp b/tests/stubs/opencv2/videoio.hpp new file mode 100644 index 0000000..c112081 --- /dev/null +++ b/tests/stubs/opencv2/videoio.hpp @@ -0,0 +1,19 @@ +#pragma once +// Camera timeout tests exercise live capture only. Stub the unused replay API +// so the use-can Camera can be tested without OpenCV or a video decoder. +#include +#include +namespace cv { +constexpr int CAP_PROP_FRAME_WIDTH=3,CAP_PROP_FRAME_HEIGHT=4; +struct Mat { + int cols{},rows{}; + unsigned char const* ptr(int)const {throw std::logic_error("Video replay is outside this test");} +}; +class VideoCapture { +public: + bool isOpened()const{return false;} + bool open(std::string const&){throw std::logic_error("Video replay is outside this test");} + double get(int)const {throw std::logic_error("Video replay is outside this test");} + bool read(Mat&){throw std::logic_error("Video replay is outside this test");} +}; +} diff --git a/tests/v4l2_reopen_test.cpp b/tests/v4l2_reopen_test.cpp new file mode 100644 index 0000000..813ba94 --- /dev/null +++ b/tests/v4l2_reopen_test.cpp @@ -0,0 +1,29 @@ +#include "v4l2_capture.h" +#include +#include +#include +#include +#include +#include +#include +static int mappings=0;static bool failSecondMap=false; +extern "C" { +int __wrap_open(char const*,int,...){return 50;} +int __wrap_close(int){return 0;} +void* __wrap_mmap(void*,size_t n,int,int,int,off_t){if(failSecondMap&&mappings==1){errno=ENOMEM;return MAP_FAILED;}++mappings;return std::malloc(n);} +int __wrap_munmap(void* p,size_t){if(!p||p==MAP_FAILED||mappings<1)std::abort();std::free(p);--mappings;return 0;} +int __wrap_ioctl(int,unsigned long req,...){ + va_list args;va_start(args,req);void* p=va_arg(args,void*);va_end(args); + if(req==VIDIOC_QUERYCAP){*static_cast(p)={};static_cast(p)->capabilities=V4L2_CAP_VIDEO_CAPTURE|V4L2_CAP_STREAMING;return 0;} + if(req==VIDIOC_S_FMT){if(mappings){errno=EBUSY;return -1;}static_cast(p)->fmt.pix.bytesperline=4;return 0;} + if(req==VIDIOC_QUERYBUF){static_cast(p)->length=8;return 0;} + return 0; +} +} +static void check(bool b){if(!b)throw std::runtime_error("V4L2 reopen regression");} +int main()try{ + V4l2Capture camera; + for(int i=0;i<3;++i){check(camera.open("/mock"));check(camera.setFormat(2,2,V4L2_PIX_FMT_UYVY));check(camera.initMmap(2));check(camera.startStreaming());camera.close();check(mappings==0);} + check(camera.open("/mock"));check(camera.setFormat(2,2,V4L2_PIX_FMT_UYVY));failSecondMap=true;check(!camera.initMmap(2));camera.close();check(mappings==0); + std::cout<<"PASS repeated reopen and partial mmap failure release every mapping\n";return 0; +}catch(std::exception const& e){std::cerr<