Make the thermal camera's frame rate hold on the wire

Setting Framerate_Hz while grabbing only thinned out what the SDK handed over;
the camera went on sending 10 fps (and 50 from power-up). Set while the grab
is stopped, once the stream has run, it holds. So the grab is restarted with
the rate set when the first frame is in, and the packet rate of the camera's
port is counted: if it still carries more than 1.5x the rate's frames, the
restart is repeated (up to three times). On tower 111: 5.3 fps, 2 224 packets
a second instead of 22 234, on the second attempt; system load 18 % down from
about 29 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Peter Bochmann 2026-10-15 20:21:49 +02:00
parent 0076e623d0
commit a326a758c4
Signed by: peter.bochmann
GPG Key ID: 94E8C523489B8AC1
4 changed files with 107 additions and 14 deletions

View File

@ -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'

View File

@ -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.

View File

@ -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<Ret, int> 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<float>(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<size_t>(w) * h * 2;
if (bytes < 0 || static_cast<size_t>(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<float>(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<long long>(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) {

View File

@ -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<std::mutex> l(t.sdk.m); return t.sdk.rate > 49.f; }));
std::lock_guard<std::mutex> 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");