diff --git a/docs/known-issues.md b/docs/known-issues.md index 9160fdc..592b020 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -173,9 +173,10 @@ scripts for this are worth keeping; note that wrapping them in `timeout` SIGTERM were the camera's own NUC every 30 s (a 0.7 s pause in its stream); since 2026-10-15 the program runs the NUC at the start of a move instead. A NIR unit would be a third driver behind the same key. -- The InfraTec SDK costs about a quarter of one core on the 3 Delta even between pictures: three threads - it starts in `dll_init` and one per connection poll the raw socket in `nanosleep` loops (measured on - tower 111, 2026-10-14). The frame rate does not change it. In the first minutes after deploying, the +- The InfraTec SDK keeps the CPU awake: about 15 of its threads sleep 1 ms at a time, some 15 000 wake-ups + a second, so the cores never settle and the CPU clocks them at turbo (2.8 GHz on the N5105) whenever they + run. Until 2026-10-15 the camera also sent 50 fps (31 MB/s) because the frame rate was not taken; at the + intended 5 fps the system load fell from about 29 % to 18 %, but the wake-ups remain. In the first minutes after deploying, the CPU guard slowed the scan once (86 °C, back to normal after 2 min). Watch Health `cpu_c` over a warm day before a tower without air flow gets a thermal camera. - Tower 111's colour profile does not apply: every start since 2026-10-07 logs `applying profile 'standard' diff --git a/docs/tower-installation.md b/docs/tower-installation.md index bc51c5b..5f48024 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -950,8 +950,14 @@ Measured on tower 111 (2026-10-14, SDK 4.3.44, SN 2017744): - A profile switch takes about 3 s; frames keep the old scale until `Calib_ChangeInProgress` reaches 0, and the program takes none meanwhile. - The camera's frame counter (`ImageNum`) is always 0; `MissedPackets` was - always 0 on the direct link. It delivers about 50 fps whatever `Framerate_Hz` - says; the program polls it at 10 Hz. + always 0 on the direct link. +- Frame rate (2026-10-15): from power-up the camera sends 50 fps, 31 MB/s and + 22 000 packets a second. `Framerate_Hz` set right after connecting answers ok + and changes nothing; set while grabbing it only thins out what the SDK hands + over; set while the grab is stopped, after the stream has run, it holds on the + wire. The program does that once the first frame is in and checks the packet + rate of `[Camera.tir] interface` (5 Hz: about 2 200 packets a second); the log + says `5.3 fps on enp1s0 (2224 packets/s), set on attempt 2`. - NUC (2026-10-15): the camera runs one every 30 s (`NUC_Interval`) and sends no frame for 650-760 ms meanwhile. `NUC_Interval = 0` stops that; `NUC_Perform` (1-point) runs one and returns when it is done, after about 0.67 s. diff --git a/src/camera/TarisCameraSource.cpp b/src/camera/TarisCameraSource.cpp index fd9c1c7..2f2511e 100644 --- a/src/camera/TarisCameraSource.cpp +++ b/src/camera/TarisCameraSource.cpp @@ -138,6 +138,14 @@ struct TarisCameraSource::Impl { double last_cam_ts = -1.0; // its MemInfo time, ms long long polls_since_frame = 0; std::map codes_since_frame; // getdata answers other than a new frame + // The frame rate (see setFrameRate()): set by restarting the grab once the + // first frame is in, then checked on the wire. + int rate_attempts = 0; + bool rate_pending = false; // restart the grab with it at the next frame + long long wire_check_ms = 0; // steady: when to count packets; 0 = none + long long wire_packets0 = -1; + long long wire_start_ms = 0; + size_t frame_bytes = 0; // NUC run by the program (Options::nuc_interval_ms). int nuc_interval_found = -1; // the camera's own timer, put back on close bool nuc_ours = false; // the camera's timer is off and we run it @@ -418,15 +426,9 @@ struct TarisCameraSource::Impl { } setInt32(param::kPollingEnable, 1); - // The frame rate only once grabbing: set before the first startgrab after - // connecting, the camera answers ok and keeps sending what it sent before - // - 50 fps from power-up, 31 MB/s and 22 000 packets a second for the SDK - // and the CPU to take in (measured on tower 111). Set now, it holds. - float rate = static_cast(o.frame_rate_hz); - r = api->dev_setparam(dev, param::kFramerateHz, &rate, kTypeSingle); - if (!ok(r)) - LOG_WARN << "thermal camera: cannot set " << o.frame_rate_hz - << " Hz: " << describe(r) << "; it runs at its own rate"; + rate_attempts = 0; + rate_pending = true; + wire_check_ms = 0; std::string model; getString(param::kCameraName, model); @@ -486,6 +488,7 @@ struct TarisCameraSource::Impl { return; const size_t need = static_cast(w) * h * 2; if (bytes < 0 || static_cast(bytes) < need) return; + frame_bytes = need; // Polling hands out the newest frame again until a newer one arrives. The // camera's frame counter stays 0 (TarisIR mini, SDK 4.3.44), so a new @@ -584,6 +587,84 @@ struct TarisCameraSource::Impl { } } + // ---- frame rate ---------------------------------------------------------- + // The camera sends 50 fps from power-up: 31 MB/s and 22 000 packets a second + // for the SDK and the CPU to take in, for one picture every few seconds. Set + // right after connecting, Framerate_Hz answers ok and changes nothing; set + // while grabbing, it thins out what the SDK hands over but the camera keeps + // sending. Set while the grab is stopped, after the stream has run, it holds + // on the wire (tower 111: 5 Hz, 2 100 packets a second). So that is what is + // done once the first frame is in, and checked on the port's packet count. + void setFrameRate() { + float rate = static_cast(o.frame_rate_hz); + const Ret r = api->dev_setparam(dev, param::kFramerateHz, &rate, kTypeSingle); + ++rate_attempts; + if (!ok(r)) + LOG_WARN << "thermal camera: cannot set " << o.frame_rate_hz << " Hz: " << describe(r) + << "; it runs at its own rate"; + } + + static long long rxPackets(const std::string& interface) { + if (interface.empty()) return -1; + std::ifstream f("/sys/class/net/" + interface + "/statistics/rx_packets"); + long long v = -1; + f >> v; + return f ? v : -1; + } + + void restartForRate() { + rate_pending = false; + api->dev_stopgrab(dev, 0); + setFrameRate(); + setInt32(param::kPollingEnable, 1); + const Ret r = api->dev_startgrab(dev, 0); + setInt32(param::kPollingEnable, 1); + last_frame_steady = steadyMs(); // grace for the restart + if (!ok(r)) { + setOffline("cannot restart grabbing: " + describe(r)); + disconnect(); + return; + } + wire_check_ms = steadyMs() + 1000; // count packets once the stream is up again + wire_packets0 = -1; + } + + // Packets per second on the camera's port against what the rate should send. + void checkWire() { + if (wire_check_ms == 0 || steadyMs() < wire_check_ms) return; + if (o.interface.empty()) { + wire_check_ms = 0; + LOG_INFO << "thermal camera: " << o.frame_rate_hz + << " fps set; not checked on the wire (no [Camera.tir] interface)"; + return; + } + const long long now = steadyMs(), pk = rxPackets(o.interface); + if (pk < 0) { wire_check_ms = 0; return; } + if (wire_packets0 < 0) { // start counting + wire_packets0 = pk; + wire_start_ms = now; + wire_check_ms = now + 3000; + return; + } + wire_check_ms = 0; + const double pps = (pk - wire_packets0) * 1000.0 / std::max(1, now - wire_start_ms); + const double per_fr = frame_bytes > 0 ? frame_bytes / 1464.0 : 420.0; // GVSP payload + const double fps_wire = pps / per_fr; + if (fps_wire <= o.frame_rate_hz * 1.5) { + LOG_INFO << "thermal camera: " << std::lround(fps_wire * 10) / 10.0 << " fps on " + << o.interface << " (" << std::lround(pps) << " packets/s), set on attempt " + << rate_attempts; + } else if (rate_attempts >= 3) { + LOG_WARN << "thermal camera: still " << std::lround(fps_wire) << " fps on " << o.interface + << " (" << std::lround(pps) << " packets/s), not " << o.frame_rate_hz + << "; frame rate not taken"; + } else { + LOG_INFO << "thermal camera: " << std::lround(fps_wire) << " fps on " << o.interface + << ", not " << o.frame_rate_hz << "; restarting the grab again"; + rate_pending = true; + } + } + // ---- NUC --------------------------------------------------------------- // Switch the camera's own NUC timer off: from now on the program runs the // NUC, while the gimbal moves. The timer's value goes back on close. (Read @@ -787,6 +868,9 @@ struct TarisCameraSource::Impl { if (!connected) continue; serviceNuc(); poll(); + if (rate_pending && last_cam_ts >= 0.0) restartForRate(); // once a frame is in + if (!connected) continue; + checkWire(); const long long now = steadyMs(); if (now - last_frame_steady > o.stall_ms) { diff --git a/tests/test_tariscamerasource.cpp b/tests/test_tariscamerasource.cpp index bf9988a..38eec3b 100644 --- a/tests/test_tariscamerasource.cpp +++ b/tests/test_tariscamerasource.cpp @@ -233,6 +233,8 @@ TEST_CASE("the thermal camera connects, trying adapters until one answers") { t.cam.start(); t.sdk.push(29315, 57315); REQUIRE(t.online()); + // The rate is set by restarting the grab once the first frame is in. + CHECK(waitFor([&] { std::lock_guard l(t.sdk.m); return t.sdk.rate > 49.f; })); std::lock_guard l(t.sdk.m); REQUIRE(t.sdk.connect_attempts.size() >= 4); CHECK(t.sdk.connect_attempts.front() == "SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0");