Compare commits

..

20 Commits

Author SHA1 Message Date
Peter Bochmann 2faa4508d9
Merge support for the older GigE cameras (Manta) on rietschen
Rietschen runs three Allied Vision Manta G-1236 on one GbE port, whose
firmware predates the names the program and the Alvium use. A feature
missing by its SFNC name is looked up by the older one (ExposureTimeAbs,
AcquisitionFrameRateAbs, ExposureAutoTarget, ExposureAutoAdjustTol), and
export_camera_profile.py picks a gige recipe for such a camera: frame
trigger off, BayerRG12Packed or BayerRG8 or Mono8, 60 MB/s, the frame rate
at the camera's maximum, auto-exposure on the frame mean at 30 % +- 5 %.
BayerRG12Packed, the GigE Vision 1.x 12-bit packing, is read as a layout of
its own. The camera probe lists every camera and install-tower asks which
is the colour one.

Two fixes reach every tower: the keep rule now reads the camera's own
target, tolerance and limits after the camera is opened (before, it judged
every frame against 50 +- 5 % until the first profile switch), and test
hardware env waits one read period for a DHT11's first reading.

Running on rietschen since 2026-10-20, in 12 bits since 2026-10-26: 52
waypoints at 3 s, 2.9 min a sweep. Tests 544/544 (mock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:59:47 +01:00
Peter Bochmann 44b8a2e84a
Read the Manta's 12 bits: BayerRG12Packed, packed the GigE Vision 1.x way
Rietschen's RGB camera switched to 12 bits offers BayerRG12Packed, not
BayerRG12p, and the program took it for an unknown format: from 2026-10-26
11:33 every picture was saved as "legacy", undebayered raw bytes. The two
are not the same format under two names. Both put two pixels in three
bytes, but the GigE Vision 1.x packing carries the first pixel's high bits
in the first byte, where GenICam's 12p carries its low ones; read as 12p
every other pixel is scrambled.

BayerRG12Packed is now a layout of its own, unpacked and sampled for
brightness its own way and de-Bayered like 12p. Checked against the
hardware on rietschen's NIR camera (Mono12Packed, the same packing): the
12-bit frame unpacked this way differs from an 8-bit frame of the same view
by 2.8 on average, unpacked as 12p by 29. The gige recipe prefers
BayerRG12Packed, so a re-export keeps the 12 bits. Tests 544/544 (mock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:46:32 +01:00
Peter Bochmann df11808d78
Aim the Mantas at 30 %: at 35 the sky was still too bright
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:59:50 +02:00
Peter Bochmann 7332c47d6d
Judge the keep rule against the camera's own target, and aim Mantas at 35 %
configureSettle() read the camera's auto-exposure target, tolerance and
limits before the camera was opened, so it got none of them and judged every
frame against its 50 +- 5 % default until the first profile switch - on
every tower. It now runs again once the camera is open. On the Alviums this
brings their own IntensityControllerTolerance and, at night, the at-limit
verdict from their chunk data into play.

The gige recipe aims at 35 % instead of 50: at 50 the sky over the horizon
burnt out at noon on rietschen, where smoke would rise. The old program ran
these cameras at 23-25. Tests 542/542 (mock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:40:44 +02:00
Peter Bochmann c62d8270b3
Let a Manta aim at the frame mean the keep rule waits for
Rietschen's RGB camera came with ExposureAutoAlg = FitRange and a 10 %
tolerance. FitRange fits the histogram wherever the mean lands, and the
camera stopped adjusting 10 % off its target, so the keep rule's 50 +- 5 %
band was never reached: at a 3 s interval every waypoint ran into its 2 s
frame_timeout (2026-10-21). The gige recipe now sets Mean and 5 %, as the
NIR camera already had. By day the exposure floor goes from 200 to 20 us:
at noon the camera sat at 200-600 us with no room to darken a bright view,
and without chunk data the host cannot tell a camera at its limit from one
still settling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:03:08 +02:00
Peter Bochmann fdb3c50b68
Run a Manta at its frame-rate limit, and wait for a DHT11's first reading
A free-running Manta runs at AcquisitionFrameRateAbs, not at its limit:
rietschen's RGB camera came set to 0.955 fps and delivered 5 frames in 5.2 s.
The gige recipe now writes a short fixed exposure and then the rate, clamped
to the camera's maximum (4.7 fps at 60 MB/s), before auto-exposure goes back
on. The export summary shows the rate set, not the limit, which had read
4.71 fps for a camera running at 0.96.

test hardware env asked for a DHT11 reading at once, but the firmware reads
it every 10 s, the first time 2 s after it starts. install-tower --verify
failed on rietschen 1.4 s into its run, 36 s after the same test had passed.
It now waits one read period for the first reading. Tests 542/542 (mock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:54:52 +02:00
Peter Bochmann 8850337250
Support the older GigE cameras (Manta) on rietschen
Rietschen has three Manta G-1236 on one GbE port, whose firmware predates the
names the program uses. A feature missing by its SFNC name is now looked up by
the older one (ExposureTimeAbs, AcquisitionFrameRateAbs, ExposureAutoTarget,
ExposureAutoAdjustTol), so test camera, the telemetry and the keep rule's
reads work. export_camera_profile.py picks a gige recipe for such a camera:
frame trigger off, BayerRG8 or Mono8, 60 MB/s, auto-exposure target 50 %.
At the cameras' old target of 25 every waypoint ran into frame_timeout_ms.

The camera probe now lists every camera. With several, the hardware.ini kit
writes CHOOSE_ONE, install-tower asks which is the colour one, and the profile
export is given that id - the first one the SDK lists was the NIR camera.
install-tower names the unit that holds the motor port instead of assuming
start_gimbal.service.

Tested on rietschen's NIR camera with the motor mocked, beside the running old
program: test camera PASS, a snapshot as a 3008x4112 grey JXL, a scan keeping
a picture every 5 s. Tests 542/542 (mock build). The colour camera is not yet
tested; the old program has it open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:13:04 +02:00
Peter Bochmann ed72f97fe5
Merge taking a thermal picture beside the colour one at every waypoint
A TarisIR mini (driver = taris) as a second camera source: the colour and the
thermal picture of each waypoint, the thermal counts saved 16-bit lossless,
the CamEvent extended by cal, missed, tmax, tmed, hotx and hoty. The program
runs the camera's NUC while the gimbal moves and holds its frame rate on the
wire; scripts/fgc-setcap gives the binary cap_net_raw.

Built and tested in an Ubuntu 22.04 container (mock and Vimba+MQTT, 542/542)
and running on tower 111 since 2026-10-14; from 2026-10-15 20:19 on, 15 h and
then 2.6 days of scanning without a missing thermal picture. Not yet tested:
pulling the camera's cable during a scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:16:41 +02:00
Peter Bochmann a326a758c4
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>
2026-10-15 20:21:49 +02:00
Peter Bochmann 0076e623d0
Set the thermal camera's frame rate once it is grabbing
Set before the first startgrab after connecting, Framerate_Hz answers ok and
changes nothing: the camera went on sending its power-up 50 fps, 31 MB/s and
22 000 packets a second on tower 111, which kept all four cores clocked up for
pictures taken one every 5 s. Set after startgrab it holds (5 Hz: 2 100
packets a second).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:10:08 +02:00
Peter Bochmann fc507f7407
Give the thermal picture the time its own frame arrived
Both frames of a waypoint used to take the colour frame's time, which made
the thermal one look taken at a moment it was not: it is the newest frame the
camera had, up to max_frame_age_ms old. Each file and CamEvent now has the
time its own frame reached the host; the two of one waypoint share hdg/pit and
are at most that age apart. The docs say to pair them by that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:42:32 +02:00
Peter Bochmann ff89643a4f
Run the thermal camera's NUC while the gimbal moves
The camera recalibrated against its shutter every 30 s and sent no frame for
about 0.7 s meanwhile: a picture falling into that had no thermal image (0.8 %
on tower 111, the 30 s rhythm shown by the new gap logging). The program now
switches the camera's timer off and runs the NUC itself (NUC_Perform, which
returns when it is done) when a move starts and nuc_interval_s (30) has passed,
or after twice that with no move. The scheduler tells the cameras of every move
(ICameraSource::moving), and a picture waits up to 1 s for a camera between
frames (frameReady) rather than go without it. The camera's own timer goes
back on exit; nuc_interval_s = 0 leaves it alone.

Measured on tower 111 before writing it: NUC_Interval = 0 gave 70 s without a
gap; NUC_Perform ran three NUCs of 657-677 ms; NUC_INPROGRESS never changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:02:20 +02:00
Peter Bochmann 1c2f0a0506
Log why a picture got no thermal frame
Every miss now says how old the newest frame was and whether a NUC was
running; every pause over 500 ms in the frame stream is logged with the
camera's own clock, the polls and getdata answers in it, and NUC_INPROGRESS,
which is watched on every poll; a summary of the stream goes out every ten
minutes, and the NUC configuration at connect. Adds the SDK's NUC parameters
(410-415), for running the NUC on purpose later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:59:49 +02:00
Peter Bochmann 28ff027950
Note what the thermal camera's SDK costs in CPU on tower 111
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:13:55 +02:00
Peter Bochmann 474e3cac8a
Fix what the thermal camera's first run on tower 111 showed
- The thermal frame was numbered camera 0 - label RGB, and the colour file of
  the same time overwritten - because Vimba counts no camera before open().
  The composite now takes each source's configured camera count, and two
  enabled cameras may no longer share a label.
- The SDK switches LC_NUMERIC to de_DE when loaded, which made "hotx":0,4336.
  Its calls now run under a guard that restores the locale, and the JSON's
  numbers no longer go through printf.
- The camera's frame counter is always 0: new frames are told by their time,
  so the stream no longer stops after the first one.
- The LUT is empty for the first frame: it is asked again on later ones.
- The SDK is libirbgrab_x86_64-linux.so in the ld cache.
- The null channel traces the CamEvent payload it would have sent.

Tested on tower 111 with the TarisIR mini, the motors mocked: cap_net_raw is
enough, RGB/TIR pairs with one timestamp, the TIR file decodes to the counts
its CamEvent describes, profile switches 0-1-0. 536/536 in the 22.04 container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:01:33 +02:00
Peter Bochmann 3d90af8490
Take a thermal picture beside the colour one at every waypoint
A TarisIR mini (driver = taris) is a second camera source behind a
CompositeCameraSource: the colour camera is asked first, then the thermal one
for its newest frame, and both frames carry the colour frame's timestamp.
Counts are saved 16-bit lossless under TIR/, and the CamEvent gains cal,
missed, tmax, tmed, hotx and hoty (docs/mqtt-api.md). The SDK is declared in
IrbgrabApi.h and loaded with dlopen, so no SDK file is in the repository; a
missing or unreachable camera stays offline (Health cams_offline) while the
scan goes on. scripts/fgc-setcap gives the binary cap_net_raw after a build.

Built and tested in an Ubuntu 22.04 container (mock, MQTT, Vimba+MQTT,
534/534), plus a mock run with RGB/TIR pairs. Not yet run against the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:37:25 +02:00
Peter Bochmann a03da2d8f4
Merge slowing down, then pausing, the scan when the tower computer runs hot or cannot keep up
Built and tested in an Ubuntu 22.04 container (mock and Vimba+MQTT, 494/494)
and running on tower 111 since 2026-10-14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:30:21 +02:00
pgdalmeida e1f63108f2
Merge stopping the gimbal where it is when switching from a scan to manual 2026-10-09 14:26:21 +02:00
pgdalmeida 2737811e2e
Stop the gimbal where it is when switching from a scan to manual 2026-10-09 14:25:51 +02:00
pgdalmeida 6fa43f1e08
Slow down, then pause, the scan when the tower computer runs hot or cannot keep up 2026-10-08 17:21:58 +02:00
94 changed files with 8777 additions and 145 deletions

View File

@ -94,7 +94,12 @@ add_library(fgc_core STATIC
src/core/ChannelHistory.cpp src/core/ChannelHistory.cpp
src/core/ChannelTopics.cpp src/core/ChannelTopics.cpp
src/core/RawImage.cpp src/core/RawImage.cpp
src/core/ThermalStats.cpp
src/camera/IrbgrabApi.cpp
src/camera/TarisCameraSource.cpp
src/camera/CompositeCameraSource.cpp
src/core/SettleRule.cpp src/core/SettleRule.cpp
src/core/ThermalGuard.cpp
src/sensors/I2cBus.cpp src/sensors/I2cBus.cpp
src/sensors/Sht41Protocol.cpp src/sensors/Sht41Protocol.cpp
src/sensors/McuEnvSensor.cpp src/sensors/McuEnvSensor.cpp
@ -107,7 +112,7 @@ target_include_directories(fgc_core PUBLIC
${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR} # for the bundled ini.h ${CMAKE_SOURCE_DIR} # for the bundled ini.h
) )
target_link_libraries(fgc_core PUBLIC Threads::Threads) target_link_libraries(fgc_core PUBLIC Threads::Threads ${CMAKE_DL_LIBS})
# PUBLIC so the application and the tests both see the same build identity. # PUBLIC so the application and the tests both see the same build identity.
target_compile_definitions(fgc_core PUBLIC FGC_GIT_REV="${FGC_GIT_REV}") target_compile_definitions(fgc_core PUBLIC FGC_GIT_REV="${FGC_GIT_REV}")

View File

@ -322,7 +322,7 @@ job. Details, payloads and safety rules: [docs/mqtt-api.md](docs/mqtt-api.md).
| Command | Does | | Command | Does |
|---|---| |---|---|
| `mode auto` / `mode manual` | sweep the scan grid / stop and take the gimbal by hand (it stays homed and powered) | | `mode auto` / `mode manual` | sweep the scan grid / stop the gimbal where it is and take it by hand (it stays homed and powered) |
| `mode pan <heading> <span> [<pitch>]` | watch one direction: `mode pan 90 30` covers 75°-105°, a picture every 6° | | `mode pan <heading> <span> [<pitch>]` | watch one direction: `mode pan 90 30` covers 75°-105°, a picture every 6° |
| `gimbal home` | find the endstops and centre - needed after every power-up | | `gimbal home` | find the endstops and centre - needed after every power-up |
| `gimbal move <yaw>[,<pitch>]` | point at a **true bearing** (0 = north) and an elevation, in degrees; without a pitch only yaw moves. A yaw-only tower ignores the pitch and refuses pitch-only commands | | `gimbal move <yaw>[,<pitch>]` | point at a **true bearing** (0 = north) and an elevation, in degrees; without a pitch only yaw moves. A yaw-only tower ignores the pitch and refuses pitch-only commands |

View File

@ -70,7 +70,7 @@ demosaic = edge_aware
; generated human-readable record of the same settings; it configures nothing. ; generated human-readable record of the same settings; it configures nothing.
[Camera.rgb] [Camera.rgb]
enabled = true enabled = true
driver = vimba ; only vimba is implemented driver = vimba ; vimba (colour) or taris (thermal, below)
id = DEV_0000000000 ; USB DEV_ id, or a GigE IP id = DEV_0000000000 ; USB DEV_ id, or a GigE IP
label = RGB ; output subdir + CamEvent "cam" label = RGB ; output subdir + CamEvent "cam"
profile_dir = camera_profiles/rgb profile_dir = camera_profiles/rgb
@ -98,6 +98,29 @@ settle_rule = brightness
settle_wait_ms = 2500 settle_wait_ms = 2500
frame_timeout_ms = 5000 frame_timeout_ms = 5000
; --- A thermal camera (InfraTec TarisIR mini), beside the colour one ---------
; It takes its picture at the same waypoints and intervals, right after the
; colour camera. Each file and CamEvent has the time its own frame arrived; the
; two of one waypoint share hdg/pit and are at most max_frame_age_ms apart.
; The counts are saved 16-bit and always lossless (jxl_distance must stay 0);
; T[K] = count x 0.01 under profile 0, x 0.02 under profile 1, emissivity 1.
; Its driver, id (the serial) and interface belong in hardware.ini; [Hardware]
; cameras lists it. Needs the InfraTec SDK and licence on the tower, and
; cap_net_raw on the binary (scripts/fgc-setcap, which deploy.sh runs) - see
; docs/tower-installation.md. No settings XML: profile only names the timing.
;[Camera.tir]
;label = TIR
;calib_profile = 0 ; 0: -40..120 C (forest), 1: 0..600 C
;frame_rate_hz = 5 ; the camera free-runs at this, throttled
;max_frame_age_ms = 400 ; older is not kept; below the scan's settle wait
;nuc_interval_s = 30 ; NUC at the start of a move, not mid-picture; 0 = camera's timer
;rotation_deg = 0
;jxl_effort = 3
;
;[Camera.tir.standard] ; optional: without it 0 / 0, as it should be
;settle_wait_ms = 0 ; it takes the frame it already has
;frame_timeout_ms = 0
[Serial] [Serial]
; Motor-controller serial device and baud rate. ; Motor-controller serial device and baud rate.
device = /dev/ttyACM0 device = /dev/ttyACM0
@ -273,6 +296,9 @@ enable_env = false
; The mocks are NOT legacy: substituting a simulated device is a choice about ; The mocks are NOT legacy: substituting a simulated device is a choice about
; this run, not a statement about what is bolted to the tower, so it stays here. ; this run, not a statement about what is bolted to the tower, so it stays here.
mock_camera = false mock_camera = false
; The simulated camera's frames: small (640x480 colour) or full (the real
; camera's 5328x3040 12-bit raw - saving then loads the CPU as on a tower).
;mock_camera_size = small
mock_serial = false mock_serial = false
mock_imu = false mock_imu = false
mock_env = false mock_env = false

View File

@ -53,7 +53,8 @@ env = sht41
; The legacy towers: off to 25 C, +20/255 per degree, full from 37.75 C, and full ; The legacy towers: off to 25 C, +20/255 per degree, full from 37.75 C, and full
; whenever the sensor cannot be read. Needs env = dht11 and [Pins] fan_pwm. ; whenever the sensor cannot be read. Needs env = dht11 and [Pins] fan_pwm.
;cooling = fan ;cooling = fan
; The [Camera.<name>] blocks expected to be present and found. ; The [Camera.<name>] blocks expected to be present and found ("rgb, tir" with
; the thermal camera).
cameras = rgb cameras = rgb
; How much bearing one picture covers, in degrees. Pan mode ('mode pan') cuts ; How much bearing one picture covers, in degrees. Pan mode ('mode pan') cuts
; its span into as many pictures as this takes. 6 until the lens is measured. ; its span into as many pictures as this takes. 6 until the lens is measured.
@ -64,6 +65,7 @@ pan_step_deg = 6
; (N5105) needs 2 workers for that. jxl_threads is libjxl's threads per frame; ; (N5105) needs 2 workers for that. jxl_threads is libjxl's threads per frame;
; 0 or absent = 4 / save_workers. save_queue_max is how many frames may wait ; 0 or absent = 4 / save_workers. save_queue_max is how many frames may wait
; or be saving before the scan holds at its waypoint (each raw frame ~24 MB). ; or be saving before the scan holds at its waypoint (each raw frame ~24 MB).
; A thermal camera adds a frame per picture (0.6 MB): raise it by 2.
save_workers = 1 save_workers = 1
;jxl_threads = 0 ;jxl_threads = 0
save_queue_max = 4 save_queue_max = 4
@ -76,6 +78,42 @@ save_queue_max = 4
; A tower with no hardware.ini at all is never reconciled, whatever ; A tower with no hardware.ini at all is never reconciled, whatever
; strict_presence says: gaining this file must not change how a tower behaves. ; strict_presence says: gaining this file must not change how a tower behaves.
[Thermal]
; The CPU guard: in summer the sun and the gimbal heat this computer until its
; CPU slows itself down. The guard spaces pictures out, then pauses them (gimbal
; parked, camera stream stopped) until it cools - it never stops the program and
; never touches picture quality. Every key may be left out; these are the
; defaults. See docs/thermal-throttling.md.
;enabled = true
; Slowed: pictures at least slow_factor times further apart (more if saving
; needs it: 1.5 x the save time per worker), from slow_c, or when the CPU
; throttles, or when saving is the bottleneck - the scan waited for it
; held_share_pct of the last minute, or held_slow_s in one go. Back to full pace
; once below slow_c - hysteresis_c, keeping up, and saving fast enough for it,
; for calm_s.
;slow_c = 85
;hysteresis_c = 5
;slow_factor = 2
;calm_s = 120
;held_share_pct = 25
;held_slow_s = 30
; Paused: at pause_c for pause_after_s, or saving still the bottleneck
; held_pause_s after slowing (or one wait that long). Resumes (slowed) after
; min_pause_s once at resume_c or below and nothing is left to save.
;pause_c = 92
;pause_after_s = 20
;held_pause_s = 120
;resume_c = 75
;min_pause_s = 300
; Throttling is also judged from the clock - below clock_low_pct of the chip's
; top speed while at least busy_high_pct busy - which catches the power-limit
; slow-downs the kernel does not count.
;clock_low_pct = 50
;busy_high_pct = 50
;sample_ms = 2000
; A fake /sys + /proc tree for a simulation (tools/thermal-sim/fake-temp.sh).
;sysfs_root = /
; --- Where to find it ----------------------------------------------------- ; --- Where to find it -----------------------------------------------------
; Device bindings. These follow the hardware, not the tower, so they belong here ; Device bindings. These follow the hardware, not the tower, so they belong here
; rather than in config.ini. ; rather than in config.ini.
@ -211,3 +249,11 @@ has_encoder = true
; The serial of the physical camera fitted to this tower. A USB DEV_ id, or a ; The serial of the physical camera fitted to this tower. A USB DEV_ id, or a
; GigE IP address. ; GigE IP address.
id = DEV_1AB22C049CD1 id = DEV_1AB22C049CD1
; A thermal camera (TarisIR mini): its serial number, as the SDK's search finds
; it, and the network port it is cabled to (static address, no DHCP, MTU 1500;
; only used to say what is wrong when the camera is not found).
;[Camera.tir]
;driver = taris
;id = 2017744
;interface = enp1s0

View File

@ -308,6 +308,10 @@ ssh "$REMOTE_HOST" "$RESOLVE_CMAKE $CHECK_DEPS_CMD cd '$REMOTE_DIR' && \
GIT_REV='$GIT_REV' \"\$CMAKE\" -S . -B build $CMAKE_ARGS && \ GIT_REV='$GIT_REV' \"\$CMAKE\" -S . -B build $CMAKE_ARGS && \
\"\$CMAKE\" --build build $JOBS_FLAG" \"\$CMAKE\" --build build $JOBS_FLAG"
echo ">> build complete: $REMOTE_DIR/build/fire_gimbal_control" echo ">> build complete: $REMOTE_DIR/build/fire_gimbal_control"
# The rebuilt binary has lost its capabilities; a thermal camera needs
# cap_net_raw back (scripts/fgc-setcap). Without a thermal camera: nothing.
# shellcheck disable=SC2029
ssh "$REMOTE_HOST" "cd '$REMOTE_DIR' && scripts/fgc-setcap --if-thermal" || true
# A pushed config.ini was written on another machine: check that it loads on # A pushed config.ini was written on another machine: check that it loads on
# this one, with this tower's hardware.ini layered over it, before anyone runs it. # this one, with this tower's hardware.ini layered over it, before anyone runs it.

View File

@ -17,7 +17,7 @@ The design separates **policy** (the control logic) from **mechanism** (the I/O
|-----------|------|-------------| |-----------|------|-------------|
| `IMotorController` | `SerialMotorController` (Boost.Asio) | `MockMotorController` (simulated sweep) | | `IMotorController` | `SerialMotorController` (Boost.Asio) | `MockMotorController` (simulated sweep) |
| `IControlChannel` | `MqttControlChannel` (Paho) | `NullControlChannel` (no broker) | | `IControlChannel` | `MqttControlChannel` (Paho) | `NullControlChannel` (no broker) |
| `ICameraSource` | `VimbaCameraSource` (Vimba X) | `MockCameraSource` (synthetic frames) | | `ICameraSource` | `VimbaCameraSource` (Vimba X), `TarisCameraSource` (InfraTec TarisIR, thermal), both behind `CompositeCameraSource` | `MockCameraSource` (synthetic frames, also thermal counts) |
| `IImuSource` | `MtiImuSource` (Xsens MTi, Boost.Asio) | `MockImuSource` (synthetic orientation) | | `IImuSource` | `MtiImuSource` (Xsens MTi, Boost.Asio) | `MockImuSource` (synthetic orientation) |
| `IEnvSensor` | `Sht41EnvSensor` (SHT41 over I2C) | `MockEnvSensor` (synthetic temp/humidity) | | `IEnvSensor` | `Sht41EnvSensor` (SHT41 over I2C) | `MockEnvSensor` (synthetic temp/humidity) |
@ -54,6 +54,7 @@ thread so all geometry/state mutation stays single-threaded.
| Image worker | `ImagePipeline` | drains the frame queue: rotate → encode → write → publish | | Image worker | `ImagePipeline` | drains the frame queue: rotate → encode → write → publish |
| MQTT client | Paho (internal) | delivers callbacks; auto-reconnects (no busy-wait loop) | | MQTT client | Paho (internal) | delivers callbacks; auto-reconnects (no busy-wait loop) |
| Camera acquisition | Vimba X (internal) | free-runs at the mode's frame rate and delivers frames via the observer, which keeps the latest complete one | | Camera acquisition | Vimba X (internal) | free-runs at the mode's frame rate and delivers frames via the observer, which keeps the latest complete one |
| Thermal acquisition | `TarisCameraSource` worker | owns every SDK call: connects (and reconnects), polls the newest frame of counts, switches the calibration profile; `trigger()` only takes the kept frame |
Shared state is mutex-guarded: latest `MotorTelemetry` (serial), `RemoteCommand` queue (channel), the frame queue Shared state is mutex-guarded: latest `MotorTelemetry` (serial), `RemoteCommand` queue (channel), the frame queue
(pipeline), the console command queue, and the latest `UiSnapshot`. The `CaptureScheduler` runs only on the (pipeline), the console command queue, and the latest `UiSnapshot`. The `CaptureScheduler` runs only on the
@ -132,6 +133,24 @@ everything below is an implementation detail of the camera source. That is what
## Image capture ## Image capture
### Two cameras, one array: `CompositeCameraSource`
With a thermal camera the scheduler and the pipeline still see one
`ICameraSource`. The composite numbers the colour cameras first (camera 0 is
the one the keep rule reads) and the thermal one after them. Its `trigger()`
asks the colour source first; only if that delivered does it ask the thermal
source, which hands over its newest frame if it is no older than
`max_frame_age_ms` and otherwise says no at once. Each frame keeps the time it
reached the host, so the two of one waypoint differ by up to that age. The
colour camera failing stops the run as
before; the thermal one failing never does (`offline()` says why, Health
`cams_offline` names it). Thermal frames are `Mono16` counts: the pipeline
rotates them, takes `tmax`/`tmed`/hot spot on the rotated image
(`ThermalStats.h`), and saves them 16-bit lossless whatever `jxl_distance` says.
The SDK is declared in `IrbgrabApi.h` (not copied: its licence forbids that) and
loaded with `dlopen`, so every build has the thermal code and its tests run
against a fake SDK.
### The model: free-run, then hold still ### The model: free-run, then hold still
The camera **runs continuously** at the mode's frame rate with its own auto-exposure and auto-gain in The camera **runs continuously** at the mode's frame rate with its own auto-exposure and auto-gain in

View File

@ -62,10 +62,10 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
| `Network` | `mqtt_user` / `mqtt_pw` | string | — | MQTT credentials (see secrets below) | | `Network` | `mqtt_user` / `mqtt_pw` | string | — | MQTT credentials (see secrets below) |
| `Serial` | `device` | string | `/dev/ttyACM0` | Motor-controller serial device | | `Serial` | `device` | string | `/dev/ttyACM0` | Motor-controller serial device |
| `Serial` | `baud` | int | `115200` | Serial baud rate | | `Serial` | `baud` | int | `115200` | Serial baud rate |
| `Camera.<name>` | `id` | string | — | USB `DEV_` id or GigE IP. Required when enabled | | `Camera.<name>` | `id` | string | — | `vimba`: USB `DEV_` id or GigE IP; `taris`: the camera's serial number. Required when enabled |
| `Camera.<name>` | `driver` | `vimba` | `vimba` | Backend. Only `vimba` is implemented | | `Camera.<name>` | `driver` | `vimba` \| `taris` | `vimba` | Backend: Allied Vision Vimba X (colour), or InfraTec TarisIR (thermal, [below](#thermal-camera-driver--taris)). Colour cameras always come first in the camera order, whatever the block names |
| `Camera.<name>` | `label` | string | the block name | Output subdirectory and `CamEvent` `cam` field | | `Camera.<name>` | `label` | string | the block name | Output subdirectory and `CamEvent` `cam` field |
| `Camera.<name>` | `profile_dir` | string | — | Directory of profile XMLs; a profile named X is `<profile_dir>/X.xml`. A relative path resolves against the directory holding the `config.ini` in use | | `Camera.<name>` | `profile_dir` | string | — | `vimba` only. Directory of profile XMLs; a profile named X is `<profile_dir>/X.xml`. A relative path resolves against the directory holding the `config.ini` in use |
| `Camera.<name>` | `profile` | string | `standard` | Profile to start in. Switch live with `set camera profile [<camera>] <name>` | | `Camera.<name>` | `profile` | string | `standard` | Profile to start in. Switch live with `set camera profile [<camera>] <name>` |
| `Camera.<name>` | `enabled` | bool | `true` | A disabled camera is not opened and does not affect the scan timing | | `Camera.<name>` | `enabled` | bool | `true` | A disabled camera is not opened and does not affect the scan timing |
| `Camera.<name>` | `rotation_deg` | `0`/`90`/`180`/`270` | `0` | Clockwise rotation applied to that camera's saved images | | `Camera.<name>` | `rotation_deg` | `0`/`90`/`180`/`270` | `0` | Clockwise rotation applied to that camera's saved images |
@ -75,6 +75,12 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
| `Camera` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | `0.8` / `4` | Encoder defaults inherited by every camera block | | `Camera` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | `0.8` / `4` | Encoder defaults inherited by every camera block |
| `Camera.<name>` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | the `[Camera]` value | Per-camera override of the encoder settings. Distance 0 is truly lossless | | `Camera.<name>` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | the `[Camera]` value | Per-camera override of the encoder settings. Distance 0 is truly lossless |
| `Camera` / `Camera.<name>` | `demosaic` | `edge_aware` \| `bilinear` | `edge_aware` | How raw profiles (`BayerRG8`, `BayerRG12p`) are converted to colour on the tower | | `Camera` / `Camera.<name>` | `demosaic` | `edge_aware` \| `bilinear` | `edge_aware` | How raw profiles (`BayerRG8`, `BayerRG12p`) are converted to colour on the tower |
| `Camera.<name>` (`taris`) | `calib_profile` | int ≥ 0 | `0` | Calibration profile set on connect: 0 = -40..120 °C at 0.01 K/count, 1 = 0..600 °C at 0.02 K/count. Switch live with `camera calib <n>` |
| `Camera.<name>` (`taris`) | `frame_rate_hz` | 0 < x ≤ 50 | `5` | The rate the camera free-runs at |
| `Camera.<name>` (`taris`) | `max_frame_age_ms` | int > 0 | `400` | A frame older than this when the picture is taken is not kept: that picture has no thermal image. Keep it below the scan's settle wait, or the frame can predate standstill (warned at startup) |
| `Camera.<name>` (`taris`) | `nuc_interval_s` | int 0..3600 | `30` | Seconds between NUCs, run by the program at the start of a move so none blanks a picture (after twice this with no move, one runs anyway). `0` leaves the camera's own timer, which now and then takes a picture's frame |
| `Camera.<name>` (`taris`) | `library` | string | `libirbgrab_x86_64-linux.so` | The InfraTec SDK, found through the ld cache |
| `Camera.<name>` (`taris`, hardware.ini) | `interface` | string | — | The network port the camera is cabled to; only named in the message when it is not found |
| `Hardware` (hardware.ini) | `save_workers` | int 1..4 | `1` | Frames saved at once. 1 on the IOTA, 2 on the 3 Delta ([measurements](raw12-pipeline-measurements.md)) | | `Hardware` (hardware.ini) | `save_workers` | int 1..4 | `1` | Frames saved at once. 1 on the IOTA, 2 on the 3 Delta ([measurements](raw12-pipeline-measurements.md)) |
| `Hardware` (hardware.ini) | `jxl_threads` | int 0..8 | `0` = 4 / `save_workers` | libjxl threads per saved frame | | `Hardware` (hardware.ini) | `jxl_threads` | int 0..8 | `0` = 4 / `save_workers` | libjxl threads per saved frame |
| `Hardware` (hardware.ini) | `save_queue_max` | int 1..32 | `4` | Frames waiting or being saved before the scan holds at its waypoint (a raw 16 MP frame is ~24 MB) | | `Hardware` (hardware.ini) | `save_queue_max` | int 1..32 | `4` | Frames waiting or being saved before the scan holds at its waypoint (a raw 16 MP frame is ~24 MB) |
@ -84,6 +90,7 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
| `Features` | `enable_serial` | bool | `true` | (reserved) | | `Features` | `enable_serial` | bool | `true` | (reserved) |
| `Features` | `enable_imu` | bool | `false` | **Legacy.** Derived from `[Hardware] imu` wherever that is declared; kept only for towers without a `hardware.ini`. A flag contradicting the declaration is warned about and ignored | | `Features` | `enable_imu` | bool | `false` | **Legacy.** Derived from `[Hardware] imu` wherever that is declared; kept only for towers without a `hardware.ini`. A flag contradicting the declaration is warned about and ignored |
| `Features` | `mock_camera` | bool | `false` | Use the simulated camera | | `Features` | `mock_camera` | bool | `false` | Use the simulated camera |
| `Features` | `mock_camera_size` | `small`/`full` | `small` | The simulated camera's frames: `small` = 640×480 colour; `full` = the real camera's 5328×3040 12-bit raw, so saving costs the CPU what it does on a tower (throttling simulations) |
| `Features` | `mock_serial` | bool | `false` | Use the simulated motor controller | | `Features` | `mock_serial` | bool | `false` | Use the simulated motor controller |
| `Features` | `mock_imu` | bool | `false` | Use the simulated IMU instead of the MTi | | `Features` | `mock_imu` | bool | `false` | Use the simulated IMU instead of the MTi |
| `Features` | `enable_env` | bool | `false` | **Legacy.** Derived from `[Hardware] env` wherever that is declared; same rule as `enable_imu` | | `Features` | `enable_env` | bool | `false` | **Legacy.** Derived from `[Hardware] env` wherever that is declared; same rule as `enable_imu` |
@ -541,6 +548,35 @@ measured), both edges included - so `mode pan 90 30` takes six pictures, 75°
to 105°. It is a property of the camera and lens, so it lives in to 105°. It is a property of the camera and lens, so it lives in
`hardware.ini`. Must be above 0. See [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual). `hardware.ini`. Must be above 0. See [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual).
### When the computer runs hot: `[Thermal]`
The CPU guard ([ThermalGuard.h](../include/fgc/ThermalGuard.h)) reads the CPU's
temperature, clock, throttle counters and busy time every `sample_ms` and paces
the scan to them. It never stops the program, never changes the mode and never
touches picture quality. Per computer, so normally in `hardware.ini`
(`config.ini` works too). Every key is optional.
| Key | Default | Meaning |
|---|---|---|
| `enabled` | `true` | `false`: full pace however hot (readings and throttling log go on) |
| `slow_c` | 85 | Slowed from this CPU temperature (°C) |
| `hysteresis_c` | 5 | Back to normal needs `slow_c - hysteresis_c` |
| `slow_factor` | 2 | Slowed = pictures at least this many times further apart (1..10); further still when saving needs it (1.5 × the save time per worker) |
| `calm_s` | 120 | Slowed → normal once all clear this long, and the normal pace fits what saving takes |
| `held_share_pct` | 25 | Slowed when the scan spent this share of the last minute waiting for saving |
| `held_slow_s` | 30 | Slowed when saving has kept the scan waiting this long in one go |
| `pause_c` | 92 | Paused once at this temperature for `pause_after_s` |
| `pause_after_s` | 20 | |
| `held_pause_s` | 120 | Paused when saving has kept the scan waiting this long in one go, or is still the bottleneck this long after slowing down |
| `resume_c` | 75 | A pause ends at or below this, after `min_pause_s`, with nothing left to save |
| `min_pause_s` | 300 | |
| `clock_low_pct` / `busy_high_pct` | 50 / 50 | Throttling judged from the clock: below this share of the top clock while at least this busy |
| `sample_ms` | 2000 | 500..60000 |
| `sysfs_root` | `/` | A fake `/sys` + `/proc` tree for a simulation ([tools/thermal-sim](../tools/thermal-sim/)) |
Must hold: `resume_c < slow_c < pause_c`, `held_slow_s <= held_pause_s`. The
whole story, and how to simulate it, is in [thermal-throttling.md](thermal-throttling.md).
### The legacy towers: a DHT11 and a fan on the motor MCU ### The legacy towers: a DHT11 and a fan on the motor MCU
Four towers carry an ambient DHT11 and an enclosure fan wired to the Leonardo Four towers carry an ambient DHT11 and an enclosure fan wired to the Leonardo
@ -683,6 +719,7 @@ Parsed by Boost.Program_options ([main.cpp](../main.cpp)). Flags override `[Feat
| `-c, --config <path>` | Explicit config file path | | `-c, --config <path>` | Explicit config file path |
| `--no-mqtt` | Disable MQTT (use the null channel) | | `--no-mqtt` | Disable MQTT (use the null channel) |
| `--mock-camera` | Use the simulated camera | | `--mock-camera` | Use the simulated camera |
| `--mock-camera-full` | Simulated camera with tower-size 12-bit frames (implies `--mock-camera`) |
| `--mock-serial` | Use the simulated motor controller | | `--mock-serial` | Use the simulated motor controller |
| `--mock-env` | Use the simulated ambient temp/humidity sensor (implies `[Features] enable_env`) | | `--mock-env` | Use the simulated ambient temp/humidity sensor (implies `[Features] enable_env`) |
| `--degraded` | Start even though declared hardware is missing (field triage; overrides `strict_presence` and says so loudly in the log) | | `--degraded` | Start even though declared hardware is missing (field triage; overrides `strict_presence` and says so loudly in the log) |
@ -725,6 +762,8 @@ Handled in `Application::Impl::handleCommand`.
| `snapshot` | One image now, through the scan's settle-and-trigger path (manual mode, homed) | | `snapshot` | One image now, through the scan's settle-and-trigger path (manual mode, homed) |
| `capture [on\|off]` | Continuous capture where the gimbal is aimed: one image per `image_interval` since the last, each once the gimbal has stood still for `settle_wait_ms`. Manual mode, homed; ends on any mode change, homing, heading setup or test. Bare: report | | `capture [on\|off]` | Continuous capture where the gimbal is aimed: one image per `image_interval` since the last, each once the gimbal has stood still for `settle_wait_ms`. Manual mode, homed; ends on any mode change, homing, heading setup or test. Bare: report |
| `camera reset` | Reboot the cameras and put them back on their profiles | | `camera reset` | Reboot the cameras and put them back on their profiles |
| `thermal` | The CPU's temperature, clock, load and throttling, and whether the scan is slowed or paused for it. Any mode |
| `thermal resume` / `thermal off` / `thermal on` | Back to full pace now (the rules then apply afresh) / turn the CPU guard off or on until restart. Any mode |
| `test camera [frames\|dark\|bright]` | Check the camera delivers frames and responds to exposure/gain | | `test camera [frames\|dark\|bright]` | Check the camera delivers frames and responds to exposure/gain |
| `test [<subsystem> [<leaf>]] [<profile>]` | Run the hardware self-test suite (see below) | | `test [<subsystem> [<leaf>]] [<profile>]` | Run the hardware self-test suite (see below) |
| `test list` / `test baseline` | List profiles / pin the last report as the comparison baseline | | `test list` / `test baseline` | List profiles / pin the last report as the comparison baseline |
@ -1021,3 +1060,37 @@ grid (front to back, repeating); in **pan** mode it walks the pan's headings lef
pitch, then starts again from the left edge (see [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual)). Telemetry arrives as pitch, then starts again from the left edge (see [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual)). Telemetry arrives as
firmware `ST` lines (per-axis state + encoder counts), parsed by firmware `ST` lines (per-axis state + encoder counts), parsed by
[TelemetryParser](../src/core/TelemetryParser.cpp). [TelemetryParser](../src/core/TelemetryParser.cpp).
## Thermal camera (`driver = taris`)
A TarisIR mini beside the colour camera takes its picture at the same waypoints
and intervals: the colour camera is asked first, and only once it has delivered
is the thermal one asked for its newest frame. Each file and CamEvent has the
time its own frame arrived: the two of one waypoint share `hdg`/`pit` and are at
most `max_frame_age_ms` apart (see [mqtt-api.md](mqtt-api.md#thermal-camera-cam-tir)).
- **Best effort.** A thermal camera that is missing, unlicensed or has no fresh
frame costs that picture its thermal image, nothing more: the scan goes on, the
camera reconnects by itself, and Health `cams_offline` names it meanwhile.
- **Always lossless.** The counts are measurements; `jxl_distance` must be 0
(the default for this driver) and the loader refuses anything else.
- **No settings XML.** The camera is set up from its keys; `profile` only picks a
`[Camera.<name>.<profile>]` timing block, which it does not need: without one
its wait is 0 and the colour camera's settle wait alone sets the pace. The
frame it hands over is at most `max_frame_age_ms` old, so that must stay below
the colour camera's `settle_wait_ms`.
- **Kept grabbing**: from start to exit, also in manual mode. A microbolometer
that is restarted begins with zero frames and a fresh NUC.
- **NUC while the gimbal moves.** The camera recalibrates against its shutter
every 30 s and sends no frame for about 0.7 s meanwhile; a picture falling
into that had no thermal image (0.8 % of them on tower 111). The program
switches the camera's timer off and runs the NUC itself at the start of a
move once `nuc_interval_s` has passed; the move and the settle wait leave
seconds before the next picture. A picture that still finds the camera
between frames waits up to 1 s for the next one. On exit the camera's timer
is put back.
- **Split between the files** like any camera: `driver`, `id` and `interface`
in hardware.ini (with `tir` in `[Hardware] cameras`), the rest in config.ini.
`save_queue_max` counts frames, so raise it by 2.
- The binary needs `cap_net_raw` (the SDK uses a raw socket):
[tower-installation.md](tower-installation.md#phase-2b---thermal-camera).

View File

@ -166,8 +166,25 @@ scripts for this are worth keeping; note that wrapping them in `timeout` SIGTERM
key features (`AcquisitionFrameRate`, `Width`, `PixelFormat`) would turn a silent misconfiguration into key features (`AcquisitionFrameRate`, `Width`, `PixelFormat`) would turn a silent misconfiguration into
an error. an error.
- Graceful shutdown on SIGINT (currently exit via `exit`/Ctrl-D; a pending `getline` can delay shutdown). - Graceful shutdown on SIGINT (currently exit via `exit`/Ctrl-D; a pending `getline` can delay shutdown).
- Support a second camera backend. `driver` is parsed and validated but only `vimba` is accepted, so a - ~~Support a second camera backend.~~ Done for the thermal camera (`driver = taris`,
thermal or NIR unit needs a new `ICameraSource` implementation behind that key. `TarisCameraSource`, behind `CompositeCameraSource`), tested with the TarisIR mini on tower 111 on
2026-10-14 (docs/tower-installation.md, Phase 2b). Not yet tested: a pulled camera cable during a scan
(the reconnect is only unit-tested). The 0.8 % of pictures without a thermal frame in the first long run
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 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'
from .../rgb/standard.xml failed (error -39)`, and the camera runs on whatever it had. Unrelated to the
thermal camera; seen again while testing it.
- Per-channel boresight: `[Motor]` heading and tilt calibration belong to the tower, but the thermal camera
sits on the gimbal with its own small offset against the colour one. `hotx`/`hoty` are therefore in the
thermal image's own frame; projecting them onto terrain needs a per-camera offset nobody has measured yet.
- Reintroduce optional image upload to the ground station, config-driven (the old hardcoded NFS/SMB upload was - Reintroduce optional image upload to the ground station, config-driven (the old hardcoded NFS/SMB upload was
removed). removed).
- **Calibration persistence needs the TUI.** `gimbal pitch setup` and `gimbal heading fit` are always - **Calibration persistence needs the TUI.** `gimbal pitch setup` and `gimbal heading fit` are always

View File

@ -26,7 +26,7 @@ Per-file reference for the refactored tree, plus the shared data structures.
| [include/fgc/DiagParser.h](../include/fgc/DiagParser.h), [src/core/DiagParser.cpp](../src/core/DiagParser.cpp) | `parseDiag` firmware `DG` self-test stream → `DiagResult` (per-axis tests, PASS/FAIL); `formatDiag` | | [include/fgc/DiagParser.h](../include/fgc/DiagParser.h), [src/core/DiagParser.cpp](../src/core/DiagParser.cpp) | `parseDiag` firmware `DG` self-test stream → `DiagResult` (per-axis tests, PASS/FAIL); `formatDiag` |
| [include/fgc/TestRunner.h](../include/fgc/TestRunner.h), [src/core/TestRunner.cpp](../src/core/TestRunner.cpp) | The `test` command: resolves `test [<subsystem> [<leaf>]]` to a set of leaf tests, knows which need a homed gimbal (the `test hardware` leaves deliberately do not), and runs them on a worker thread with live progress, handing back a `TestReport` | | [include/fgc/TestRunner.h](../include/fgc/TestRunner.h), [src/core/TestRunner.cpp](../src/core/TestRunner.cpp) | The `test` command: resolves `test [<subsystem> [<leaf>]]` to a set of leaf tests, knows which need a homed gimbal (the `test hardware` leaves deliberately do not), and runs them on a worker thread with live progress, handing back a `TestReport` |
| [include/fgc/TestReport.h](../include/fgc/TestReport.h), [src/core/TestReport.cpp](../src/core/TestReport.cpp) | A self-test run's result as plain data: formats a human-readable text report that `parseTestReport` reads back, so a run can be compared with the baseline or the previous one | | [include/fgc/TestReport.h](../include/fgc/TestReport.h), [src/core/TestReport.cpp](../src/core/TestReport.cpp) | A self-test run's result as plain data: formats a human-readable text report that `parseTestReport` reads back, so a run can be compared with the baseline or the previous one |
| [include/fgc/HostMetrics.h](../include/fgc/HostMetrics.h), [src/core/HostMetrics.cpp](../src/core/HostMetrics.cpp) | Tower computer health for `test host`: CPU temperature, disk, memory and load. Pure parsers split from the live `/sys` and `/proc` readers so they unit-test anywhere; readers report `ok = false` rather than throw | | [include/fgc/HostMetrics.h](../include/fgc/HostMetrics.h), [src/core/HostMetrics.cpp](../src/core/HostMetrics.cpp) | Tower computer health for `test host` and the CPU guard: CPU temperature, clock, kernel throttle counters, busy time, disk, memory and load. Every `/sys` and `/proc` path sits under a `root`, so a fake tree stands in for a simulation. Pure parsers split from the live `/sys` and `/proc` readers so they unit-test anywhere; readers report `ok = false` rather than throw |
| [include/fgc/Calibration.h](../include/fgc/Calibration.h), [src/core/Calibration.cpp](../src/core/Calibration.cpp) | `linearFit` (least-squares, R²) + `circularMeanDeg` for the IMU-referenced calibration | | [include/fgc/Calibration.h](../include/fgc/Calibration.h), [src/core/Calibration.cpp](../src/core/Calibration.cpp) | `linearFit` (least-squares, R²) + `circularMeanDeg` for the IMU-referenced calibration |
| [include/fgc/PitchSetup.h](../include/fgc/PitchSetup.h), [src/core/PitchSetup.cpp](../src/core/PitchSetup.cpp) | `gimbal pitch setup` worker thread: home-if-needed → pitch sweep dwelling on the gravity-referenced IMU pitch → least-squares fit → park level; tunable via `CalibParams`; exposes `progress()`/`report()`/`takeResult()`. Yaw is not touched | | [include/fgc/PitchSetup.h](../include/fgc/PitchSetup.h), [src/core/PitchSetup.cpp](../src/core/PitchSetup.cpp) | `gimbal pitch setup` worker thread: home-if-needed → pitch sweep dwelling on the gravity-referenced IMU pitch → least-squares fit → park level; tunable via `CalibParams`; exposes `progress()`/`report()`/`takeResult()`. Yaw is not touched |
| [include/fgc/HeadingFit.h](../include/fgc/HeadingFit.h), [src/core/HeadingFit.cpp](../src/core/HeadingFit.cpp) | `gimbal heading fit`: least-squares fit of the yaw map from operator sightings of landmarks with known true bearings. Sorts by encoder position, unwraps bearings so a set spanning north fits one line, and refuses a baseline under 10° (two points always fit exactly, so a short baseline looks perfect and is wrong). No IMU involved | | [include/fgc/HeadingFit.h](../include/fgc/HeadingFit.h), [src/core/HeadingFit.cpp](../src/core/HeadingFit.cpp) | `gimbal heading fit`: least-squares fit of the yaw map from operator sightings of landmarks with known true bearings. Sorts by encoder position, unwraps bearings so a set spanning north fits one line, and refuses a baseline under 10° (two points always fit exactly, so a short baseline looks perfect and is wrong). No IMU involved |
@ -41,7 +41,8 @@ Per-file reference for the refactored tree, plus the shared data structures.
| [include/fgc/ChannelHistory.h](../include/fgc/ChannelHistory.h), [src/core/ChannelHistory.cpp](../src/core/ChannelHistory.cpp) | Ring buffer of recently published messages (time, topic, payload, outcome) plus the delivery counters — sent, failed, connects, losses, last status. Feeds the expanded Connectivity view so a publish can be confirmed without a broker-side subscriber | | [include/fgc/ChannelHistory.h](../include/fgc/ChannelHistory.h), [src/core/ChannelHistory.cpp](../src/core/ChannelHistory.cpp) | Ring buffer of recently published messages (time, topic, payload, outcome) plus the delivery counters — sent, failed, connects, losses, last status. Feeds the expanded Connectivity view so a publish can be confirmed without a broker-side subscriber |
| [include/fgc/RemoteCommands.h](../include/fgc/RemoteCommands.h), [src/core/RemoteCommands.cpp](../src/core/RemoteCommands.cpp) | Console commands that arrive over MQTT: run through the same handler as typed ones, on the control thread, and answered on `CommandReply` with every line they logged. Retained messages are never run, a repeated id is answered again rather than re-run, each is audited in the log, and a reply that depends on the firmware (`gimbal dump`) waits for its answer | | [include/fgc/RemoteCommands.h](../include/fgc/RemoteCommands.h), [src/core/RemoteCommands.cpp](../src/core/RemoteCommands.cpp) | Console commands that arrive over MQTT: run through the same handler as typed ones, on the control thread, and answered on `CommandReply` with every line they logged. Retained messages are never run, a repeated id is answered again rather than re-run, each is audited in the log, and a reply that depends on the firmware (`gimbal dump`) waits for its answer |
| [include/fgc/Hash.h](../include/fgc/Hash.h), [src/core/Hash.cpp](../src/core/Hash.cpp) | FNV-1a 64 over a string or a file, with an algorithm-prefixed label. Change detection for the profile records, deliberately not a crypto hash | | [include/fgc/Hash.h](../include/fgc/Hash.h), [src/core/Hash.cpp](../src/core/Hash.cpp) | FNV-1a 64 over a string or a file, with an algorithm-prefixed label. Change detection for the profile records, deliberately not a crypto hash |
| [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces: move, standstill, settle wait (+ `SettleRule`), the rest of the interval since the last frame, keep; holds while saving is behind; injectable clock; `setGeometry` adopts a recalibration | | [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces: move, standstill, settle wait (+ `SettleRule`), the rest of the interval since the last frame, keep; holds while saving is behind; slowed (`setIntervalScale`) or paused (`setPaused`) by the CPU guard; injectable clock; `setGeometry` adopts a recalibration |
| [include/fgc/ThermalGuard.h](../include/fgc/ThermalGuard.h), [src/core/ThermalGuard.cpp](../src/core/ThermalGuard.cpp) | The CPU guard, pure: fed one host sample at a time (temperature, clock, throttle counters, busy %, whether saving holds the scan), it says normal / slowed / paused and returns the log lines; `ThrottleWatch` logs throttling as it starts, once a minute while it lasts, and when it ends. Never stops the program or changes the mode ([thermal-throttling.md](thermal-throttling.md)) |
| [include/fgc/SettleRule.h](../include/fgc/SettleRule.h), [src/core/SettleRule.cpp](../src/core/SettleRule.cpp) | The `settle_rule = brightness` keep rule, pure: fed one frame's brightness, exposure and gain at a time, says when the image has settled (in the camera's target band and steady for 3 frames) or is pinned at the auto-exposure limits | | [include/fgc/SettleRule.h](../include/fgc/SettleRule.h), [src/core/SettleRule.cpp](../src/core/SettleRule.cpp) | The `settle_rule = brightness` keep rule, pure: fed one frame's brightness, exposure and gain at a time, says when the image has settled (in the camera's target band and steady for 3 frames) or is pinned at the auto-exposure limits |
| [include/fgc/RawImage.h](../include/fgc/RawImage.h), [src/core/RawImage.cpp](../src/core/RawImage.cpp) | Pixel layouts the program accepts (`Mono8`, `RGB8`, `BayerRG8`, `BayerRG12p`), their sizes and bit depths, `unpack12p`, and `sampleBrightness` - the cheap per-frame sample the keep rule reads | | [include/fgc/RawImage.h](../include/fgc/RawImage.h), [src/core/RawImage.cpp](../src/core/RawImage.cpp) | Pixel layouts the program accepts (`Mono8`, `RGB8`, `BayerRG8`, `BayerRG12p`), their sizes and bit depths, `unpack12p`, and `sampleBrightness` - the cheap per-frame sample the keep rule reads |
| [include/fgc/CameraStall.h](../include/fgc/CameraStall.h), [src/core/CameraStall.cpp](../src/core/CameraStall.cpp) | When a camera stream delivers nothing new: wait, restart the stream, or reset the camera. Pure policy, scaled by the exposure and frame period so a slow frame is not taken for a stall | | [include/fgc/CameraStall.h](../include/fgc/CameraStall.h), [src/core/CameraStall.cpp](../src/core/CameraStall.cpp) | When a camera stream delivers nothing new: wait, restart the stream, or reset the camera. Pure policy, scaled by the exposure and frame period so a slow frame is not taken for a stall |

View File

@ -78,6 +78,7 @@ setup, or while a test or calibration runs.
|-------|------|---------|--------------| |-------|------|---------|--------------|
| `GGS/FWT/<tower>/StatusCode` | At startup, on every mode change, and again on every reconnect | the mode: `"0"` auto, `"1"` pan, `"2"` manual | 1 / retained | | `GGS/FWT/<tower>/StatusCode` | At startup, on every mode change, and again on every reconnect | the mode: `"0"` auto, `"1"` pan, `"2"` manual | 1 / retained |
| `GGS/FWT/<tower>/CamEvent` | After each image is saved | JSON object (below) | 1 / retained | | `GGS/FWT/<tower>/CamEvent` | After each image is saved | JSON object (below) | 1 / retained |
| `GGS/FWT/<tower>/Health` | When the CPU guard changes state or throttling starts/ends, once a minute, after a `thermal` command, and on every reconnect | JSON object (below) | 1 / retained |
StatusCode says what the tower **is doing**, not what it was last asked to do: StatusCode says what the tower **is doing**, not what it was last asked to do:
it is published by the mode change itself, whatever caused it. Being retained, it is published by the mode change itself, whatever caused it. Being retained,
@ -94,7 +95,7 @@ Built by `MqttControlChannel::publishCamEvent` from a `CamEvent`:
| Field | Type | Meaning | | Field | Type | Meaning |
|-------|------|---------| |-------|------|---------|
| `fwt` | string | Tower name (`config.ini` `tower_name`) | | `fwt` | string | Tower name (`config.ini` `tower_name`) |
| `cam` | string | Camera label: `RGB`, `ACR`, or `NIR` (by camera index) | | `cam` | string | Camera label (`[Camera.<name>] label`): `RGB`, `TIR`, `ACR` or `NIR` |
| `hdg` | int | Gimbal **true bearing** × 10 (0 = north) at capture time, from the **yaw encoder** | | `hdg` | int | Gimbal **true bearing** × 10 (0 = north) at capture time, from the **yaw encoder** |
| `pit` | int | Gimbal pitch elevation **× 10** at capture time, from the **IMU** — elevation relative to gravity, i.e. true pointing angle (fallbacks below) | | `pit` | int | Gimbal pitch elevation **× 10** at capture time, from the **IMU** — elevation relative to gravity, i.e. true pointing angle (fallbacks below) |
| `time` | int | Capture timestamp, Unix epoch **milliseconds** (matches the `.jxl` filename) | | `time` | int | Capture timestamp, Unix epoch **milliseconds** (matches the `.jxl` filename) |
@ -134,6 +135,80 @@ Both `hdg` and `pit` are sampled **when the frame arrives**, not when its JPEG X
finishes, so a slow encode or a backed-up queue cannot attribute a later pose to earlier finishes, so a slow encode or a backed-up queue cannot attribute a later pose to earlier
pixels. pixels.
#### Thermal camera (`cam: "TIR"`)
A thermal camera's events carry six more fields; a colour camera's never do, and
their payload is unchanged.
```json
{ "fwt":"ExampleTower", "cam":"TIR", "hdg":1373, "pit":300, "time":1719312345678,
"cal":0, "missed":0, "tmax":3015, "tmed":187, "hotx":0.7359, "hoty":0.5833 }
```
| Field | Type | Meaning |
|-------|------|---------|
| `cal` | int | Calibration profile the image was taken on. **Required to read the file**: it sets the counts' scale |
| `missed` | int | GigE packets the SDK lost for this frame (transfer quality) |
| `tmax` | int/null | Hottest pixel, **apparent** temperature in tenths of a °C |
| `tmed` | int/null | Median of the image, tenths of a °C (the anchor for a display window) |
| `hotx`, `hoty` | number/null | Where the hottest pixel is: its centre, 0..1 across and down the image **as stored** (after `rotation_deg`), as for detection boxes |
- **Its own `time`.** The thermal camera is asked right after the colour one, at
the same waypoint, and hands over the newest frame it has. `time` (and the file
name) is when that frame reached the tower computer, so it is up to
`max_frame_age_ms` (400) off the colour picture's, earlier or later. The `RGB`
and `TIR` event of one waypoint share `hdg` and `pit` (sampled together) and
follow each other in the stream; pair them by that, not by an equal `time`. A
picture the thermal camera had no fresh frame for has no `TIR` event.
- **The file holds counts**, 16-bit single channel, lossless:
`T[K] = count × 0.01` under `cal` 0 (`FF (-40-120C)`), `× 0.02` under `cal` 1
(`FF (0-600C)`), valid for `count ≤ 65280` (above it the camera's table wraps;
0 is never a temperature).
- **Apparent temperature**: emissivity and path transmission are taken as 1.
Vegetation (ε ≈ 0.97) over kilometres of air reads a few kelvin off its true
temperature; the counts keep every correction possible later. `tmax`/`tmed`
use the same convention, so they are for triage and display, not for a
reported fire temperature.
- `null` in the four statistics means the frame had no valid pixel; `cal` and
`missed` are still set.
CamEvent and Health leave through a small queue on their own thread, so a slow link
never holds an image save or the control loop. If 64 are waiting, the oldest is
dropped (and shows as dropped in the connectivity view).
### Health payload
The tower computer's heat and load, and whether the scan is slowed or paused for it
(see [thermal-throttling.md](thermal-throttling.md)). The mode in StatusCode does
**not** change: a paused auto scan is still `"0"`, and carries on by itself once cool.
```json
{"fwt":"t121","state":"paused","reason":"CPU at 93 °C for 20 s","guard":true,
"cpu_c":93.0,"cpu_mhz":1188,"cpu_busy_pct":41,"throttling":true,
"throttle_events_last_min":14,"enclosure_c":48.2,"saved_last_min":0,
"save_ms_mean":null,"save_ms_max":null,"cams_offline":"","held_s":0,
"interval_s":10.0,"time":1759917600000}
```
| Field | Type | Meaning |
|-------|------|---------|
| `state` | string | `normal`, `slowed` (pictures `slow_factor`× further apart) or `paused` (gimbal parked, camera stream stopped) |
| `reason` | string | Why not normal, in words; `""` when normal |
| `guard` | bool | `false` after `thermal off`: full pace whatever the heat |
| `cpu_c` | number/null | Hottest CPU thermal zone, °C |
| `cpu_mhz` | number/null | Mean clock over the cores |
| `cpu_busy_pct` | number/null | How busy the CPU was since the last sample |
| `throttling` | bool | The CPU is slowing itself (kernel counters, or a low clock while busy) |
| `throttle_events_last_min` | int | Kernel throttle events counted in the last minute |
| `enclosure_c` | number/null | The ambient sensor (SHT41/DHT11), when there is one |
| `saved_last_min`, `save_ms_mean`, `save_ms_max` | int, number/null | Pictures saved in the last minute and how long each took (every camera's: with a thermal camera, two per waypoint) |
| `cams_offline` | string | Labels of cameras not delivering, comma-separated (`"TIR"`); `""` when all are. Only a camera the scan can do without - the thermal one - is ever listed: it reconnects by itself while the scan goes on |
| `held_s` | int | How long the scan has been waiting for saving to catch up |
| `interval_s` | number | Time between pictures now |
| `time` | int | Unix epoch ms |
A missing reading is `null`, never `0`.
## Remote commands: `Command` / `CommandReply` ## Remote commands: `Command` / `CommandReply`
For development and maintenance: **any console command** - everything that can For development and maintenance: **any console command** - everything that can
@ -239,6 +314,7 @@ subscribe: GGS/FWT/<tower>/Command (any console command: JSON {id, cmd}
publish: GGS/FWT/<tower>/StatusCode (the mode: 0 auto, 1 pan, 2 manual) publish: GGS/FWT/<tower>/StatusCode (the mode: 0 auto, 1 pan, 2 manual)
GGS/FWT/<tower>/CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms) GGS/FWT/<tower>/CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms)
GGS/FWT/<tower>/Health (JSON: CPU guard state, CPU °C/MHz/busy, throttling, saving)
GGS/FWT/<tower>/CommandReply (JSON: id, cmd, level, lines, truncated - not retained) GGS/FWT/<tower>/CommandReply (JSON: id, cmd, level, lines, truncated - not retained)
``` ```

247
docs/thermal-throttling.md Normal file
View File

@ -0,0 +1,247 @@
# When the tower computer runs hot
In summer the sun and the gimbal heat the LattePanda until its CPU slows itself
down (throttles). Since the 12-bit raw pipeline, the tower also de-Bayers and
encodes every picture, ~1.4 CPU-seconds each
([raw12-pipeline-measurements.md](raw12-pipeline-measurements.md)). This page
says what happens then, what the program does about it, and how to simulate it.
**The rule:** heat or overload only ever **pauses** the program - it never
exits, never changes the mode, never touches picture quality or resolution. It
slows the scan, then parks it, and carries on by itself once the computer has
cooled. An exit would leave the tower dark until someone restarted it; a paused
tower costs some pictures and comes back alone.
## What was already safe, and what was not
| | Before | Now |
|---|---|---|
| Saving falls behind | The scan holds at its waypoint (`save_queue_max`), so frames never pile up in memory | Same, and a long hold now slows, then pauses the scan |
| CPU temperature | Read only by `test host` | Read every 2 s; on the Sensors panel, in the log, on MQTT `Health` |
| Throttling | Invisible (`ThermalInfo::throttled` was never set) | Logged when it starts, once a minute while it lasts, and when it ends |
| A hot, struggling CPU | Kept at full pace: the camera streaming, the encoder busy, the modem starved | Pictures spaced out (to fit the measured save time), then paused with the camera stream off |
| Slow 5G link | Each picture's CamEvent held the save worker up to 3 s | CamEvent and Health leave through their own queue and thread |
| Save workers | Same priority as the control loop, serial, IMU, MQTT | nice +5 (libjxl's threads inherit it) |
| Camera streaming, nothing saved | ~40% of one core on tower 111, always | Stopped while paused (buffer reuse was tried and measured no gain, so dropped) |
## The CPU guard
[ThermalGuard.h](../include/fgc/ThermalGuard.h). Every `sample_ms` (2 s) the
control loop reads the hottest thermal zone, the cores' clock, the kernel's
throttle counters and how busy the CPU was, plus whether saving is holding the
scan. The guard is pure - a reading in, a state and log lines out - so every
rule is unit-tested ([test_thermalguard.cpp](../tests/test_thermalguard.cpp)).
```
CPU >= slow_c (85), or throttling, or the scan waited for saving
25% of the last minute (or 30 s in one go)
NORMAL -----------------------------------------------------------> SLOWED
(as <-- 120 s all clear: CPU < 80, no throttling, waiting under pictures 2x further
configured) 12%, and saving fast enough for the normal pace --------- apart, or 1.5x the
| save time if longer
| CPU >= pause_c (92) for 20 s, or one 120 s wait for saving, |
| or saving still the bottleneck 120 s after slowing |
+--------------------------------> PAUSED <-------------------------+
gimbal parked where it is,
camera stream stopped, saving drains
|
| after 300 s, CPU <= resume_c (75)
v and nothing left to save
SLOWED
```
- **Slowed** stretches the interval (`CaptureScheduler::setIntervalScale`):
at least `slow_factor`, and as far as 1.5 × the measured save time per worker
when that is longer - so a CPU that saves slowly runs about two-thirds busy
instead of flat out. Fewer pictures per minute is less encoding and less motor
work. It returns to the normal pace only once that pace fits what saving
takes, so a saver that is simply too slow does not flap between the two.
- **Saving as the bottleneck** is judged by the share of each minute the scan
waits for it, not only by one long wait: a saver a little too slow holds the
scan 2-3 s at every waypoint and never once for long, yet keeps the CPU
pegged (see the results below).
- **Paused** finishes the waypoint under way, then starts no new move
(`setPaused`); once parked, the camera stream stops, so the CPU all but idles.
A snapshot an operator asks for is still taken.
- **Throttling** counts two ways: the kernel's `thermal_throttle` counters rising
(heat), or the clock below 50% of the chip's top while at least 50% busy, two
samples running. The second catches the power-limit slow-downs the counters
never count - and a clock capped by `cap-cpu.sh`.
- Missing readings (no thermal zone, no cpufreq) are left out of the rules, never
read as 0 °C or as hot.
- The mode does not change: StatusCode still says `0` for a paused auto scan.
Health says `paused`, and why.
All thresholds are `[Thermal]` keys, normally in `hardware.ini`
([configuration.md](configuration.md#when-the-computer-runs-hot-thermal)).
### Seeing it
- **Sensors panel** (`s`): a *LattePanda CPU* group - CPU temp, clock, load,
throttled - with a `HOT` / `THROTTLED` / `SLOWED` / `PAUSED` badge. The
expanded view adds the per-core clocks and any cap, load averages, throttle
counts, save time per picture, and the guard's state and reason.
- **Log**, in plain words:
```
CPU is slowing itself to stay cool: 94 °C, running at 1.2 GHz of 3.6 GHz
CPU still slowed: 14 times in the last minute, 94 °C, 1.1 GHz
CPU at 94 °C for 20 s: pausing pictures for at least 5 min, until the CPU is at 75 °C or below and saving has caught up
camera stream stopped while pictures are paused
CPU at 72 °C: pictures resume, one picture every 10.0 s instead of 5.0 until it has been calm for 2 min
Saving kept the scan waiting 27% of the last minute: one picture every 6.9 s instead of 2.0 until saving is fast enough for the normal pace, for 2 min
CPU no longer slowed: 72 °C
saving: 28 pictures in the last minute, 653 ms each on average (longest 817 ms)
```
- **MQTT** `GGS/FWT/<tower>/Health`, retained ([mqtt-api.md](mqtt-api.md#health-payload)).
- **Console / MQTT Command**: `thermal` (readings and pace), `thermal resume`,
`thermal off|on`. Allowed in every mode.
## Simulating it
[tools/thermal-sim/](../tools/thermal-sim/):
| Script | What it does | Root |
|---|---|---|
| `fake-temp.sh <root> init\|<C> [throttle]\|clock <MHz>\|ramp ...` | A fake `/sys` tree (busy % stays the real `/proc/stat`); the program reads it with `[Thermal] sysfs_root = <root>` | no |
| `cap-cpu.sh <MHz>\|restore` | Caps every core's clock (`scaling_max_freq`) | yes |
| `cap-power.sh <W>\|restore` | Caps package power (RAPL), like the 6 W TDP mode | yes |
| `heat-load.sh <cores> <secs>` | Busy cores competing with the program | no |
With `--mock-camera-full` (or `[Features] mock_camera_size = full`) the mock
camera delivers the real camera's 5328×3040 12-bit raw frames, so the program
saves - and loads the CPU - as on a tower, on any machine:
```
fire_gimbal_control -c sim/config.ini --mock-camera-full --mock-serial --no-mqtt
```
A CPU that cannot keep up, without root: `systemd-run --user --scope -p CPUQuota=50% fire_gimbal_control ...`.
## Results on the desk (2026-10-08)
i5-7300U (4 threads, 3.5 GHz - near the IOTA's N150), mock gimbal, tower-size
12-bit mock frames, `image_interval = 2`, short guard timings for the test
(`pause_after_s 10`, `calm_s 20`, `min_pause_s 30`, `held_pause_s 60`). A save
took 0.65-0.85 s.
**A. Heat (fake temperatures, real program).** `fake-temp.sh` walked the CPU
70 → 88 → 94 (+ throttle events) → 72 °C while the scan ran:
| Time | CPU | What the program did | Picture spacing |
|---|---|---|---|
| 0:00 | 70 °C | normal | 2.1-2.2 s |
| +0:01 | 88 °C | slowed (within one sample) | 4.2 s |
| +0:30 | 94 °C, throttling | "CPU is slowing itself to stay cool" at once; paused after 11 s, camera stream off 2 s later once the waypoint finished | none for 29 s |
| +1:11 | 72 °C | resumed, still slowed, after the 30 s minimum pause | 4.2 s |
| +2:09 | 72 °C | "CPU no longer slowed" one quiet minute after the last throttle event | 4.2 s |
| +2:51 | 72 °C | back to the normal pace after 20 s calm | 2.1 s |
No waypoint skipped, no overrun warnings, clean shutdown, 68 pictures.
**B. A CPU that cannot keep up (no heat).** The program held to half of one core
(`CPUQuota=50%`), so a save took 4.5-5 s against the 2 s interval:
- *Before* the share rule: the save gate held the scan 2-3 s at every waypoint,
pictures came every ~4.6 s, and the CPU sat **pegged for all 5 minutes**. The
guard never acted - no single wait reached 30 s - and the log said "saving is
behind / caught up" twice per picture.
- *After:* "Saving kept the scan waiting 27% of the last minute: one picture every
6.9 s instead of 2.0" after the first minute; from then on no holds at all and
the spacing followed the save time (7-10 s), stable for the rest of the run -
no flapping back to the 2 s pace it could not hold, no pause needed. The hold
warning is now said once, not at every waypoint.
The dashboard was checked in a real terminal: the Sensors panel's *LattePanda
CPU* group (88.0 °C, 2.8 GHz, 37 %, "yes, 2x last min", `SLOWED`) and the
expanded view's CPU section.
## Results on tower 111 (3 Delta, N5105, 2026-10-08)
Ambient: the enclosure sensor read 21 °C. The tower program was stopped for the
benchmark and replaced by the test build for the run; the tower's own build and
config were not touched. Raw data: `tools/camera-bench/results-2026-10-08-tower111-throttle/`.
### What a throttled Delta can sustain (`raw12_bench`)
The synthetic 5328×3040 12-bit frame (`make_synthetic.py`), edge-aware
de-Bayer, JPEG XL distance 1.0 effort 3, the tower's 1 worker × 4 threads. Each
condition: 60 s flat out, then 120 s at one picture per 2 s. `cap-cpu.sh` and
`cap-power.sh` set the limits; `stress-ng --cpu 2` is "2 busy cores".
| Condition | Save time | Most per second | CPU-s / picture | Pile-up after 2 min at one per 2 s | Temp mean / max |
|---|---|---|---|---|---|
| none (2.9 GHz) | 1.0 s | 0.98 | 2.3 | 0 | 63 / 80 °C |
| clock capped 1.6 GHz | 1.7 s | 0.58 | 4.0 | 0 | 57 / 59 °C |
| clock capped 1.2 GHz | 2.2 s | 0.45 | 5.2 | 6 frames (latency to 14.6 s) | 56 / 58 °C |
| clock capped 0.8 GHz | 3.3 s | 0.30 | 7.6 | 23 frames (latency to 51 s) | 54 / 56 °C |
| power capped 6 W | 1.0 s | 0.87 | 2.4-2.7 | 0 | 61 / 74 °C |
| 2 busy cores | 1.4 s | - | 2.3 | 0 | 79 / **88** °C |
| 0.8 GHz + 2 busy cores | 4.4 s | 0.22 | 6.9 | 33 frames (latency to 69 s) | 52 / 54 °C |
- At the tower's real pace, a picture every **5 s**, it keeps up in every case:
at 0.8 GHz saving takes two-thirds of the time, and with 2 busy cores on top
(4.4 s per save) it is on the edge - where the guard's headroom rule would
space pictures to ~6.6 s.
- The bare benchmark has no brake, so a slow CPU piles frames up (23-33 in two
minutes, ~24 MB each). In the program the save gate stops that and the guard
then slows the scan.
- The 6 W power cap hardly matters: saving draws only 6-8 W of the 10 W limit.
- The hottest run was competing load, not saving: 88 °C with 2 busy cores.
- **The kernel never counted a throttle event** in any run (all counters 0) -
21 °C was not hot enough. The counters exist on this kernel, so real heat
will be counted; the clock caps here are seen by the clock rule instead.
- Tower 111's camera is a 1800 U-1240c, 4024×3036: its real saves are 0.64-0.68 s,
about two-thirds of the synthetic frame's.
### The program on the tower, CPU capped to 0.8 GHz with 2 busy cores
The test build with the tower's own config, camera, gimbal and local MQTT
broker: 3 min normal, 6 min capped + loaded, 3 min restored.
| Time | What happened | Pictures | Save time |
|---|---|---|---|
| 16:57 | homed, auto scan, Health `normal`, CPU 51 °C | one per 5 s | 0.64-0.68 s |
| 16:59:32 | clock capped, 2 cores busy | | |
| 16:59:36 | "CPU is running slow while busy (judged from its clock...): 53 °C, running at 0.8 GHz of 2.9 GHz"; Health `slowed` | one per 10 s | 3.6-3.8 s |
| every minute | "CPU still slowed: clock low all minute" | | |
| 17:05:32 | cap lifted | | 0.64-0.72 s |
| 17:06:31 | "CPU no longer slowed" | one per 10 s | |
| 17:08:31 | "back to the normal picture pace"; Health `normal` | one per 5 s | |
The scan never waited for saving (`held_s` 0 throughout), the CPU ran 66-91%
busy while capped and loaded, and the program shut down cleanly; 34 pictures,
13 Health messages on `GGS/FWT/dev/Health`.
### What the camera stream costs on its own
Manual mode, one snapshot to start the stream, then 60 s of the program's own
CPU time with nothing saved (`stream_cost.sh`):
| Build | CPU while streaming |
|---|---|
| installed (main) | 39.2% of one core |
| reusing the frame buffer instead of a fresh 24 MB per frame | 40.8% of one core |
Reusing the buffer made no difference, so it was dropped. The stream itself
costs **about 40% of one core - a tenth of the computer - all the time it runs**,
whether or not pictures are saved: the program copies every frame it streams.
That is why a pause stops the stream. A lower streaming frame rate, or copying
only the frame that is kept, would cut it while scanning too; both touch the
fragile camera stream, so they are left for a separate change.
### Found on the way
- `x86_pkg_temp` is not the only thermal zone on the towers: tower 111 also lists
the Wi-Fi card (`iwlwifi_1`) and `acpitz`. The guard now reads the CPU package
zone, so a hot Wi-Fi card cannot pause the scan; `test host` still reports the
hottest zone.
- Tower 111's camera refuses its profile at every start since 2026-10-07 17:22
("applying profile 'standard' ... failed (error -39)"), with the installed
build as with this one, and captures anyway. Unrelated to this work; worth a
look.
## Still to do
- Tower 121 (the IOTA), when cleared: the same matrix and program run.
- A real hot day: the clock caps stand in for throttling; the kernel counters
have not yet seen any.

View File

@ -714,6 +714,53 @@ feature vocabularies differ, so copying another tower's XML does not work:
| `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** | | `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** |
| Sensor | 12 MP | 16 MP, 5328x3040 | | Sensor | 12 MP | 16 MP, 5328x3040 |
### Older GigE cameras (Manta) `[partial]`
Rietschen has three Allied Vision Manta G-1236 on one GbE port (`enp1s0`,
MTU 8000): RGB (G-1236C), NIR and ACR (both G-1236B). Their firmware
(00.01.54) predates the names the program and the Alvium use:
| Program / Alvium | Manta |
|---|---|
| `ExposureTime` | `ExposureTimeAbs` |
| `AcquisitionFrameRate` (+ `…Enable`) | `AcquisitionFrameRateAbs` - which a free-running Manta runs at, up to `AcquisitionFrameRateLimit` |
| `IntensityControllerTarget` / `…Tolerance` | `ExposureAutoTarget` / `ExposureAutoAdjustTol` |
| `DeviceLinkThroughputLimit` | `StreamBytesPerSecond` (max 124 MB/s, shared by every camera on the port) |
| `BayerRG12p` | `BayerRG12Packed`: the GigE Vision 1.x packing, p0's high bits first - its own unpacking, see `RawImage.h` |
| chunk data, `DeviceReset` | none |
The program looks a missing feature up under its older name, so `test camera`
and the telemetry work. `export_camera_profile.py` picks its `gige` recipe by
itself when a camera has `ExposureTimeAbs` and no `ExposureTime`: frame trigger
off (the legacy cameras booted with `TriggerMode = On`, `Line1`), `BayerRG8` on
the colour camera (`BayerRG12Packed`, 12 bits, when it has it) and `Mono8` on a
mono one, 60 MB/s each, the frame rate at
the camera's maximum (rietschen's RGB camera came set to 0.955 fps), and
auto-exposure on the frame mean (`ExposureAutoAlg = Mean`) at 30 % +- 5 %, down
to 20 us by day. The keep rule waits for the frame mean to reach the camera's
own target and tolerance, read once the camera is open. With the RGB camera's
`FitRange`, which fits the histogram wherever its mean lands, no waypoint
settled. 30 rather than the Alvium's 50: at 50 the sky over the horizon burnt
out at noon, where smoke would rise, and at 35 it was still too bright; the old
program ran these cameras at 23-25.
Give `--camera` the id: with several cameras the first one the SDK lists is
not the colour one, and the order changes from run to run. `install.sh
--probe` lists them all, `install.sh --hardware-ini` writes `CHOOSE_ONE`
without `--camera-id`, and `install-tower` asks (`camera_id`).
What is still missing: a Manta has no `DeviceReset`, so the program cannot
reset a stuck camera (it says so and goes on); and without chunk data the keep
rule cannot tell a dark scene at the exposure limit, so at night it keeps the
newest frame after `frame_timeout_ms`.
`[verified]` on rietschen's NIR camera, 2026-10-20, with the motor mocked and
the old program running beside it: `test camera` PASS (5 frames 4112x3008
Mono8 in 1.7 s, dark 100 % <= 20, bright mean 255), `snapshot` writes a
3008x4112 grey JXL (`rotation_deg = 270`, as the old program rotated), and a
scan keeps a picture every 5 s without a timeout. The colour camera is not yet
tested: the old program has it open.
### Binning is not optional, and the frame-rate ceiling is not what it looks like ### Binning is not optional, and the frame-rate ceiling is not what it looks like
Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB
@ -884,6 +931,89 @@ Then `mode auto`, and confirm `.jxl` files appear under the configured output di
> running an older binary, export the profiles (step 8) first or it will refuse > running an older binary, export the profiles (step 8) first or it will refuse
> with `no camera profiles (*.xml)`. > with `no camera profiles (*.xml)`.
### Phase 2b - thermal camera
Only for a tower with an InfraTec TarisIR mini (tower 111 since 2026-10). The
program runs as the tower's user; the SDK needs three things from the system.
1. **SDK and licence.** Packages `infratec-sdk-irbgrab` and
`infratec-itcamtarisirmini` (amd64), then the licence for the camera's serial
via the vendor's `copycustomerdata.sh` into `/var/lib/infratec/.license/`
(`root:infratec`, `rw-rw----`). **Repeat after every OS update.** Keep the
source files on the tower (`~/InfraTec/LINUX/` on 111) so it can be re-run.
The tower's user must be in group `infratec` (`id` shows it; `sudo usermod -aG
infratec ggs` and log in again otherwise). The library must be in the ld
cache: `ldconfig -p | grep irbgrab`.
2. **Network.** Camera and port directly cabled, static address, **no DHCP**
(`dhcp4: false` in netplan: DHCP on a link without a server keeps failing and
takes the static address with it), **MTU 1500** - the camera does no jumbo
frames. The SDK finds the camera by serial and MAC on raw Ethernet; the IP
only matters to ordinary GigE Vision tools. On 111: `enp1s0`, 192.168.0.111,
camera 192.168.0.200.
3. **`cap_net_raw` on the binary.** The SDK opens a raw `AF_PACKET` socket.
Rather than running the program as root, the binary gets this one
capability. A rebuild drops it, so `deploy.sh` and `install-tower` run
`scripts/fgc-setcap --if-thermal` after every build. Once per tower, let
deploy do that without a password:
```bash
scripts/fgc-setcap --install-sudoers # /etc/sudoers.d/fire-gimbal-control-setcap
scripts/fgc-setcap && getcap build/fire_gimbal_control
```
A capability binary runs in the loader's secure mode (`LD_*` ignored, no core
dumps): check once that Vimba X still opens the colour camera, and that
`scripts/fgc status` and `stop` still find the process.
Then hardware.ini (`driver = taris`, `id = <serial>`, `interface = enp1s0`,
`cameras = rgb, tir`) and config.ini (`label = TIR`, see
[configuration.md](configuration.md#thermal-camera-driver--taris)), and a run
with the real cameras but no motors:
```bash
./build/fire_gimbal_control --mock-serial --mock-env --no-mqtt --no-tui
```
```
[INFO ] IRBGRAB SDK 4.3.44
[INFO ] thermal camera connected via SN:2017744|...|192.168.0.200#0
[INFO ] thermal camera: profile 0, 0.01 K per count
```
`snapshot` then writes `RGB/<t>.jxl` and `TIR/<t>.jxl` with the same `<t>`.
`camera calib` reports the profile. If the camera stays offline the log says
why, once per change - `no cap_net_raw`, `not found (found: ...)`, the port's
state and MTU. A `0x80000017` that keeps coming is the licence before it is the
NUC.
Measured on tower 111 (2026-10-14, SDK 4.3.44, SN 2017744):
- `cap_net_raw` alone is enough; no root, no `cap_net_admin`. Vimba X still
opens the colour camera from the capability binary.
- Connecting takes about 15 s (SDK init and search); the scan does not wait.
- `Framerate_Hz` is settable. The LUT is empty for the first frame after
connecting and right from then on: 0.0099998 K/count (profile 0), 0.0199995
(profile 1).
- 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.
- 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.
`NUC_INPROGRESS` stays 0 throughout, also for those. The program now runs the
NUC itself at the start of a move (`nuc_interval_s`).
- The SDK switches the process's C locale to `LC_NUMERIC=de_DE` when it is
loaded. The program puts it back after every SDK call and writes its JSON
without the C library's number formatting.
### Phase 3 - the broker ### Phase 3 - the broker
Set `[Features] enable_mqtt = true` and point `zkms_server_ip` at the broker. Set `[Features] enable_mqtt = true` and point `zkms_server_ip` at the broker.

View File

@ -100,6 +100,9 @@ probe_mcu() {
# therefore taken only from a block whose transport layer is the USB or GigE # therefore taken only from a block whose transport layer is the USB or GigE
# TL. A tower configured with DEV_Cam1 would "find its camera" and capture # TL. A tower configured with DEV_Cam1 would "find its camera" and capture
# synthetic images forever. # synthetic images forever.
#
# One id per line, like probe_mcu: rietschen has three GigE cameras on one port,
# and the first one listed is not the colour one, so the caller has to choose.
probe_camera_id() { probe_camera_id() {
local tool local tool
for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do
@ -113,7 +116,7 @@ probe_camera_id() {
done done
"$tool" 2>/dev/null | awk ' "$tool" 2>/dev/null | awk '
/Camera ID/ { id = $NF } /Camera ID/ { id = $NF }
/Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) { print id; exit } } /Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) print id; id = "" }
' '
return return
done done
@ -134,7 +137,8 @@ probe_camera_lpm_path() {
probe_report() { probe_report() {
local board mcu cam lpm local board mcu cam lpm
board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)"; lpm="$(probe_camera_lpm_path)" board="$(probe_board)"; mcu="$(probe_mcu)"; lpm="$(probe_camera_lpm_path)"
cam="$(probe_camera_id | paste -sd ' ')"
echo "board : $board" echo "board : $board"
echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))" echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))"
echo "motor MCU : ${mcu:-NOT FOUND}" echo "motor MCU : ${mcu:-NOT FOUND}"
@ -270,7 +274,7 @@ do_vimba() {
local id; id="$(probe_camera_id)" local id; id="$(probe_camera_id)"
if [[ -n $id ]]; then if [[ -n $id ]]; then
say "camera found: $id" say "camera found: ${id//$'\n'/, }"
else else
warn "no camera reported by ListCameras yet" warn "no camera reported by ListCameras yet"
warn "the SDK asks for a reboot before the transport layers are usable" warn "the SDK asks for a reboot before the transport layers are usable"
@ -374,6 +378,11 @@ emit_hardware_ini() {
warn "$mcu" warn "$mcu"
mcu="CHOOSE_ONE" mcu="CHOOSE_ONE"
fi fi
if [[ $(wc -l <<< "$cam") -gt 1 ]]; then
warn "several cameras found; pick one with --camera-id:"
warn "$cam"
cam="CHOOSE_ONE"
fi
cat <<EOF cat <<EOF
; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh ; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh

View File

@ -21,6 +21,7 @@ struct RuntimeOptions {
// Tri-state feature overrides (unset => use the config value). // Tri-state feature overrides (unset => use the config value).
std::optional<bool> use_mqtt; std::optional<bool> use_mqtt;
std::optional<bool> mock_camera; std::optional<bool> mock_camera;
std::optional<bool> mock_camera_full; // --mock-camera-full: tower-size 12-bit frames
std::optional<bool> mock_serial; std::optional<bool> mock_serial;
std::optional<bool> mock_env; // implies [Features] enable_env std::optional<bool> mock_env; // implies [Features] enable_env
std::optional<bool> use_tui; // --tui / --no-tui; unset => [UI] enable_tui std::optional<bool> use_tui; // --tui / --no-tui; unset => [UI] enable_tui

View File

@ -86,6 +86,27 @@ public:
void setSaveGate(std::function<bool()> can_save) { can_save_ = std::move(can_save); } void setSaveGate(std::function<bool()> can_save) { can_save_ = std::move(can_save); }
// True while the scan is held for saving to catch up. // True while the scan is held for saving to catch up.
bool holdingForSave() const { return holding_; } bool holdingForSave() const { return holding_; }
// All the time spent held for saving since construction, the current hold
// included (ms). Only rises; the CPU guard takes its share of each minute.
long long heldForSaveTotalMs() const;
// The CPU guard (ThermalGuard) pacing the scan to the computer's heat.
// Interval scale: pictures this many times further apart (>= 1; 1 = as
// configured). Takes effect from the next picture.
void setIntervalScale(double scale);
double intervalScale() const { return interval_scale_; }
// Paused: the waypoint under way finishes, then no new move starts, and in
// manual no continuous-capture frame is kept - the gimbal stays where it is.
// A requested snapshot is still taken: an operator asked for it. Resuming
// carries on from the next waypoint with no overrun warning for the wait.
void setPaused(bool paused);
bool paused() const { return paused_; }
// Paused and nothing under way: what the camera stream may stop for.
bool pausedAtRest() const {
return paused_ && !moving_ && !snapshot_pending_ && continuous_hold_.due_ms == 0;
}
// The current interval between pictures, scale included. 0 = never.
double intervalMs() const { return interval_ms_; }
// Per-camera-profile timing (see CameraProfile): how long to wait after standstill // Per-camera-profile timing (see CameraProfile): how long to wait after standstill
// for the exposure to settle, and how long to keep retrying before abandoning // for the exposure to settle, and how long to keep retrying before abandoning
@ -166,8 +187,15 @@ private:
std::function<long long()> now_ms_; std::function<long long()> now_ms_;
std::function<bool()> can_save_; std::function<bool()> can_save_;
bool holding_ = false; bool holding_ = false;
long long hold_started_ms_ = 0;
long long held_total_ms_ = 0;
long long last_hold_end_ms_ = -1;
bool hold_logged_ = false;
double image_rate_; double image_rate_;
double interval_ms_; // 1000 / image_rate_; 0 = never double base_interval_ms_; // 1000 / image_rate_; 0 = never
double interval_ms_; // base_interval_ms_ x interval_scale_
double interval_scale_ = 1.0;
bool paused_ = false;
long long timer_start_ = 0; // the last frame kept or given up, or the mode start long long timer_start_ = 0; // the last frame kept or given up, or the mode start
Mode mode_ = Mode::Manual; Mode mode_ = Mode::Manual;

View File

@ -17,6 +17,8 @@ namespace fgc {
// The tower's mode, out (0 auto, 1 pan, 2 manual - see OperatingMode.h). // The tower's mode, out (0 auto, 1 pan, 2 manual - see OperatingMode.h).
inline constexpr const char* kTopicStatusCode = "StatusCode"; inline constexpr const char* kTopicStatusCode = "StatusCode";
inline constexpr const char* kTopicCamEvent = "CamEvent"; inline constexpr const char* kTopicCamEvent = "CamEvent";
// The computer's heat and load, and whether the scan is slowed or paused for it.
inline constexpr const char* kTopicHealth = "Health";
// Any console command, in; its answer, out. See docs/mqtt-api.md. // Any console command, in; its answer, out. See docs/mqtt-api.md.
inline constexpr const char* kTopicCommand = "Command"; inline constexpr const char* kTopicCommand = "Command";
inline constexpr const char* kTopicCommandReply = "CommandReply"; inline constexpr const char* kTopicCommandReply = "CommandReply";
@ -25,6 +27,7 @@ std::string towerTopic(const std::string& tower, const std::string& leaf);
// Payload builders. One definition each, used by both channels. // Payload builders. One definition each, used by both channels.
std::string camEventJson(const CamEvent& e); std::string camEventJson(const CamEvent& e);
std::string healthJson(const HealthReport& h);
// A Command payload: {"id":"...","cmd":"..."} (a flat JSON object of strings; // A Command payload: {"id":"...","cmd":"..."} (a flat JSON object of strings;
// other keys ignored), or a bare command line with no id - for testing from a // other keys ignored), or a bare command line with no id - for testing from a

View File

@ -0,0 +1,79 @@
#pragma once
#include "fgc/ICameraSource.h"
#include <memory>
#include <vector>
namespace fgc {
// Several camera sources behind one, so the scheduler and the pipeline keep
// seeing a single camera array: the colour cameras through Vimba and the
// thermal one through its own SDK take their pictures at the same waypoints
// and intervals without either knowing about the other.
//
// The first child leads. Its cameras come first in the index order (camera 0
// is its first camera, which the keep rule reads), and its trigger() decides
// whether a picture was taken: only once it has delivered are the others
// asked, so a retry while the leader waits never yields a second thermal
// frame for the same waypoint. The others are best effort - a follower with
// no fresh frame costs that picture its thermal image, nothing more.
//
// Every frame keeps its own time, when it reached the host: the thermal frame
// is the newest the camera had, so its time is up to max_frame_age_ms off the
// colour one, and that is what the file name and the CamEvent say.
//
// A follower that is briefly without a fresh frame (frameReady() false: the
// thermal camera's NUC) holds the picture back for up to `follower_wait_ms`,
// so the pair stays whole; past that the leader goes on alone.
//
// Indices are global: child k's camera i is offset(k) + i. Calls about one
// camera go to the child that has it.
//
// The offsets come from `counts`, how many cameras each child is configured
// with, not from cameraCount(): VimbaCameraSource counts what it has opened,
// which is 0 before open(), and a thermal frame numbered from that landed as
// camera 0 - the colour camera's label and file (seen on tower 111). Without
// counts, each child's cameraCount() at construction is taken.
class CompositeCameraSource : public ICameraSource {
public:
explicit CompositeCameraSource(std::vector<std::unique_ptr<ICameraSource>> children,
std::vector<int> counts = {}, int follower_wait_ms = 1000);
void open() override;
void close() override;
void start() override;
void stop() override;
bool trigger() override;
void moving() override;
bool frameReady() override;
bool setProfile(int camera_index, const CameraProfile& profile) override;
void setFrameCallback(FrameCallback cb) override;
uint64_t frameSeq() override;
std::vector<FrameStats> framesSince(uint64_t after) override;
bool readFeature(int camera_index, const std::string& name, double& value) override;
int cameraCount() const override;
std::vector<CameraDeviceInfo> deviceInfo() override;
std::string offline(int camera_index) override;
bool grabFrame(int camera_index, Frame& out, int timeout_ms) override;
bool setFeature(int camera_index, const std::string& name, const std::string& value) override;
bool featureRange(int camera_index, const std::string& name, double& min, double& max) override;
bool restoreProfile(int camera_index) override;
bool resetCamera(int camera_index) override;
size_t childCount() const { return children_.size(); }
ICameraSource& child(size_t k) { return *children_[k]; }
private:
// The child holding a global index and its index there; nullptr if none.
ICameraSource* route(int camera_index, int& local) const;
std::vector<std::unique_ptr<ICameraSource>> children_;
std::vector<int> counts_; // cameras each child is configured with
std::vector<int> offsets_; // global index of each child's camera 0
FrameCallback callback_;
int follower_wait_ms_;
long long waiting_since_ = 0; // steady ms a picture began waiting for a follower
};
} // namespace fgc

View File

@ -3,6 +3,7 @@
#include "fgc/ImuMount.h" #include "fgc/ImuMount.h"
#include "fgc/Geometry.h" #include "fgc/Geometry.h"
#include "fgc/OperatingMode.h" #include "fgc/OperatingMode.h"
#include "fgc/ThermalGuard.h"
#include <map> #include <map>
#include <string> #include <string>
@ -198,11 +199,14 @@ struct CameraProfileTiming {
struct CameraSpec { struct CameraSpec {
std::string name; // the block suffix, e.g. "rgb" std::string name; // the block suffix, e.g. "rgb"
bool enabled = true; bool enabled = true;
std::string driver = "vimba"; // only "vimba" is implemented // "vimba" (Allied Vision, colour) or "taris" (InfraTec TarisIR, thermal).
std::string id; // USB DEV_ id or GigE IP std::string driver = "vimba";
std::string id; // vimba: USB DEV_ id or GigE IP; taris: serial number
std::string label; // output subdir + CamEvent "cam"; defaults to name std::string label; // output subdir + CamEvent "cam"; defaults to name
std::string profile_dir; // relative => resolved next to config.ini std::string profile_dir; // vimba only; relative => resolved next to config.ini
std::string profile = "standard"; // which profile to start in // Which profile to start in. For vimba a settings XML in profile_dir; for
// taris only the name of a timing block, the camera having no settings file.
std::string profile = "standard";
int rotation_deg = 0; // clockwise; 0/90/180/270 int rotation_deg = 0; // clockwise; 0/90/180/270
// Encoding, defaulted from [Camera] and overridable per camera. // Encoding, defaulted from [Camera] and overridable per camera.
@ -211,6 +215,23 @@ struct CameraSpec {
// De-Bayering for raw profiles (BayerRG8/12p): "edge_aware" or "bilinear". // De-Bayering for raw profiles (BayerRG8/12p): "edge_aware" or "bilinear".
std::string demosaic = "edge_aware"; std::string demosaic = "edge_aware";
// ---- driver = taris only (TarisCameraSource.h) ----
// Operation (config.ini):
int calib_profile = 0; // camera calibration profile; sets the counts' scale
double frame_rate_hz = 5.0; // the camera free-runs at this rate, throttled
// A frame older than this at capture is not kept. Must stay below the scan's
// settle wait, or the frame may have been taken while the gimbal still moved.
int max_frame_age_ms = 400;
std::string library = "libirbgrab_x86_64-linux.so"; // the SDK, found through the ld cache
// Seconds between NUCs the program runs itself, each at the start of a move
// so it never blanks a picture; 0 leaves the camera's own timer, which fires
// mid-picture now and then (TarisCameraSource.h).
int nuc_interval_s = 30;
// Mounting (hardware.ini): the camera's network port, for diagnostics only.
std::string interface;
bool thermal() const { return driver == "taris"; }
std::map<std::string, CameraProfileTiming> timing; // by profile name std::map<std::string, CameraProfileTiming> timing; // by profile name
// Timing for `profile`, or the defaults when the block does not name it. // Timing for `profile`, or the defaults when the block does not name it.
@ -218,7 +239,10 @@ struct CameraSpec {
}; };
struct CameraConfig { struct CameraConfig {
std::vector<CameraSpec> cameras; // in config order; empty is a config error // Colour (vimba) cameras first, then thermal ones, each group in block-name
// order: camera 0 is the one the keep rule and the scan pace follow, and
// that must be a colour camera. Empty is a config error.
std::vector<CameraSpec> cameras;
// [Camera] defaults, inherited by each block unless it overrides them. // [Camera] defaults, inherited by each block unless it overrides them.
double jxl_distance = 0.8; double jxl_distance = 0.8;
int jxl_effort = 4; int jxl_effort = 4;
@ -244,6 +268,10 @@ struct FeaturesConfig {
bool enable_serial = true; bool enable_serial = true;
bool enable_imu = false; // Xsens MTi orientation/IMU (off by default) bool enable_imu = false; // Xsens MTi orientation/IMU (off by default)
bool mock_camera = false; // use a simulated camera instead of Vimba X bool mock_camera = false; // use a simulated camera instead of Vimba X
// The simulated camera's frames: false = small 640x480 colour (cheap), true =
// the real camera's 5328x3040 12-bit raw, so saving costs the CPU what it
// does on a tower (throttling simulations, tools/thermal-sim/).
bool mock_camera_full = false;
bool mock_serial = false; // use a simulated motor controller bool mock_serial = false; // use a simulated motor controller
bool mock_imu = false; // use a simulated IMU instead of the MTi bool mock_imu = false; // use a simulated IMU instead of the MTi
bool enable_env = false; // ambient temp/humidity sensor (off by default) bool enable_env = false; // ambient temp/humidity sensor (off by default)
@ -389,6 +417,7 @@ struct AppConfig {
PathsConfig paths; PathsConfig paths;
BoardConfig board; // [Board] from hardware.ini BoardConfig board; // [Board] from hardware.ini
HardwareConfig hardware; // [Hardware] from hardware.ini HardwareConfig hardware; // [Hardware] from hardware.ini
ThermalParams thermal; // [Thermal] the CPU guard, normally from hardware.ini
FeaturesConfig features; FeaturesConfig features;
LoggingConfig logging; LoggingConfig logging;
UiConfig ui; // [UI] terminal dashboard toggle UiConfig ui; // [UI] terminal dashboard toggle

View File

@ -1,18 +1,58 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector>
namespace fgc::hostmetrics { namespace fgc::hostmetrics {
// Host (LattePanda / Linux) health reads for the `test host` subsystem. Pure // Host (LattePanda / Linux) health reads, for the `test host` subsystem and the
// parsers are split from the live readers so they unit-test without a real /sys // running program's CPU watch (ThermalGuard). Pure parsers are split from the
// or /proc. All readers degrade gracefully (ok=false) rather than throw. // live readers so they unit-test without a real /sys or /proc. All readers
// degrade gracefully (ok=false) rather than throw.
//
// `root` is put in front of every /sys and /proc path ("/" = the real ones), so
// a test or a throttling simulation can hand in a fake tree:
// <root>/sys/class/thermal/thermal_zone*/{temp,type}
// <root>/sys/devices/system/cpu/cpu*/cpufreq/{scaling_cur_freq,cpuinfo_max_freq,scaling_max_freq}
// <root>/sys/devices/system/cpu/cpu*/thermal_throttle/{package,core}_throttle_count
// <root>/proc/stat (when the tree has none, the real /proc/stat)
struct ThermalInfo { struct ThermalInfo {
bool ok = false; bool ok = false;
double max_temp_c = 0.0; // hottest thermal zone double max_temp_c = 0.0; // hottest thermal zone
std::string hottest_zone; // e.g. "x86_pkg_temp" std::string hottest_zone; // e.g. "x86_pkg_temp"
bool throttled = false; // best-effort (false if unknown) bool throttled = false; // best-effort (false if unknown)
// The CPU's own zone, for the CPU guard: x86_pkg_temp where there is one,
// else the hottest. Not simply the hottest: the towers also list the Wi-Fi
// card (iwlwifi_1) and the ACPI zone, and the guard watches the CPU.
double cpu_temp_c = 0.0;
std::string cpu_zone;
};
// CPU clock, from cpufreq. MHz.
struct CpuFreqInfo {
bool ok = false;
double mean_mhz = 0.0; // mean of scaling_cur_freq over the cores
double hw_max_mhz = 0.0; // cpuinfo_max_freq: what the chip can do
double cap_mhz = 0.0; // lowest scaling_max_freq: what it may do now
std::vector<double> core_mhz; // per core, cpu0 first
};
// The kernel's count of times the CPU slowed itself because it ran too hot
// (x86 thermal_throttle). It only rises; a rise between two reads is throttling.
// Slow-downs to stay inside the power limit are NOT counted here - ThrottleWatch
// (ThermalGuard.h) also watches the clock for those.
struct ThrottleCount {
bool ok = false;
long long events = 0; // package count (cpu0) + every core's count
};
// Cumulative CPU time from the first line of /proc/stat, in clock ticks. Busy %
// is the change in `busy` over the change in `total` between two reads.
struct CpuTimes {
bool ok = false;
long long busy = 0;
long long total = 0;
}; };
struct DiskInfo { struct DiskInfo {
@ -37,7 +77,10 @@ struct LoadInfo {
}; };
// ---- live readers ---- // ---- live readers ----
ThermalInfo readThermal(); // scans /sys/class/thermal ThermalInfo readThermal(const std::string& root = "/"); // scans sys/class/thermal
CpuFreqInfo readCpuFreq(const std::string& root = "/");
ThrottleCount readThrottleCount(const std::string& root = "/");
CpuTimes readCpuTimes(const std::string& root = "/");
DiskInfo readDisk(const std::string& path); // statvfs() on the filesystem holding `path` DiskInfo readDisk(const std::string& path); // statvfs() on the filesystem holding `path`
MemInfo readMeminfo(); // /proc/meminfo MemInfo readMeminfo(); // /proc/meminfo
LoadInfo readLoad(); // getloadavg() + nproc LoadInfo readLoad(); // getloadavg() + nproc
@ -45,5 +88,8 @@ LoadInfo readLoad(); // getloadavg() + nproc
// ---- pure parsers (reused by the readers; exposed for tests) ---- // ---- pure parsers (reused by the readers; exposed for tests) ----
MemInfo parseMeminfo(const std::string& proc_meminfo); MemInfo parseMeminfo(const std::string& proc_meminfo);
double parseMilliCelsius(const std::string& zone_temp_contents); // "52000\n" -> 52.0 double parseMilliCelsius(const std::string& zone_temp_contents); // "52000\n" -> 52.0
CpuTimes parseProcStatCpu(const std::string& proc_stat); // its "cpu " line
// Busy percentage between two reads; -1 when either is bad or no time passed.
double busyPercent(const CpuTimes& before, const CpuTimes& after);
} // namespace fgc::hostmetrics } // namespace fgc::hostmetrics

View File

@ -3,9 +3,11 @@
#include "fgc/CameraProfile.h" #include "fgc/CameraProfile.h"
#include "fgc/RawImage.h" #include "fgc/RawImage.h"
#include "fgc/SettleRule.h" #include "fgc/SettleRule.h"
#include "fgc/ThermalStats.h"
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@ -32,6 +34,8 @@ struct Frame {
// Sampled from `data` on arrival (see RawImage.h); invalid when the layout // Sampled from `data` on arrival (see RawImage.h); invalid when the layout
// is Unknown. // is Unknown.
BrightnessSample brightness; BrightnessSample brightness;
// Set by a thermal camera only: how to read its Mono16 counts.
std::optional<ThermalFrameInfo> thermal;
}; };
// Live per-camera identity, sensor telemetry, and acquisition stats, for the // Live per-camera identity, sensor telemetry, and acquisition stats, for the
@ -82,6 +86,16 @@ public:
// yet, and the caller simply retries. // yet, and the caller simply retries.
virtual bool trigger() = 0; virtual bool trigger() = 0;
// The gimbal has started moving: nothing will be captured until it stands
// still again and has settled. A camera with housekeeping that blanks its
// stream - the thermal camera's NUC - does it now. Default: nothing.
virtual void moving() {}
// Would trigger() hand over a fresh frame right now? False while a camera is
// briefly between frames (a NUC), so a picture can wait a moment for it
// rather than go without. Default: always.
virtual bool frameReady() { return true; }
// Apply a profile to one camera, identified by its index in the order the // Apply a profile to one camera, identified by its index in the order the
// source opened them (the same index Frame::cam_id carries). The backend // source opened them (the same index Frame::cam_id carries). The backend
// loads the profile's settings XML through its SDK - see CameraProfile.h. // loads the profile's settings XML through its SDK - see CameraProfile.h.
@ -113,6 +127,12 @@ public:
// Default: none. Implementations may refresh a throttled cache, so non-const. // Default: none. Implementations may refresh a throttled cache, so non-const.
virtual std::vector<CameraDeviceInfo> deviceInfo() { return {}; } virtual std::vector<CameraDeviceInfo> deviceInfo() { return {}; }
// Why one camera delivers nothing right now ("not found on the network"),
// or empty while it works. For a camera the scan can do without (the
// thermal one), which reconnects by itself rather than stopping the run.
// Default: always working.
virtual std::string offline(int /*camera_index*/) { return {}; }
// ---- For `test camera` only ------------------------------------------------ // ---- For `test camera` only ------------------------------------------------
// The camera's configuration otherwise changes through setProfile() and // The camera's configuration otherwise changes through setProfile() and
// nothing else; these let the self-test drive a camera to known extremes and // nothing else; these let the self-test drive a camera to known extremes and

View File

@ -1,17 +1,58 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
namespace fgc { namespace fgc {
// What a thermal camera's CamEvent carries beyond the common fields (see
// docs/mqtt-api.md). Temperatures are apparent (emissivity 1), in tenths of a
// degree Celsius; the hot spot is normalised 0..1 across the stored image.
struct ThermalEventFields {
int cal = 0; // calibration profile: says how to read the counts
int missed = 0; // GigE packets lost for this frame
bool stats_ok = false; // false: no valid pixel, the four below go out as null
int tmax_decic = 0;
int tmed_decic = 0;
double hotx = 0.0;
double hoty = 0.0;
};
// Notification published after an image is captured and saved. // Notification published after an image is captured and saved.
struct CamEvent { struct CamEvent {
std::string tower; // tower / FWT name std::string tower; // tower / FWT name
std::string camera; // "RGB" / "ACR" / "NIR" std::string camera; // "RGB" / "TIR" / "ACR" / "NIR"
int heading_decideg = 0; // yaw heading * 10 (one decimal as integer) int heading_decideg = 0; // yaw heading * 10 (one decimal as integer)
int pitch_decideg = 0; // pitch elevation * 10 (one decimal as integer) int pitch_decideg = 0; // pitch elevation * 10 (one decimal as integer)
long long timestamp_ms = 0; // Unix epoch ms; matches the image filename long long timestamp_ms = 0; // Unix epoch ms; matches the image filename
std::optional<ThermalEventFields> thermal; // thermal cameras only
};
// The tower computer's health, on GGS/FWT/<tower>/Health (retained): the CPU
// guard's state and the readings behind it (ThermalGuard.h). A reading that is
// unknown goes out as null.
struct HealthReport {
std::string tower;
std::string state = "normal"; // normal / slowed / paused
std::string reason; // why not normal, in plain words; "" when normal
bool guard_on = true; // false after `thermal off`
bool cpu_c_ok = false;
double cpu_c = 0.0;
bool cpu_mhz_ok = false;
double cpu_mhz = 0.0;
double cpu_busy_pct = -1.0; // -1 = unknown
bool throttling = false;
long long throttle_events_last_min = 0;
bool enclosure_c_ok = false;
double enclosure_c = 0.0;
int save_count_last_min = 0;
double save_ms_mean = 0.0;
double save_ms_max = 0.0;
std::string cams_offline; // labels of cameras not delivering, comma-separated
long long held_s = 0; // the scan waiting for saving, seconds
double interval_s = 0.0; // the time between pictures now
long long timestamp_ms = 0; // Unix epoch ms
}; };
// A console command that arrived over MQTT, on GGS/FWT/<tower>/Command. It is // A console command that arrived over MQTT, on GGS/FWT/<tower>/Command. It is
@ -75,6 +116,8 @@ public:
// StatusCode - on every change, and again on every reconnect. // StatusCode - on every change, and again on every reconnect.
virtual void publishStatus(int code) = 0; virtual void publishStatus(int code) = 0;
virtual void publishCamEvent(const CamEvent& event) = 0; virtual void publishCamEvent(const CamEvent& event) = 0;
// The computer's health (HealthReport), retained. Default: dropped.
virtual void publishHealth(const HealthReport& /*report*/) {}
// Commands received since the last call, oldest first; clears them. And the // Commands received since the last call, oldest first; clears them. And the
// answer to one. Defaults: none / dropped, for a channel that takes no commands. // answer to one. Defaults: none / dropped, for a channel that takes no commands.

View File

@ -5,6 +5,7 @@
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
@ -25,7 +26,9 @@ struct Orientation {
// Camera.rotation_deg, encode to JPEG XL, write to // Camera.rotation_deg, encode to JPEG XL, write to
// <output_dir>/<label>/<timestamp>.jxl, and publish a CamEvent. Raw frames // <output_dir>/<label>/<timestamp>.jxl, and publish a CamEvent. Raw frames
// (BayerRG8, BayerRG12p) are de-Bayered first and saved at their own bit depth, // (BayerRG8, BayerRG12p) are de-Bayered first and saved at their own bit depth,
// so a 12-bit frame becomes a 12-bit file. Runs background workers so encoding // so a 12-bit frame becomes a 12-bit file. Thermal counts (Mono16) are saved
// as they are, 16-bit and always lossless, and their CamEvent carries the
// frame's temperature figures (ThermalStats.h). Runs background workers so encoding
// never blocks acquisition. Split out of the old VimbaHandler::SaveImage so any // never blocks acquisition. Split out of the old VimbaHandler::SaveImage so any
// camera source (real or mock) can feed it. // camera source (real or mock) can feed it.
class ImagePipeline { class ImagePipeline {
@ -88,10 +91,26 @@ public:
// Last CamEvent published (camera label, heading/pitch, timestamp), for the // Last CamEvent published (camera label, heading/pitch, timestamp), for the
// UI's camera panel. Thread-safe; nullopt until the first capture is saved. // UI's camera panel. Thread-safe; nullopt until the first capture is saved.
std::optional<CamEvent> lastEvent() const; std::optional<CamEvent> lastEvent() const;
// The same for the last thermal picture only (its event carries `thermal`).
std::optional<CamEvent> lastThermalEvent() const;
// Number of images successfully written this session (for the camera view). // Number of images successfully written this session (for the camera view).
long long imagesSaved() const { return saved_.load(); } long long imagesSaved() const { return saved_.load(); }
// How long saving took over the last minute: de-Bayer, encode and write, per
// picture (the MQTT publish after it is not included). count = 0: none saved.
struct SaveTiming {
int count = 0;
double mean_ms = 0.0;
double max_ms = 0.0;
};
SaveTiming saveTiming() const;
// Workers run at this nice value (and libjxl's threads, which inherit it), so
// the control loop, serial, IMU and MQTT threads keep their share when the
// CPU is short. 0 = leave as is.
static constexpr int kWorkerNice = 5;
private: private:
// A frame and the pose it was taken at, captured together at submit() time so // A frame and the pose it was taken at, captured together at submit() time so
// encoding latency and queue depth cannot separate them. // encoding latency and queue depth cannot separate them.
@ -102,8 +121,10 @@ private:
void run(); void run();
void process(const QueuedFrame& queued); void process(const QueuedFrame& queued);
// Encode and write one image; false (logged) on failure. // Encode and write one image; false (logged) on failure. For a thermal
bool save(const Frame& frame, const CameraOutput& out, const std::string& file); // frame, `stats` receives its figures, taken after rotation.
bool save(const Frame& frame, const CameraOutput& out, const std::string& file,
ThermalStats* stats = nullptr);
// Output settings for a camera index, or sane defaults if the index is out of // Output settings for a camera index, or sane defaults if the index is out of
// range (a source delivering more cameras than were configured). // range (a source delivering more cameras than were configured).
const CameraOutput& outputFor(int cam_id) const; const CameraOutput& outputFor(int cam_id) const;
@ -121,7 +142,14 @@ private:
mutable std::mutex last_event_mutex_; mutable std::mutex last_event_mutex_;
std::optional<CamEvent> last_event_; std::optional<CamEvent> last_event_;
std::optional<CamEvent> last_thermal_event_;
std::atomic<long long> saved_{0}; // images successfully written this session std::atomic<long long> saved_{0}; // images successfully written this session
// (steady ms when done, save ms) of each picture saved in the last minute.
void noteSaveTime(double ms);
mutable std::mutex timing_mutex_;
std::deque<std::pair<long long, double>> save_times_;
long long timing_logged_ms_ = 0;
}; };
} // namespace fgc } // namespace fgc

148
include/fgc/IrbgrabApi.h Normal file
View File

@ -0,0 +1,148 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
// The few calls of InfraTec's IRBGRAB SDK (libirbgrab, v4) that the thermal
// camera needs, declared here rather than taken from the SDK's own headers:
// the SDK is licensed per tower and may not be copied into this repository or
// a build image. Everything below was established against the installed SDK
// (4.3.44) and a TarisIR mini - see TarisIR-Integration.md in the workspace.
//
// The library is loaded at run time (dlopen), so the program builds and its
// tests run on machines that have never seen the SDK; a tower without it
// simply has no thermal camera.
namespace fgc::irbgrab {
using Handle = size_t; // the SDK's handles are pointer-sized integers
using Ret = uint32_t;
// Return codes: a type in the top bits, a code below.
inline constexpr Ret kTypeOk = 0x10000000;
inline constexpr Ret kTypeStatus = 0x20000000;
inline constexpr Ret kTypeErr = 0x80000000;
inline constexpr Ret kSuccess = kTypeOk | 0x01;
inline constexpr Ret kErrNotSupported = kTypeErr | 0x02;
inline constexpr Ret kErrNotFound = kTypeErr | 0x03;
// Means both "NUC in progress" and "invalid licence". With the licence
// installed correctly it was never seen in testing, so when it piles up the
// licence is the first suspect.
inline constexpr Ret kErrNucOrLicence = 0x80000017;
inline bool ok(Ret r) { return r == kSuccess; }
// Fixed text buffers the SDK fills.
inline constexpr int kCharBuffer = 1024;
// What dev_getdata hands out.
enum MemObject : int32_t {
kMemBitmap32 = 1,
kMemIrDigitFrame = 2, // uint16 counts: what is archived
kMemTemperatures = 3, // float32 Kelvin: the counts through the LUT, nothing more
};
// Parameter ids (dev_getparam / dev_setparam).
namespace param {
inline constexpr uint32_t kCalibMaxFrameRate = 117;
inline constexpr uint32_t kCalibChangeInProgress = 118;
inline constexpr uint32_t kCalibCount = 119;
inline constexpr uint32_t kCalibActive = 120;
inline constexpr uint32_t kCalibName = 121;
inline constexpr uint32_t kPollingEnable = 184; // getdata without a callback
inline constexpr uint32_t kSearchString = 202;
inline constexpr uint32_t kCameraName = 203;
inline constexpr uint32_t kCameraSerialNum = 211;
inline constexpr uint32_t kCameraTempHousing = 217; // Kelvin
inline constexpr uint32_t kFramerateHz = 240;
inline constexpr uint32_t kFrameWidth = 294;
inline constexpr uint32_t kFrameHeight = 295;
// Non-uniformity correction, done in the camera with its own shutter.
inline constexpr uint32_t kNucActive = 410; // R bool: correction applied
inline constexpr uint32_t kNucPerform = 411; // W Int2 {mode, flags}: run one now
inline constexpr uint32_t kNucInProgress = 412; // R bool
inline constexpr uint32_t kNucInterval = 415; // R/W s between automatic ones, 0 = off
} // namespace param
// kNucPerform's mode (Int2::value1); value2 takes flags, 0 for none.
inline constexpr int32_t kNucMode1Point = 2;
#pragma pack(push, 1)
struct String {
char* text;
int32_t len;
};
struct IdxString {
int32_t index;
String value;
};
struct IdxInt32 {
int32_t index;
int32_t value;
};
struct Int2 {
int32_t value1;
int32_t value2;
};
// Per-frame information (mem_getinfo). struct_size must be set before the call.
struct MemInfo {
int32_t struct_size;
int32_t triggered;
double timestamp_ms; // since the grab started
int64_t image_num;
int32_t missed_packets;
};
#pragma pack(pop)
// Data type tokens: base type | extension | size of what the pointer points at.
inline constexpr uint32_t kBaseInt32 = 0x40000000;
inline constexpr uint32_t kBaseInt64 = 0x50000000;
inline constexpr uint32_t kBaseFloat32 = 0xA0000000;
inline constexpr uint32_t kBaseString = 0xF0000000;
inline constexpr uint32_t kExIndex = 0x01000000;
inline constexpr uint32_t kExArray2 = 0x02020000; // a pair
inline constexpr uint32_t kTypeInt32 = kBaseInt32 | sizeof(int32_t);
inline constexpr uint32_t kTypeInt64 = kBaseInt64 | sizeof(int64_t);
inline constexpr uint32_t kTypeSingle = kBaseFloat32 | sizeof(float);
inline constexpr uint32_t kTypeString = kBaseString | sizeof(String);
inline constexpr uint32_t kTypeIdxString = kExIndex | kBaseString | sizeof(IdxString);
inline constexpr uint32_t kTypeIdxInt32 = kExIndex | kBaseInt32 | sizeof(IdxInt32);
inline constexpr uint32_t kType2Int32 = kExArray2 | kBaseInt32 | sizeof(Int2);
// The entry points, as function pointers so a test can stand in for the SDK.
struct Api {
Ret (*dll_init)() = nullptr;
void (*dll_uninit)() = nullptr;
Ret (*dll_version)(char* buf, int len) = nullptr;
Ret (*dll_devicetypenames)(char* buf, int len) = nullptr; // ';'-separated
Ret (*dev_create)(Handle* dev, int32_t type, char* ini) = nullptr;
Ret (*dev_free)(Handle* dev) = nullptr;
Ret (*dev_search)(Handle dev, int32_t* count) = nullptr;
Ret (*dev_connect)(Handle dev, char* search) = nullptr;
Ret (*dev_disconnect)(Handle dev) = nullptr;
Ret (*dev_startgrab)(Handle dev, int32_t stream) = nullptr;
Ret (*dev_stopgrab)(Handle dev, int32_t stream) = nullptr;
Ret (*dev_getparam)(Handle dev, uint32_t what, void* data, uint32_t type) = nullptr;
Ret (*dev_setparam)(Handle dev, uint32_t what, void* data, uint32_t type) = nullptr;
Ret (*dev_getdata)(Handle dev, int32_t what, Handle* mem) = nullptr;
Ret (*mem_free)(Handle* mem) = nullptr;
Ret (*mem_getdimension)(Handle mem, uint32_t* w, uint32_t* h, uint32_t* type) = nullptr;
Ret (*mem_getdataptr)(Handle mem, Handle* data, int32_t* bytes) = nullptr;
Ret (*mem_getinfo)(Handle mem, void* info) = nullptr;
// The counts-to-Kelvin table (65536 floats) for the given emissivity,
// transmission, environment and path temperature.
Ret (*mem_getdvlut)(Handle mem, void* lut, float eps, float tau, float t_env,
float t_path) = nullptr;
};
// Load the SDK. RTLD_GLOBAL, because the camera plugins it loads in dll_init()
// resolve their symbols against it; without it no device type registers. The
// library stays loaded for the life of the process. nullptr, with `error` set,
// if the library or one of its entry points is missing.
std::shared_ptr<const Api> load(const std::string& library, std::string& error);
// "0x80000017 (NUC in progress or invalid licence)", for logs.
std::string describe(Ret r);
} // namespace fgc::irbgrab

View File

@ -4,9 +4,13 @@
#include "fgc/IControlChannel.h" #include "fgc/IControlChannel.h"
#include <atomic> #include <atomic>
#include <condition_variable>
#include <deque>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread>
#include <utility>
#include <vector> #include <vector>
#include <mqtt/async_client.h> #include <mqtt/async_client.h>
@ -35,6 +39,7 @@ public:
void publishStatus(int code) override; void publishStatus(int code) override;
void publishCamEvent(const CamEvent& event) override; void publishCamEvent(const CamEvent& event) override;
void publishHealth(const HealthReport& report) override;
std::vector<RemoteCommand> takeCommands() override; std::vector<RemoteCommand> takeCommands() override;
void publishCommandReply(const CommandReply& reply) override; void publishCommandReply(const CommandReply& reply) override;
@ -52,11 +57,20 @@ private:
void publish(const std::string& topic, const std::string& payload, bool retained = true); void publish(const std::string& topic, const std::string& payload, bool retained = true);
// CamEvent and Health go out through a small queue with its own thread, so a
// slow link (each publish waits up to 3 s for the broker) never holds an
// image-save worker or the control loop. Bounded: when full, the oldest
// waiting message is dropped and recorded as such.
void post(const std::string& topic, const std::string& payload);
void runOutbox();
void stopOutbox(); // sends what is waiting (a few seconds at most), then ends
ChannelHistory history_; ChannelHistory history_;
std::string broker_uri_; std::string broker_uri_;
std::string tower_; std::string tower_;
std::string topic_status_; std::string topic_status_;
std::string topic_cam_event_; std::string topic_cam_event_;
std::string topic_health_;
std::string topic_command_; std::string topic_command_;
std::string topic_command_reply_; std::string topic_command_reply_;
@ -66,7 +80,14 @@ private:
std::mutex mutex_; std::mutex mutex_;
std::string last_status_; // republished on reconnect (under mutex_) std::string last_status_; // republished on reconnect (under mutex_)
std::string last_health_; // republished on reconnect (under mutex_)
std::vector<RemoteCommand> commands_; // received, not yet taken (under mutex_) std::vector<RemoteCommand> commands_; // received, not yet taken (under mutex_)
std::mutex outbox_mutex_;
std::condition_variable outbox_cv_;
std::deque<std::pair<std::string, std::string>> outbox_; // (topic, payload)
bool outbox_stop_ = false;
std::thread outbox_thread_;
}; };
} // namespace fgc } // namespace fgc

View File

@ -17,19 +17,30 @@ namespace fgc {
// BayerRG12p the same mosaic at 12 bits, packed: two pixels in three bytes // BayerRG12p the same mosaic at 12 bits, packed: two pixels in three bytes
// (GenICam "12p": p0 = b0 | (b1 & 0x0F) << 8, // (GenICam "12p": p0 = b0 | (b1 & 0x0F) << 8,
// p1 = b1 >> 4 | b2 << 4) // p1 = b1 >> 4 | b2 << 4)
// BayerRG12Packed
// the same, packed the GigE Vision 1.x way that older GigE
// cameras use (the Manta G-1236C on rietschen): the first byte
// holds p0's HIGH bits, not its low ones
// p0 = b0 << 4 | (b1 & 0x0F),
// p1 = b2 << 4 | b1 >> 4
// Same size as BayerRG12p; read as 12p it would scramble every
// other pixel.
// Mono16 two bytes per pixel, grey, host byte order - the thermal
// camera's counts (TarisCameraSource.h), all 16 bits used
// //
// OpenCV names Bayer patterns by the second row, so the camera's BayerRG is // OpenCV names Bayer patterns by the second row, so the camera's BayerRG is
// OpenCV's COLOR_BayerBG2* (measured 2026-09-30 against the camera's own RGB8). // OpenCV's COLOR_BayerBG2* (measured 2026-09-30 against the camera's own RGB8).
enum class PixelLayout { Unknown, Mono8, RGB8, BayerRG8, BayerRG12p }; enum class PixelLayout { Unknown, Mono8, RGB8, BayerRG8, BayerRG12p, Mono16, BayerRG12Packed };
// The GenICam name ("BayerRG12p"), or "Unknown". // The GenICam name ("BayerRG12p", "BayerRG12Packed"), or "Unknown".
const char* layoutName(PixelLayout layout); const char* layoutName(PixelLayout layout);
// From a GenICam name; Unknown for anything the program does not handle. // From a GenICam name; Unknown for anything the program does not handle.
PixelLayout layoutFromName(const std::string& name); PixelLayout layoutFromName(const std::string& name);
// True for the raw mosaic layouts, which need de-Bayering before saving as colour. // True for the raw mosaic layouts, which need de-Bayering before saving as colour.
bool layoutIsBayer(PixelLayout layout); bool layoutIsBayer(PixelLayout layout);
// Bits per value as delivered: 8, or 12 for BayerRG12p. 0 for Unknown. // Bits per value as delivered: 8, 12 for the 12-bit mosaics, 16 for Mono16. 0
// for Unknown.
int layoutBits(PixelLayout layout); int layoutBits(PixelLayout layout);
// Bytes one frame of width x height takes in this layout (0 for Unknown). // Bytes one frame of width x height takes in this layout (0 for Unknown).
size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height); size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height);
@ -38,11 +49,16 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height);
// (0..4095). `in` holds ceil(pixels * 3 / 2) bytes; an odd last pixel uses the // (0..4095). `in` holds ceil(pixels * 3 / 2) bytes; an odd last pixel uses the
// low half of its pair, as GenICam specifies. // low half of its pair, as GenICam specifies.
void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels); void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels);
// The same for BayerRG12Packed (GigE Vision 1.x packing, see PixelLayout).
void unpack12Packed(const uint8_t* in, uint16_t* out, size_t pixels);
// Either of the two, by layout: the 12-bit mosaic unpacked to 16-bit words.
void unpack12(PixelLayout layout, const uint8_t* in, uint16_t* out, size_t pixels);
// Brightness of a frame from a sparse sample of its pixels: every `stride`-th // Brightness of a frame from a sparse sample of its pixels: every `stride`-th
// pixel, so all colour sites of a mosaic are visited. Cheap enough to run on // pixel, so all colour sites of a mosaic are visited. Cheap enough to run on
// every frame the camera delivers (a few thousand samples). // every frame the camera delivers (a few thousand samples).
// mean on a 0..255 scale whatever the bit depth (12-bit values / 16) // mean on a 0..255 scale whatever the bit depth (12-bit values / 16,
// 16-bit values / 256)
// clipped share of samples at or near white (>= 250 on that scale) // clipped share of samples at or near white (>= 250 on that scale)
struct BrightnessSample { struct BrightnessSample {
double mean = -1.0; double mean = -1.0;

View File

@ -0,0 +1,92 @@
#pragma once
#include "fgc/Config.h"
#include "fgc/ICameraSource.h"
#include "fgc/IrbgrabApi.h"
#include <functional>
#include <memory>
#include <string>
namespace fgc {
// The thermal camera: an InfraTec TarisIR mini through the IRBGRAB SDK
// (IrbgrabApi.h), one camera per source.
//
// The camera is kept grabbing from open() to close(), whatever the scan does:
// a microbolometer that is stopped and restarted starts with zero frames and a
// fresh NUC. A worker thread owns every SDK call - connecting, polling the
// newest frame of counts, switching the calibration profile, reconnecting
// when the camera goes quiet - so nothing here ever blocks the control loop.
// trigger() hands over the newest frame if it is fresh enough, and otherwise
// says no at once: the thermal picture of a waypoint is best effort, the
// colour camera's is not.
//
// A camera that is missing, unlicensed or unreachable is not a startup error.
// The source reports why in offline(), keeps retrying, and the scan goes on
// without it.
//
// NUC: the camera recalibrates itself against its shutter every 30 s and sends
// no frame for about 0.7 s while it does (measured on tower 111). A picture
// that falls into that gets no thermal frame. So the program switches the
// camera's timer off and runs the NUC itself, at the start of a move - the
// move and the settle wait leave seconds before the next picture - once
// nuc_interval has passed, or after twice that without any move (manual mode).
class TarisCameraSource : public ICameraSource {
public:
struct Options {
std::string serial; // the camera's serial number
int calib_profile = 0;
double frame_rate_hz = 5.0;
int max_frame_age_ms = 400;
std::string library = "libirbgrab_x86_64-linux.so";
std::string interface; // for diagnostics only; may be empty
int stall_ms = 5000; // connected but no frame this long: reconnect
int retry_min_ms = 2000; // between connection attempts, doubling...
int retry_max_ms = 30000;// ...up to this
int calib_timeout_ms = 15000;// a profile switch takes about 3 s
int nuc_interval_ms = 30000;// 0: leave the camera's own NUC timer
};
static Options fromSpec(const CameraSpec& spec);
// api: the SDK, or null to load opts.library when the worker starts.
// has_net_raw: may this process open a raw socket (the SDK needs one)?
// Null: read the effective capabilities from /proc/self/status.
explicit TarisCameraSource(Options opts, std::shared_ptr<const irbgrab::Api> api = nullptr,
std::function<bool()> has_net_raw = nullptr);
~TarisCameraSource() override;
void open() override;
void close() override;
// Only open and close delivery; the camera keeps grabbing either way.
void start() override;
void stop() override;
bool trigger() override;
// Runs a due NUC now (on the worker), while the gimbal moves.
void moving() override;
bool frameReady() override;
void setFrameCallback(FrameCallback cb) override;
int cameraCount() const override { return 1; }
std::vector<CameraDeviceInfo> deviceInfo() override;
std::string offline(int camera_index) override;
bool grabFrame(int camera_index, Frame& out, int timeout_ms) override;
// "CalibProfile" (index), "KelvinPerCount", "HousingTemperature" (degC).
bool readFeature(int camera_index, const std::string& name, double& value) override;
// "CalibProfile": switch, and wait until the camera has (seconds).
bool setFeature(int camera_index, const std::string& name, const std::string& value) override;
// Disconnect and connect again; waits until it is back or gives up.
bool resetCamera(int camera_index) override;
// The counts' scale the profiles are documented to have (TarisIR mini):
// 0.01 K for profile 0, 0.02 K for profile 1; 0 for any other.
static double expectedKelvinPerCount(int calib_profile);
// Does /proc/self/status grant CAP_NET_RAW?
static bool processHasNetRaw();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace fgc

196
include/fgc/ThermalGuard.h Normal file
View File

@ -0,0 +1,196 @@
#pragma once
#include <deque>
#include <string>
#include <utility>
#include <vector>
namespace fgc {
// Watching the tower computer's CPU heat and load, and pacing the scan to it.
//
// In summer the sun and the gimbal heat the LattePanda until its CPU slows itself
// down (throttles). Saving a 12-bit picture costs ~1.4 CPU-seconds, so a slowed
// CPU may stop keeping up. The scan already holds while saving is behind
// (CaptureScheduler::setSaveGate), so nothing piles up in memory - but a computer
// held at its limit stays hot, and starves the modem and the control loop with it.
// So the guard backs off before that:
//
// Normal - full pace.
// Slowed - pictures further apart: slow_factor times the interval, or more
// when saving needs it - 1.5 x the measured save time per worker, so
// the saver keeps some headroom instead of running flat out. Entered
// when the CPU is at slow_c or more, when it is throttling, or when
// saving is the bottleneck: the scan spent held_share_pct of the last
// minute waiting for it, or held_slow_s in one go. Back to Normal once
// all of that has been clear for calm_s.
// Paused - no new pictures: the gimbal parks where it is and the camera stream
// stops, so the CPU all but idles. Entered when the CPU has stayed at
// pause_c or more for pause_after_s, or saving is still the
// bottleneck held_pause_s after slowing down (or kept the scan waiting
// held_pause_s in one go). Resumes (to Slowed) after min_pause_s, once
// the CPU is at resume_c or below and nothing is waiting to be saved.
//
// Why a share and not only one long wait: a CPU that saves a picture in 4.6 s
// against a 2 s interval never makes the scan wait long - the save gate lets it
// on after 2-3 s each time - yet it runs pegged for as long as the scan does
// (measured on the desk with a CPU quota, docs/thermal-throttling.md).
//
// It never stops the program and never changes the operating mode: auto stays
// auto, so the scan picks up by itself once the computer has cooled. Picture
// quality is never touched - only the pace.
//
// Pure: fed one reading at a time with its own clock, no I/O, so every rule here
// is unit-tested (test_thermalguard.cpp). Application samples the host every
// sample_ms and logs what update() returns.
struct ThermalParams {
bool enabled = true;
std::string sysfs_root = "/"; // fake /sys + /proc tree for a simulation
int sample_ms = 2000;
double slow_c = 85.0;
double pause_c = 92.0;
double resume_c = 75.0;
double hysteresis_c = 5.0; // Slowed -> Normal needs slow_c - this
int pause_after_s = 20;
int calm_s = 120;
int min_pause_s = 300;
int held_slow_s = 30;
int held_pause_s = 120;
double held_share_pct = 25.0; // of the last minute spent waiting for saving
double slow_factor = 2.0;
// Throttling is also inferred from the clock (see ThrottleWatch): below this
// share of the chip's top clock while at least this busy.
double clock_low_pct = 50.0;
double busy_high_pct = 50.0;
};
// One sample of the host. Each part has its own ok flag: a reading that is
// missing is left out of the rules, never taken as zero.
struct HostReading {
long long now_ms = 0;
bool temp_ok = false;
double cpu_c = 0.0;
std::string zone; // hottest thermal zone's name
bool clock_ok = false;
double mhz = 0.0; // mean over the cores
double hw_max_mhz = 0.0; // the chip's top clock
bool count_ok = false;
long long throttle_count = 0; // kernel thermal_throttle counters, summed
double busy_pct = -1.0; // since the last sample; -1 = unknown
bool held_for_save = false; // the scan is waiting for saving
long long held_total_ms = 0; // all the time it has waited so far (only rises)
int save_pending = 0; // frames waiting or being saved
// For the slowed pace: what the scan is configured to, and what saving takes.
double base_interval_ms = 0.0; // 0 = unknown
double save_ms_mean = 0.0; // last minute; 0 = unknown
int save_workers = 1;
};
enum class GuardLevel { Info, Warn };
using GuardLog = std::pair<GuardLevel, std::string>;
// "93 °C", "1.2 GHz" - for log lines and the UI.
std::string celsiusText(double c);
std::string ghzText(double mhz);
// Is the CPU slowing itself down? Two signs:
// - the kernel's thermal_throttle counters rose since the last sample (heat);
// - the clock is below clock_low_pct of the chip's top clock while the CPU is
// at least busy_high_pct busy. This catches the power-limit slow-downs the
// counters never count, and a clock capped for a simulation (cap-cpu.sh).
// Logs one line when it starts, at most one summary a minute while it lasts,
// and one when it has been clear for a minute.
class ThrottleWatch {
public:
explicit ThrottleWatch(const ThermalParams& p = {}) : p_(p) {}
void setParams(const ThermalParams& p) { p_ = p; }
void update(const HostReading& r, std::vector<GuardLog>& log);
bool throttling() const { return throttling_; }
// Samples that showed throttling in the last minute, and counter rises since start.
int samplesLastMinute() const { return static_cast<int>(recent_.size()); }
long long eventsTotal() const { return events_total_; }
long long eventsLastMinute() const;
// "counters" / "clock" / "counters and clock" - what said so last time.
const std::string& cause() const { return cause_; }
private:
ThermalParams p_;
bool have_count_ = false;
long long last_count_ = 0;
long long events_total_ = 0;
// (time, counter rise) of each sample that showed throttling, last 60 s.
std::deque<std::pair<long long, long long>> recent_;
bool throttling_ = false;
long long last_seen_ms_ = 0; // last sample that showed throttling
long long last_summary_ms_ = 0;
int low_clock_run_ = 0; // consecutive samples with a low, busy clock
std::string cause_;
};
enum class GuardState { Normal, Slowed, Paused };
const char* guardStateName(GuardState s); // "normal" / "slowed" / "paused"
class ThermalGuard {
public:
explicit ThermalGuard(const ThermalParams& p = {});
// One sample in, the log lines it produced out (throttling start/summary/end
// and state changes). Call every sample_ms.
std::vector<GuardLog> update(const HostReading& r);
// What the scan should do now.
GuardState state() const { return state_; }
// 1 when Normal; when Slowed or Paused, slow_factor or what saving needs
// (see the top of this file), at most 10.
double intervalScale() const { return state_ == GuardState::Normal ? 1.0 : scale_; }
bool pauseCapture() const { return state_ == GuardState::Paused; }
// Why it is not Normal, in plain words ("" when Normal).
const std::string& reason() const { return reason_; }
// `thermal resume`: back to Normal now, every timer restarted. The rules then
// apply afresh - a CPU still at pause_c pauses again after pause_after_s.
std::vector<GuardLog> forceResume(long long now_ms);
// `thermal off|on`: off = always Normal; the readings and the throttling log go on.
std::vector<GuardLog> setEnabled(bool on, long long now_ms);
bool enabled() const { return enabled_; }
const ThermalParams& params() const { return p_; }
const HostReading& last() const { return last_; }
bool haveReading() const { return have_last_; }
const ThrottleWatch& throttle() const { return watch_; }
long long heldForSaveMs() const; // how long the scan has been waiting
long long pausedForMs() const;
// Share of the last minute the scan spent waiting for saving, in %; -1 until
// there is half a minute of history to judge from.
double heldSharePct() const;
private:
void enter(GuardState s, const std::string& why, std::vector<GuardLog>& log);
void updateScale(const HostReading& r);
std::string paceText() const; // "one picture every 4.0 s instead of 2.0"
ThermalParams p_;
ThrottleWatch watch_;
bool enabled_ = true;
GuardState state_ = GuardState::Normal;
std::string reason_;
HostReading last_;
bool have_last_ = false;
long long hot_since_ms_ = -1; // CPU at pause_c or more since (-1 = not)
long long held_since_ms_ = -1; // the scan waiting for saving since (-1 = not)
long long calm_since_ms_ = -1; // Slowed, with every reason clear, since (-1 = not)
long long paused_at_ms_ = 0;
long long slowed_at_ms_ = 0;
long long overload_since_ms_ = -1; // Slowed, and saving still the bottleneck, since
// (time, held_total_ms) per sample over the last minute, for heldSharePct().
std::deque<std::pair<long long, long long>> held_hist_;
double scale_ = 1.0;
double last_save_ms_ = 0.0; // the last save time seen (0 = none yet)
double need_scale_ = 0.0; // the scale saving needs; <= 1: the normal pace fits
};
} // namespace fgc

View File

@ -0,0 +1,45 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace fgc {
// What a thermal camera reports about one frame besides its pixels. The pixels
// themselves are counts (PixelLayout::Mono16): already calibrated apparent
// temperature, T[K] = count * kelvin_per_count, with emissivity and path
// transmission both taken as 1. The scale depends on the camera's active
// calibration profile, so a count means nothing without `cal`.
struct ThermalFrameInfo {
int cal = 0; // active calibration profile index
double kelvin_per_count = 0.01; // K per count under that profile
uint16_t valid_count_max = 65280; // above this the camera's LUT wraps
int missed_packets = 0; // GigE packets the SDK lost for this frame
long long image_num = -1; // the camera's own frame counter
};
// The per-frame figures that go into a thermal CamEvent. Temperatures are the
// apparent temperature in tenths of a degree Celsius; the hot spot is the
// pixel centre, normalised 0..1 across the image as stored.
struct ThermalStats {
bool valid = false; // false: no pixel in the valid range
size_t valid_pixels = 0;
uint16_t max_count = 0;
uint16_t median_count = 0;
int tmax_decic = 0;
int tmed_decic = 0;
double hotx = 0.0;
double hoty = 0.0;
};
// Tenths of a degree Celsius for a count, rounded to nearest.
int countsToDeciC(uint32_t count, double kelvin_per_count);
// Maximum (its first occurrence, row-major), median and hot spot of a
// width x height image of counts. Only counts in 1..info.valid_count_max take
// part: 0 is what the camera sends before its first real frame, and anything
// above the limit has wrapped.
ThermalStats thermalStats(const uint16_t* counts, uint32_t width, uint32_t height,
const ThermalFrameInfo& info);
} // namespace fgc

View File

@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstring>
#include <map> #include <map>
#include <string> #include <string>
#include <thread> #include <thread>
@ -13,8 +14,10 @@ namespace fgc {
// Synthetic camera for development without hardware. On trigger() it generates // Synthetic camera for development without hardware. On trigger() it generates
// a small gradient frame (varying with time so successive captures differ) and // a small gradient frame (varying with time so successive captures differ) and
// delivers it to the frame callback - as RGB8 by default, or as the raw mosaic // delivers it to the frame callback - as RGB8 by default, as the raw mosaic
// (BayerRG8, BayerRG12p) a raw-capture profile would deliver. setProfile() only // (BayerRG8, BayerRG12p) a raw-capture profile would deliver, or as a thermal
// camera's counts (Mono16: a 15..25 degC gradient with one 300 degC spot that
// moves with time, profile 0). setProfile() only
// records the profile name - there is no sensor to configure - which is enough // records the profile name - there is no sensor to configure - which is enough
// to exercise profile switching end to end with --mock-camera. // to exercise profile switching end to end with --mock-camera.
class MockCameraSource : public ICameraSource { class MockCameraSource : public ICameraSource {
@ -54,6 +57,11 @@ public:
} }
bool readFeature(int camera_index, const std::string& name, double& value) override { bool readFeature(int camera_index, const std::string& name, double& value) override {
if (camera_index < 0 || camera_index >= count_) return false; if (camera_index < 0 || camera_index >= count_) return false;
if (layout_ == PixelLayout::Mono16) { // a thermal camera, like TarisCameraSource
if (name == "CalibProfile") { value = 0.0; return true; }
if (name == "KelvinPerCount") { value = 0.01; return true; }
return false;
}
if (name == "IntensityControllerTarget") { value = 50.0; return true; } if (name == "IntensityControllerTarget") { value = 50.0; return true; }
if (name == "IntensityControllerTolerance") { value = 5.0; return true; } if (name == "IntensityControllerTolerance") { value = 5.0; return true; }
if (name == "ExposureAutoMin") { value = 82.0; return true; } if (name == "ExposureAutoMin") { value = 82.0; return true; }
@ -95,6 +103,7 @@ public:
bool featureRange(int camera_index, const std::string& name, double& min, bool featureRange(int camera_index, const std::string& name, double& min,
double& max) override { double& max) override {
if (camera_index < 0 || camera_index >= count_) return false; if (camera_index < 0 || camera_index >= count_) return false;
if (layout_ == PixelLayout::Mono16) return false; // a thermal camera: no exposure
if (name == "ExposureTime") { min = 66.357; max = 9999971.0; return true; } if (name == "ExposureTime") { min = 66.357; max = 9999971.0; return true; }
if (name == "Gain") { min = 0.0; max = 48.0; return true; } if (name == "Gain") { min = 0.0; max = 48.0; return true; }
if (name == "BlackLevel") { min = 0.0; max = 4095.0; return true; } if (name == "BlackLevel") { min = 0.0; max = 4095.0; return true; }
@ -137,6 +146,7 @@ private:
// A mid-grey gradient frame. The pattern varies with time so successive // A mid-grey gradient frame. The pattern varies with time so successive
// captures differ. // captures differ.
Frame synth() const { Frame synth() const {
if (layout_ == PixelLayout::Mono16) return thermal();
Frame f; Frame f;
f.width = width_; f.width = width_;
f.height = height_; f.height = height_;
@ -182,6 +192,31 @@ private:
return f; return f;
} }
// A thermal scene in counts at 0.01 K per count.
Frame thermal() const {
Frame f;
f.width = width_;
f.height = height_;
f.channels = 1;
f.layout = PixelLayout::Mono16;
f.cam_id = 0;
f.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
std::vector<uint16_t> px(static_cast<size_t>(width_) * height_);
for (uint32_t y = 0; y < height_; ++y)
for (uint32_t x = 0; x < width_; ++x) // 288.15 K at the top .. 298.15 K
px[static_cast<size_t>(y) * width_ + x] =
static_cast<uint16_t>(28815 + 1000 * y / std::max<uint32_t>(1, height_ - 1));
const uint32_t hx = static_cast<uint32_t>(f.timestamp_ms / 100) % width_;
px[static_cast<size_t>(height_ / 2) * width_ + hx] = 57315; // 300 degC
f.data.resize(px.size() * 2);
std::memcpy(f.data.data(), px.data(), f.data.size());
f.brightness = sampleBrightness(f.data.data(), f.data.size(), f.layout, f.width, f.height);
f.thermal = ThermalFrameInfo{};
return f;
}
// The RGB frame as the sensor would see it: each pixel keeps only the colour // The RGB frame as the sensor would see it: each pixel keeps only the colour
// of its filter (BayerRG: R G / G B), 8-bit or packed 12-bit. // of its filter (BayerRG: R G / G B), 8-bit or packed 12-bit.
Frame mosaic(const Frame& rgb) const { Frame mosaic(const Frame& rgb) const {

View File

@ -33,11 +33,17 @@ public:
drop(kTopicStatusCode, payload); drop(kTopicStatusCode, payload);
} }
void publishCamEvent(const CamEvent& e) override { void publishCamEvent(const CamEvent& e) override {
LOG_TRACE_CAT(LogCat::Mqtt) << "PUB CamEvent cam=" << e.camera const std::string payload = camEventJson(e);
<< " hdg=" << e.heading_decideg << " t=" << e.timestamp_ms LOG_TRACE_CAT(LogCat::Mqtt) << "PUB CamEvent " << payload << " (null channel)";
<< " (null channel)"; drop(kTopicCamEvent, payload);
drop(kTopicCamEvent, camEventJson(e));
} }
void publishHealth(const HealthReport& h) override {
++health_published_;
last_health_ = h;
drop(kTopicHealth, healthJson(h));
}
int healthPublished() const { return health_published_; }
const HealthReport& lastHealth() const { return last_health_; }
// Commands: none arrive by themselves; inject() stands in for the broker, // Commands: none arrive by themselves; inject() stands in for the broker,
// so --no-mqtt runs and tests can drive the remote-command path. // so --no-mqtt runs and tests can drive the remote-command path.
@ -71,6 +77,8 @@ private:
ChannelHistory history_; ChannelHistory history_;
std::vector<RemoteCommand> pending_; std::vector<RemoteCommand> pending_;
std::vector<CommandReply> replies_; std::vector<CommandReply> replies_;
int health_published_ = 0;
HealthReport last_health_;
}; };
} // namespace fgc } // namespace fgc

View File

@ -81,15 +81,20 @@ struct SensorGroup {
// The Sensors panel, split by source so it is clear which parameters come from // The Sensors panel, split by source so it is clear which parameters come from
// which device. Each group is filled with pending placeholders until its driver // which device. Each group is filled with pending placeholders until its driver
// lands. NOTE: the only temperature here is the env sensor's *ambient* one — the // lands. NOTE: two temperatures show here, kept apart by their labels: the env
// sensor's *ambient* "Temp" and the computer's own "CPU temp" (host group). The
// MTi's *internal* device temperature is diagnostic detail and lives solely in // MTi's *internal* device temperature is diagnostic detail and lives solely in
// the expanded view (ImuView::temp_c), so the compact panel never shows two // the expanded view (ImuView::temp_c). The env group's title/backing sensor is
// temperatures the operator has to tell apart. The env group's title/backing // swappable (currently SHT41 over I2C; see IEnvSensor) without touching this
// sensor is swappable (currently SHT41 over I2C; see IEnvSensor) without // struct.
// touching this struct.
struct SensorsView { struct SensorsView {
SensorGroup imu; // Xsens MTi: Roll/Pitch/Yaw SensorGroup imu; // Xsens MTi: Roll/Pitch/Yaw
SensorGroup env; // ambient temperature + humidity (SHT41) SensorGroup env; // ambient temperature + humidity (SHT41)
SensorGroup host; // the LattePanda's CPU: temperature, clock, load, throttling
// The CPU is hot, slowing itself, or the scan is slowed/paused for it: the
// badge text ("HOT" / "SLOWED" / "PAUSED"), "" for none, and how loud.
std::string cpu_badge;
bool cpu_badge_red = false;
// The MTi is not streaming what the host configured (the detail is in // The MTi is not streaming what the host configured (the detail is in
// ImuConfigView::problems). Flagged on the compact panel too, because the // ImuConfigView::problems). Flagged on the compact panel too, because the
@ -139,6 +144,7 @@ struct CameraDeviceView {
unsigned width = 0, height = 0; long long payload_bytes = 0; unsigned width = 0, height = 0; long long payload_bytes = 0;
double exposure_us = 0, gain_db = 0, temperature_c = 0, actual_fps = 0; double exposure_us = 0, gain_db = 0, temperature_c = 0, actual_fps = 0;
long long frames_delivered = 0, frames_dropped = 0, restarts = 0; long long frames_delivered = 0, frames_dropped = 0, restarts = 0;
std::string offline; // why it delivers nothing; empty while it does
}; };
struct CaptureView { struct CaptureView {
@ -167,6 +173,15 @@ struct CaptureView {
double last_heading_deg = 0.0; double last_heading_deg = 0.0;
double last_pitch_deg = 0.0; double last_pitch_deg = 0.0;
long long last_ts_ms = 0; long long last_ts_ms = 0;
// Last thermal picture (ImagePipeline::lastThermalEvent()): its apparent
// temperatures, so a hot spot shows on the dashboard without the archive.
bool has_thermal = false;
int thermal_cal = 0;
bool thermal_stats_ok = false;
double thermal_max_c = 0.0;
double thermal_med_c = 0.0;
long long thermal_ts_ms = 0;
}; };
struct ConnView { struct ConnView {
@ -277,6 +292,40 @@ struct EnvView {
bool fan_stalled = false; // driven, but the tach reads nothing bool fan_stalled = false; // driven, but the tach reads nothing
}; };
// The tower computer's CPU and the guard pacing the scan to it (ThermalGuard),
// for the expanded Sensors view. Filled from the guard's last sample - the UI
// never reads /sys itself.
struct HostView {
bool present = false; // at least one sample taken
bool temp_ok = false;
double cpu_c = 0.0;
std::string zone; // hottest thermal zone, e.g. "x86_pkg_temp"
bool clock_ok = false;
double mean_mhz = 0.0;
std::vector<double> core_mhz;
double hw_max_mhz = 0.0; // what the chip can do
double cap_mhz = 0.0; // what it is allowed now (scaling_max_freq)
double busy_pct = -1.0; // -1 = unknown
double load1 = 0.0, load5 = 0.0, load15 = 0.0;
int cpus = 0;
bool throttling = false;
std::string throttle_cause; // "counters" / "clock" / "counters and clock"
bool counters_ok = false; // the kernel counts throttling on this host
long long throttle_total = 0; // counter rises since start
long long throttle_last_min = 0;
// The guard.
bool guard_on = true;
std::string state = "normal"; // normal / slowed / paused
std::string reason;
double slow_c = 0.0, pause_c = 0.0, resume_c = 0.0;
double interval_s = 0.0; // between pictures now
long long held_s = 0; // the scan waiting for saving
long long paused_s = 0;
// Saving, over the last minute.
int save_count = 0;
double save_mean_ms = 0.0, save_max_ms = 0.0;
};
// Status of the currently-running special operation (calibration, diagnostics, // Status of the currently-running special operation (calibration, diagnostics,
// homing, capture) plus the last completed result, for the activity strip below // homing, capture) plus the last completed result, for the activity strip below
// the log. The result block persists until replaced so it never scrolls away. // the log. The result block persists until replaced so it never scrolls away.
@ -359,6 +408,7 @@ struct UiSnapshot {
DumpView dump; DumpView dump;
ImuView imu; ImuView imu;
EnvView env; EnvView env;
HostView host;
ActivityView activity; ActivityView activity;
CalibResultView calib; CalibResultView calib;
DiagResultView diag; DiagResultView diag;

View File

@ -50,6 +50,9 @@ int main(int argc, char* argv[]) {
"or one beside config.ini)") "or one beside config.ini)")
("no-mqtt", po::bool_switch(), "disable MQTT (use a null control channel)") ("no-mqtt", po::bool_switch(), "disable MQTT (use a null control channel)")
("mock-camera", po::bool_switch(), "use a simulated camera (no hardware)") ("mock-camera", po::bool_switch(), "use a simulated camera (no hardware)")
("mock-camera-full", po::bool_switch(),
"simulated camera with the real camera's 5328x3040 12-bit frames, so saving loads "
"the CPU as on a tower (implies --mock-camera)")
("mock-serial", po::bool_switch(), "use a simulated motor controller (no hardware)") ("mock-serial", po::bool_switch(), "use a simulated motor controller (no hardware)")
("mock-env", po::bool_switch(), ("mock-env", po::bool_switch(),
"use a simulated ambient temp/humidity sensor (implies enable_env)") "use a simulated ambient temp/humidity sensor (implies enable_env)")
@ -222,6 +225,10 @@ int main(int argc, char* argv[]) {
RuntimeOptions opts; RuntimeOptions opts;
if (vm["no-mqtt"].as<bool>()) opts.use_mqtt = false; if (vm["no-mqtt"].as<bool>()) opts.use_mqtt = false;
if (vm["mock-camera"].as<bool>()) opts.mock_camera = true; if (vm["mock-camera"].as<bool>()) opts.mock_camera = true;
if (vm["mock-camera-full"].as<bool>()) {
opts.mock_camera = true;
opts.mock_camera_full = true;
}
if (vm["mock-serial"].as<bool>()) opts.mock_serial = true; if (vm["mock-serial"].as<bool>()) opts.mock_serial = true;
if (vm["mock-env"].as<bool>()) opts.mock_env = true; if (vm["mock-env"].as<bool>()) opts.mock_env = true;
opts.degraded = vm["degraded"].as<bool>(); opts.degraded = vm["degraded"].as<bool>();

View File

@ -120,6 +120,102 @@ RECIPES = {
], ],
} }
# The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) name
# things differently - ExposureTimeAbs, AcquisitionFrameRateAbs, ExposureAutoTarget
# - and have no IntensityController, no DeviceLinkThroughputLimit and no 12-bit
# packed Bayer the program reads. Measured on rietschen's NIR camera, 2026-10-20.
# The link is shared: the camera free-runs as fast as StreamBytesPerSecond lets
# it (41 MB/s gave 3.2 fps at 12.4 MB a frame), and the cameras of one tower sit
# on one GbE port (~124 MB/s), so each gets half of it - room for RGB and NIR.
# PixelFormat takes the first the camera offers: raw Bayer on the colour camera,
# de-Bayered on the tower and a third of RGB8 on the wire; Mono8 on a mono one.
# The packet size is left alone: it has to fit the port's MTU, which is the
# tower's network setup, not the camera's.
_GIGE_COMMON = [
("TriggerSelector", "FrameStart"),
("TriggerMode", "Off"),
("AcquisitionMode", "Continuous"),
("BinningVertical", 1),
("BinningHorizontal", 1),
("OffsetX", 0),
("OffsetY", 0),
("Width", 100000),
("Height", 100000),
# 12 bits where the camera has them in the GigE packing the program reads
# (rietschen's RGB camera, chosen there 2026-10-26: 18.6 MB, 3.1 fps at
# 60 MB/s); 8 bits otherwise. The mono cameras' Mono12Packed is not read.
("PixelFormat", ("BayerRG12Packed", "BayerRG8", "Mono8")),
("StreamBytesPerSecond", 60000000),
# Free-running, a Manta still runs at AcquisitionFrameRateAbs, not at its
# limit: rietschen's RGB camera came with 0.955 and delivered 5 frames in
# 5.2 s. A short fixed exposure first, so the rate is not capped by the
# exposure the camera happens to hold, as in the Alvium recipe.
("ExposureAuto", "Off"),
("ExposureTimeAbs", 1000.0),
("AcquisitionFrameRateAbs", 1000.0), # clamped to the camera's maximum
("ChunkModeActive", False),
# The keep rule waits for the frame mean to reach the camera's target, +- its
# tolerance, which it reads from the camera. The camera has to aim at the
# mean: the RGB camera came with FitRange (which fits the histogram, wherever
# the mean lands) and no waypoint settled (2026-10-21). 30 % rather than the
# Alvium's 50: at 50 the sky over the horizon burnt out at noon, where smoke
# would rise, and at 35 it was still too bright; the old program ran these
# cameras at 23-25.
("ExposureAutoAlg", "Mean"),
("ExposureAutoAdjustTol", 5),
("ExposureAutoTarget", 30),
("ExposureAuto", "Continuous"),
("GainAuto", "Continuous"),
]
GIGE_RECIPES = {
"standard": _GIGE_COMMON + [
("ExposureAutoMax", 20000),
# Rietschen's RGB camera sat at 200-600 us at noon: a floor of 200 left it
# no room to darken a bright view, and without chunk data the host cannot
# tell a camera at its limit from one still settling. The Manta goes to 16.
("ExposureAutoMin", 20),
("GainAutoMax", 24.0),
],
"night": _GIGE_COMMON + [
("ExposureAutoMax", 500000),
("ExposureAutoMin", 1000),
("GainAutoMax", 48.0), # clamped to the camera's 40
],
}
def has_feature(cam, name):
try:
cam.get_feature_by_name(name)
return True
except Exception:
return False
def recipe_family(cam):
"""'gige' for a camera that only knows the older names, else 'alvium'."""
if not has_feature(cam, "ExposureTime") and has_feature(cam, "ExposureTimeAbs"):
return "gige"
return "alvium"
def resolve_value(feat, value):
"""The value to write to this feature; None if a choice list has no match.
A tuple is a list of choices for an enum: the first one the camera offers.
An integer feature takes no float, even a whole one (ExposureAutoMin is Int
on the Manta and Float on the Alvium)."""
if isinstance(value, tuple):
try:
offered = {str(e) for e in feat.get_available_entries()}
except Exception:
offered = set()
return next((v for v in value if v in offered), None)
if isinstance(feat, vmbpy.IntFeature) and isinstance(value, float):
return int(round(value))
return value
def apply_feature(cam, name, value, dry_run): def apply_feature(cam, name, value, dry_run):
try: try:
@ -131,6 +227,10 @@ def apply_feature(cam, name, value, dry_run):
except Exception: except Exception:
before = None before = None
value = resolve_value(feat, value)
if value is None:
return ("refused", name, before, "none of the choices is offered")
# Clamp a numeric value the camera cannot reach, rather than letting the # Clamp a numeric value the camera cannot reach, rather than letting the
# write fail and leaving whatever was there before. Without this, exporting # write fail and leaving whatever was there before. Without this, exporting
# 'standard' right after 'night' wrote a standard profile running at the # 'standard' right after 'night' wrote a standard profile running at the
@ -168,6 +268,8 @@ def main():
"fire_gimbal_control", "camera_profiles", "rgb"), "fire_gimbal_control", "camera_profiles", "rgb"),
help="directory to write <profile>.xml into") help="directory to write <profile>.xml into")
ap.add_argument("--camera", default=None, help="camera id (default: the first real one)") ap.add_argument("--camera", default=None, help="camera id (default: the first real one)")
ap.add_argument("--family", default="auto", choices=("auto", "alvium", "gige"),
help="which recipe set (default: from the features the camera has)")
ap.add_argument("--dry-run", action="store_true") ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args() args = ap.parse_args()
@ -191,10 +293,12 @@ def main():
cam = cams[0] cam = cams[0]
with cam: with cam:
family = recipe_family(cam) if args.family == "auto" else args.family
recipe = (GIGE_RECIPES if family == "gige" else RECIPES)[args.profile]
print(f"camera : {cam.get_id()} {cam.get_model()}") print(f"camera : {cam.get_id()} {cam.get_model()}")
print(f"profile : {args.profile}{' (dry run)' if args.dry_run else ''}") print(f"profile : {args.profile} ({family} recipe){' (dry run)' if args.dry_run else ''}")
refused = [] refused = []
for name, value in RECIPES[args.profile]: for name, value in recipe:
state, n, before, after = apply_feature(cam, name, value, args.dry_run) state, n, before, after = apply_feature(cam, name, value, args.dry_run)
mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X", mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X",
"clamped": " ~", "would-clamp": " ~"}[state] "clamped": " ~", "would-clamp": " ~"}[state]
@ -205,13 +309,14 @@ def main():
refused.append(n) refused.append(n)
# What the settings actually cost per frame - the number that decides # What the settings actually cost per frame - the number that decides
# whether this profile can be delivered over USB at all. # whether this profile can be delivered over the link at all.
try: try:
w = cam.get_feature_by_name("Width").get() w = cam.get_feature_by_name("Width").get()
h = cam.get_feature_by_name("Height").get() h = cam.get_feature_by_name("Height").get()
fmt = cam.get_feature_by_name("PixelFormat").get() fmt = cam.get_feature_by_name("PixelFormat").get()
bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1) bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1)
rate = cam.get_feature_by_name("AcquisitionFrameRate").get() rate_name = "AcquisitionFrameRateAbs" if family == "gige" else "AcquisitionFrameRate"
rate = cam.get_feature_by_name(rate_name).get()
mb = w * h * bpp / 1e6 mb = w * h * bpp / 1e6
print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame " print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame "
f"{rate:.2f} fps = {mb * rate:.0f} MB/s") f"{rate:.2f} fps = {mb * rate:.0f} MB/s")

82
scripts/fgc-setcap Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Give fire_gimbal_control the one privilege the thermal camera needs.
#
# The InfraTec SDK talks to the TarisIR over a raw AF_PACKET socket, which
# takes CAP_NET_RAW. Rather than running the whole program - motors, MQTT
# remote control and all - as root, the binary gets that one capability and
# keeps running as the tower's user. A rebuild replaces the binary and with it
# the capability, so deploy.sh and install-tower run this after every build.
#
# scripts/fgc-setcap set it (asks for the sudo password)
# scripts/fgc-setcap --if-thermal ... only if a [Camera.*] has driver = taris;
# never fails, says what is missing
# scripts/fgc-setcap --check exit 0 if the binary has it
# scripts/fgc-setcap --install-sudoers allow exactly this setcap without a
# password, so deploy.sh can run it
#
# A binary with file capabilities runs in the loader's secure mode: LD_* is
# ignored and no core dumps are written. So the SDK (/usr/lib/infratec, in the
# ld cache) and Vimba X must be found without LD_LIBRARY_PATH - see
# docs/tower-installation.md, "Thermal camera", for the check.
set -euo pipefail
REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
BIN="$REPO/build/fire_gimbal_control"
CAPS="cap_net_raw+ep"
SETCAP=/usr/sbin/setcap # fixed: the sudoers rule names this exact path
[[ -x $SETCAP ]] || SETCAP=$(command -v setcap 2>/dev/null || echo /usr/sbin/setcap)
SUDOERS=/etc/sudoers.d/fire-gimbal-control-setcap
HW=/etc/fire_gimbal_control/hardware.ini
thermal_configured() {
local f cfg
cfg=$("$REPO/scripts/tower-config" where 2>/dev/null || true)
for f in "$cfg" "$REPO/config.ini" "$HW"; do
[[ -r $f ]] && grep -Eqi '^[[:space:]]*driver[[:space:]]*=[[:space:]]*taris' "$f" && return 0
done
return 1
}
has_cap() { command -v getcap > /dev/null && getcap "$BIN" 2>/dev/null | grep -q cap_net_raw; }
set_cap() {
[[ -x $BIN ]] || { echo "fgc-setcap: $BIN not built yet" >&2; return 1; }
if sudo -n "$SETCAP" "$CAPS" "$BIN" 2>/dev/null; then :
elif [[ -t 0 ]]; then sudo "$SETCAP" "$CAPS" "$BIN"
else return 1
fi
has_cap
}
case "${1:-}" in
--check)
has_cap
;;
--install-sudoers)
rule="$(id -un) ALL=(root) NOPASSWD: $SETCAP $CAPS $BIN"
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
printf '# fire_gimbal_control: the thermal camera needs cap_net_raw after every build\n%s\n' \
"$rule" > "$tmp"
/usr/sbin/visudo -cf "$tmp" > /dev/null || { echo "fgc-setcap: rule rejected by visudo" >&2; exit 1; }
sudo install -m 0440 -o root -g root "$tmp" "$SUDOERS"
echo "wrote $SUDOERS: $rule"
;;
--if-thermal)
thermal_configured || exit 0
if set_cap; then
echo "fgc-setcap: $BIN may open raw sockets (thermal camera)"
else
echo "!! fgc-setcap: could not give $BIN $CAPS - the thermal camera stays offline." >&2
echo "!! Once on the tower: scripts/fgc-setcap --install-sudoers (or run scripts/fgc-setcap)" >&2
fi
exit 0
;;
"")
set_cap && echo "fgc-setcap: $BIN may open raw sockets"
;;
*)
sed -n '2,20p' "$0" >&2
exit 2
;;
esac

View File

@ -334,22 +334,35 @@ set -euo pipefail
S_MCU="$(probe_mcu)" S_MCU="$(probe_mcu)"
S_MCU_N=$(grep -c . <<< "$S_MCU" || true) S_MCU_N=$(grep -c . <<< "$S_MCU" || true)
# Another program on the motor MCU's port - a legacy tower's old program, started # Another program on the motor MCU's port - a legacy tower's old program - would
# by start_gimbal.service - would fight this one for it, and the flash would # fight this one for it, and the flash would fail half-way. Our own program is
# fail half-way. Our own program is fine: it is stopped before anything needs # fine: it is stopped before anything needs the port. The old program is started
# the port. # differently from tower to tower: start_gimbal.service from
# ~/scripts/startup_gimbal.sh, which also runs the picture transfer, on
# staeffelsberg; gimbal_control.service on its own on rietschen.
if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then
for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do
args="$(ps -o args= -p "$pid" 2>/dev/null)" args="$(ps -o args= -p "$pid" 2>/dev/null)"
[[ $args == */fire_gimbal_control* ]] && continue [[ $args == */fire_gimbal_control* ]] && continue
die "the motor MCU's port is held by another program: pid $pid ($args). unit="$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')"
Stop it for good before installing. On a legacy tower it is started by if [[ $unit == start_gimbal.service ]]; then
start_gimbal.service from ~/scripts/startup_gimbal.sh, which also runs the picture how="It is started by start_gimbal.service from ~/scripts/startup_gimbal.sh, which
transfer: comment out the old program's line there, keep the transfer, then also runs the picture transfer: comment out the old program's line there, keep the
transfer, then
sudo systemctl restart start_gimbal" sudo systemctl restart start_gimbal"
elif [[ $unit == *.service ]]; then
how="It is started by $unit:
sudo systemctl disable --now $unit"
else
how="Find what starts it (systemctl status $pid) and stop that."
fi
die "the motor MCU's port is held by another program: pid $pid ($args).
Stop it for good before installing. $how"
done done
fi fi
S_CAM="$(probe_camera_id)" S_CAM="$(probe_camera_id)"
S_CAM_N=$(grep -c . <<< "$S_CAM" || true)
S_CAM_SHOWN="$(paste -sd ' ' <<< "$S_CAM")"
S_LPM="$(probe_camera_lpm_path)" S_LPM="$(probe_camera_lpm_path)"
S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')" S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
S_PIO="$(pio_path || true)" S_PIO="$(pio_path || true)"
@ -377,7 +390,7 @@ board : $S_BOARD (kit hardware/$S_BOARD, firmware env $KIT_FW_ENV)
software / fw : $S_SW_REV / $S_FW_REV software / fw : $S_SW_REV / $S_FW_REV
motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE] motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE]
running firmware : ${S_FW_NOW:-unknown} running firmware : ${S_FW_NOW:-unknown}
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)} camera : ${S_CAM_SHOWN:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
usbfs_memory_mb : $S_USBFS usbfs_memory_mb : $S_USBFS
hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent) hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent)
config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent) config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent)
@ -604,7 +617,11 @@ if [[ ! -r $HW ]]; then
ask serial "No motor MCU found - its /dev/serial/by-id path" ask serial "No motor MCU found - its /dev/serial/by-id path"
KIT_ARGS+=(--serial "$REPLY_VAL") KIT_ARGS+=(--serial "$REPLY_VAL")
fi fi
if [[ $CAMS_A != none && -z $S_CAM ]]; then if [[ $CAMS_A != none ]] && (( S_CAM_N > 1 )); then
echo "Several cameras:"; nl -w2 -s') ' <<< "$S_CAM"
ask camera_id "Which is the colour camera (its DEV_ id)"
KIT_ARGS+=(--camera-id "$REPLY_VAL")
elif [[ $CAMS_A != none && -z $S_CAM ]]; then
ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)" ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)"
KIT_ARGS+=(--camera-id "$REPLY_VAL") KIT_ARGS+=(--camera-id "$REPLY_VAL")
fi fi
@ -1064,6 +1081,15 @@ if [[ ${DO[config.ini]} != skip ]]; then
done_step config.ini done_step config.ini
fi fi
# A thermal camera's SDK opens a raw socket: give the freshly built binary
# cap_net_raw, and let later deploys do the same without a password. Does
# nothing on a tower without one.
"$REPO/scripts/fgc-setcap" --if-thermal
if "$REPO/scripts/fgc-setcap" --check 2>/dev/null && [[ ! -f /etc/sudoers.d/fire-gimbal-control-setcap ]]; then
"$REPO/scripts/fgc-setcap" --install-sudoers ||
say "could not write the sudoers rule; deploy.sh will ask you to run scripts/fgc-setcap"
fi
if [[ ${DO[platformio]} == change ]]; then if [[ ${DO[platformio]} == change ]]; then
hr platformio hr platformio
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py -o "$WORK/get-platformio.py" curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py -o "$WORK/get-platformio.py"
@ -1083,9 +1109,13 @@ if [[ ${DO[profiles]} == change ]]; then
"$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then "$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then
PROFDIR="$(prof_dir)" # the config may have moved since the plan was made PROFDIR="$(prof_dir)" # the config may have moved since the plan was made
mkdir -p "$PROFDIR" mkdir -p "$PROFDIR"
# The camera hardware.ini names, not the first one the SDK lists: on a
# tower with several (rietschen: three GigE) that is another camera.
cam_id="$(ini_get "$HW" Camera.rgb id 2>/dev/null || true)"
cam_arg=(); [[ $cam_id == DEV_* ]] && cam_arg=(--camera "$cam_id")
for prof in "${PROF_MISSING[@]}"; do for prof in "${PROF_MISSING[@]}"; do
( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \ ( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \
scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" ) || scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" "${cam_arg[@]}" ) ||
warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)" warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)"
done done
done_step profiles done_step profiles

View File

@ -0,0 +1,189 @@
#include "fgc/CompositeCameraSource.h"
#include "fgc/Logger.h"
#include <chrono>
#include <stdexcept>
namespace fgc {
namespace {
long long steadyMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
} // namespace
CompositeCameraSource::CompositeCameraSource(std::vector<std::unique_ptr<ICameraSource>> children,
std::vector<int> counts, int follower_wait_ms)
: children_(std::move(children)), counts_(std::move(counts)),
follower_wait_ms_(follower_wait_ms) {
if (children_.empty()) throw std::invalid_argument("CompositeCameraSource needs a camera");
if (counts_.empty())
for (const auto& c : children_) counts_.push_back(c->cameraCount());
if (counts_.size() != children_.size())
throw std::invalid_argument("CompositeCameraSource: one camera count per source");
int next = 0;
for (int n : counts_) {
offsets_.push_back(next);
next += n;
}
}
ICameraSource* CompositeCameraSource::route(int camera_index, int& local) const {
for (size_t k = children_.size(); k-- > 0;) {
if (camera_index < offsets_[k]) continue;
local = camera_index - offsets_[k];
return local < counts_[k] ? children_[k].get() : nullptr;
}
return nullptr;
}
void CompositeCameraSource::open() {
// The leader failing stops the run, as a lone camera would. A follower
// failing does not: the scan goes on without its pictures.
children_[0]->open();
for (size_t k = 1; k < children_.size(); ++k) {
try {
children_[k]->open();
} catch (const std::exception& e) {
LOG_ERROR << "camera " << offsets_[k] << " did not open (" << e.what()
<< "); continuing without it";
}
}
}
void CompositeCameraSource::close() {
for (auto& c : children_) c->close();
}
void CompositeCameraSource::start() {
for (auto& c : children_) c->start();
}
void CompositeCameraSource::stop() {
for (auto& c : children_) c->stop();
}
bool CompositeCameraSource::trigger() {
bool followers_ready = true;
for (size_t k = 1; k < children_.size(); ++k) followers_ready &= children_[k]->frameReady();
if (!followers_ready) {
const long long now = steadyMs();
if (waiting_since_ == 0) waiting_since_ = now;
if (now - waiting_since_ < follower_wait_ms_) return false; // asked again next tick
LOG_DEBUG << "camera: a follower had no fresh frame for " << now - waiting_since_
<< " ms; taking the picture without it";
}
waiting_since_ = 0;
const bool taken = children_[0]->trigger();
if (taken)
for (size_t k = 1; k < children_.size(); ++k) children_[k]->trigger();
return taken;
}
void CompositeCameraSource::moving() {
for (auto& c : children_) c->moving();
}
bool CompositeCameraSource::frameReady() {
for (auto& c : children_)
if (!c->frameReady()) return false;
return true;
}
void CompositeCameraSource::setFrameCallback(FrameCallback cb) {
callback_ = std::move(cb);
// The leader's frames pass through untouched (they can be 24 MB); its
// camera 0 is global camera 0, so not even the index changes.
children_[0]->setFrameCallback([this](const Frame& f) {
if (callback_) callback_(f);
});
for (size_t k = 1; k < children_.size(); ++k) {
const int offset = offsets_[k];
children_[k]->setFrameCallback([this, offset](const Frame& f) {
if (!callback_) return;
Frame g = f;
g.cam_id = f.cam_id + offset;
callback_(g);
});
}
}
bool CompositeCameraSource::setProfile(int camera_index, const CameraProfile& profile) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->setProfile(local, profile);
}
uint64_t CompositeCameraSource::frameSeq() { return children_[0]->frameSeq(); }
std::vector<FrameStats> CompositeCameraSource::framesSince(uint64_t after) {
return children_[0]->framesSince(after);
}
bool CompositeCameraSource::readFeature(int camera_index, const std::string& name,
double& value) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->readFeature(local, name, value);
}
// What the children have opened, so a camera missing from the bus shows as a
// shortfall (HardwarePresence) - while the indices stay those configured.
int CompositeCameraSource::cameraCount() const {
int n = 0;
for (const auto& c : children_) n += c->cameraCount();
return n;
}
std::vector<CameraDeviceInfo> CompositeCameraSource::deviceInfo() {
std::vector<CameraDeviceInfo> out;
for (auto& c : children_) {
auto d = c->deviceInfo();
out.insert(out.end(), d.begin(), d.end());
}
return out;
}
std::string CompositeCameraSource::offline(int camera_index) {
int local = 0;
auto* c = route(camera_index, local);
return c ? c->offline(local) : "no such camera";
}
bool CompositeCameraSource::grabFrame(int camera_index, Frame& out, int timeout_ms) {
int local = 0;
auto* c = route(camera_index, local);
if (!c || !c->grabFrame(local, out, timeout_ms)) return false;
out.cam_id = camera_index;
return true;
}
bool CompositeCameraSource::setFeature(int camera_index, const std::string& name,
const std::string& value) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->setFeature(local, name, value);
}
bool CompositeCameraSource::featureRange(int camera_index, const std::string& name, double& min,
double& max) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->featureRange(local, name, min, max);
}
bool CompositeCameraSource::restoreProfile(int camera_index) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->restoreProfile(local);
}
bool CompositeCameraSource::resetCamera(int camera_index) {
int local = 0;
auto* c = route(camera_index, local);
return c && c->resetCamera(local);
}
} // namespace fgc

View File

@ -5,8 +5,13 @@
#include "fgc/RawImage.h" #include "fgc/RawImage.h"
#include <algorithm> #include <algorithm>
#include <cerrno>
#include <chrono>
#include <filesystem> #include <filesystem>
#include <sys/resource.h>
#include <unistd.h>
#include <opencv2/core.hpp> #include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp> #include <opencv2/imgproc.hpp>
@ -71,7 +76,54 @@ void ImagePipeline::submit(const Frame& frame) {
cv_.notify_one(); cv_.notify_one();
} }
static long long steadyMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
void ImagePipeline::noteSaveTime(double ms) {
const long long now = steadyMs();
std::lock_guard<std::mutex> lock(timing_mutex_);
save_times_.emplace_back(now, ms);
while (!save_times_.empty() && now - save_times_.front().first > 60000) save_times_.pop_front();
if (timing_logged_ms_ == 0) timing_logged_ms_ = now;
if (now - timing_logged_ms_ < 60000) return;
timing_logged_ms_ = now;
double sum = 0.0, max = 0.0;
for (const auto& s : save_times_) {
sum += s.second;
max = std::max(max, s.second);
}
LOG_INFO << "saving: " << save_times_.size() << " picture"
<< (save_times_.size() == 1 ? "" : "s") << " in the last minute, "
<< static_cast<long>(sum / static_cast<double>(save_times_.size()) + 0.5)
<< " ms each on average (longest " << static_cast<long>(max + 0.5) << " ms)";
}
ImagePipeline::SaveTiming ImagePipeline::saveTiming() const {
const long long now = steadyMs();
std::lock_guard<std::mutex> lock(timing_mutex_);
SaveTiming t;
double sum = 0.0;
for (const auto& s : save_times_) {
if (now - s.first > 60000) continue;
++t.count;
sum += s.second;
t.max_ms = std::max(t.max_ms, s.second);
}
if (t.count > 0) t.mean_ms = sum / t.count;
return t;
}
void ImagePipeline::run() { void ImagePipeline::run() {
// Lower this worker's priority (Linux: nice is per thread, and the encoder's
// own threads inherit it). Best effort: a failure only costs the head start.
if (kWorkerNice != 0) {
errno = 0;
const int now = getpriority(PRIO_PROCESS, static_cast<id_t>(::gettid()));
if (errno == 0 && now < kWorkerNice)
setpriority(PRIO_PROCESS, static_cast<id_t>(::gettid()), kWorkerNice);
}
while (running_) { while (running_) {
QueuedFrame queued; QueuedFrame queued;
{ {
@ -121,7 +173,11 @@ void ImagePipeline::process(const QueuedFrame& queued) {
return; return;
} }
const fs::path file = dir / (std::to_string(frame.timestamp_ms) + ".jxl"); const fs::path file = dir / (std::to_string(frame.timestamp_ms) + ".jxl");
if (!save(frame, out, file.string())) return; const auto t0 = std::chrono::steady_clock::now();
ThermalStats stats;
if (!save(frame, out, file.string(), &stats)) return;
noteSaveTime(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0)
.count());
// Sampled at submit(), not here: by this point the encode above has run and // Sampled at submit(), not here: by this point the encode above has run and
// the gimbal may already be slewing to the next waypoint. // the gimbal may already be slewing to the next waypoint.
@ -132,10 +188,25 @@ void ImagePipeline::process(const QueuedFrame& queued) {
ev.heading_decideg = static_cast<int>(o.yaw_deg * 10); ev.heading_decideg = static_cast<int>(o.yaw_deg * 10);
ev.pitch_decideg = static_cast<int>(o.pitch_deg * 10); ev.pitch_decideg = static_cast<int>(o.pitch_deg * 10);
ev.timestamp_ms = frame.timestamp_ms; ev.timestamp_ms = frame.timestamp_ms;
if (frame.thermal) {
ThermalEventFields t;
t.cal = frame.thermal->cal;
t.missed = frame.thermal->missed_packets;
t.stats_ok = stats.valid;
t.tmax_decic = stats.tmax_decic;
t.tmed_decic = stats.tmed_decic;
t.hotx = stats.hotx;
t.hoty = stats.hoty;
ev.thermal = t;
if (!stats.valid)
LOG_WARN << "Thermal frame " << frame.timestamp_ms
<< " has no pixel in the valid range; saved, but without temperatures";
}
channel_.publishCamEvent(ev); channel_.publishCamEvent(ev);
{ {
std::lock_guard<std::mutex> lock(last_event_mutex_); std::lock_guard<std::mutex> lock(last_event_mutex_);
last_event_ = ev; last_event_ = ev;
if (ev.thermal) last_thermal_event_ = ev;
} }
saved_.fetch_add(1); saved_.fetch_add(1);
@ -143,21 +214,30 @@ void ImagePipeline::process(const QueuedFrame& queued) {
<< (known ? layoutName(frame.layout) : "legacy") << ')'; << (known ? layoutName(frame.layout) : "legacy") << ')';
} }
bool ImagePipeline::save(const Frame& frame, const CameraOutput& out, const std::string& file) { bool ImagePipeline::save(const Frame& frame, const CameraOutput& out, const std::string& file,
ThermalStats* stats) {
int code = 0; int code = 0;
bool rotate = rotateCode(out.rotation_deg, code); bool rotate = rotateCode(out.rotation_deg, code);
const int h = static_cast<int>(frame.height), w = static_cast<int>(frame.width); const int h = static_cast<int>(frame.height), w = static_cast<int>(frame.width);
auto* data = const_cast<uint8_t*>(frame.data.data()); auto* data = const_cast<uint8_t*>(frame.data.data());
cv::Mat img; // what gets encoded: 8-bit (CV_8U) or 12-bit values in CV_16U cv::Mat img; // what gets encoded: 8-bit (CV_8U), or 12- or 16-bit values in CV_16U
int bits = 8; int bits = 8;
if (frame.layout == PixelLayout::BayerRG8 || frame.layout == PixelLayout::BayerRG12p) { float distance = static_cast<float>(out.jxl_distance);
if (frame.layout == PixelLayout::Mono16) {
// Thermal counts: every bit is measurement, so always lossless whatever
// the camera's jxl_distance says (the config loader refuses anything
// else for a thermal camera; this is the backstop).
img = cv::Mat(h, w, CV_16UC1, data);
bits = 16;
distance = 0.f;
} else if (layoutIsBayer(frame.layout)) {
cv::Mat mosaic; cv::Mat mosaic;
if (frame.layout == PixelLayout::BayerRG8) { if (frame.layout == PixelLayout::BayerRG8) {
mosaic = cv::Mat(h, w, CV_8UC1, data); mosaic = cv::Mat(h, w, CV_8UC1, data);
} else { } else {
mosaic.create(h, w, CV_16UC1); mosaic.create(h, w, CV_16UC1);
unpack12p(frame.data.data(), reinterpret_cast<uint16_t*>(mosaic.data), unpack12(frame.layout, frame.data.data(), reinterpret_cast<uint16_t*>(mosaic.data),
static_cast<size_t>(w) * h); static_cast<size_t>(w) * h);
bits = 12; bits = 12;
} }
@ -176,9 +256,14 @@ bool ImagePipeline::save(const Frame& frame, const CameraOutput& out, const std:
} }
if (!img.isContinuous()) img = img.clone(); if (!img.isContinuous()) img = img.clone();
// On the image as stored, so the hot spot's position matches the file.
if (stats && frame.thermal && frame.layout == PixelLayout::Mono16)
*stats = thermalStats(reinterpret_cast<const uint16_t*>(img.data),
static_cast<uint32_t>(img.cols), static_cast<uint32_t>(img.rows),
*frame.thermal);
if (!JpegXlEncoder::encodeToFile(file, img.data, img.cols, img.rows, img.channels(), bits, if (!JpegXlEncoder::encodeToFile(file, img.data, img.cols, img.rows, img.channels(), bits,
static_cast<float>(out.jxl_distance), out.jxl_effort, distance, out.jxl_effort, params_.jxl_threads)) {
params_.jxl_threads)) {
LOG_ERROR << "Failed to encode " << file; LOG_ERROR << "Failed to encode " << file;
return false; return false;
} }
@ -190,4 +275,9 @@ std::optional<CamEvent> ImagePipeline::lastEvent() const {
return last_event_; return last_event_;
} }
std::optional<CamEvent> ImagePipeline::lastThermalEvent() const {
std::lock_guard<std::mutex> lock(last_event_mutex_);
return last_thermal_event_;
}
} // namespace fgc } // namespace fgc

61
src/camera/IrbgrabApi.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "fgc/IrbgrabApi.h"
#include <cstdio>
#include <type_traits>
#include <dlfcn.h>
namespace fgc::irbgrab {
std::shared_ptr<const Api> load(const std::string& library, std::string& error) {
// Never dlclose()d: the SDK starts threads and loads plugins of its own.
void* lib = dlopen(library.c_str(), RTLD_NOW | RTLD_GLOBAL);
if (!lib) {
const char* e = dlerror();
error = "cannot load " + library + ": " + (e ? e : "unknown error");
return nullptr;
}
auto api = std::make_shared<Api>();
bool missing = false;
auto sym = [&](auto& fn, const char* name) {
fn = reinterpret_cast<std::remove_reference_t<decltype(fn)>>(dlsym(lib, name));
if (!fn && !missing) {
missing = true;
error = library + " has no " + name + " (not the IRBGRAB v4 SDK?)";
}
};
sym(api->dll_init, "irbgrab_dll_init");
sym(api->dll_uninit, "irbgrab_dll_uninit");
sym(api->dll_version, "irbgrab_dll_version");
sym(api->dll_devicetypenames, "irbgrab_dll_devicetypenames");
sym(api->dev_create, "irbgrab_dev_create");
sym(api->dev_free, "irbgrab_dev_free");
sym(api->dev_search, "irbgrab_dev_search");
sym(api->dev_connect, "irbgrab_dev_connect");
sym(api->dev_disconnect, "irbgrab_dev_disconnect");
sym(api->dev_startgrab, "irbgrab_dev_startgrab");
sym(api->dev_stopgrab, "irbgrab_dev_stopgrab");
sym(api->dev_getparam, "irbgrab_dev_getparam");
sym(api->dev_setparam, "irbgrab_dev_setparam");
sym(api->dev_getdata, "irbgrab_dev_getdata");
sym(api->mem_free, "irbgrab_mem_free");
sym(api->mem_getdimension, "irbgrab_mem_getdimension");
sym(api->mem_getdataptr, "irbgrab_mem_getdataptr");
sym(api->mem_getinfo, "irbgrab_mem_getinfo");
sym(api->mem_getdvlut, "irbgrab_mem_getdvlut");
if (missing) return nullptr;
return api;
}
std::string describe(Ret r) {
char b[16];
std::snprintf(b, sizeof(b), "0x%08X", static_cast<unsigned>(r));
std::string s = b;
if (r == kSuccess) s += " (ok)";
else if (r == kErrNotSupported) s += " (not supported)";
else if (r == kErrNotFound) s += " (not found)";
else if (r == kErrNucOrLicence) s += " (NUC in progress or invalid licence)";
return s;
}
} // namespace fgc::irbgrab

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,8 @@ PixelLayout layoutOf(VmbPixelFormatType f) {
case VmbPixelFormatRgb8: return PixelLayout::RGB8; case VmbPixelFormatRgb8: return PixelLayout::RGB8;
case VmbPixelFormatBayerRG8: return PixelLayout::BayerRG8; case VmbPixelFormatBayerRG8: return PixelLayout::BayerRG8;
case VmbPixelFormatBayerRG12p: return PixelLayout::BayerRG12p; case VmbPixelFormatBayerRG12p: return PixelLayout::BayerRG12p;
// The older GigE cameras' 12 bits (Manta): packed differently, see RawImage.h.
case VmbPixelFormatBayerRG12Packed: return PixelLayout::BayerRG12Packed;
default: return PixelLayout::Unknown; default: return PixelLayout::Unknown;
} }
} }
@ -125,7 +127,7 @@ public:
warned_layout_ = true; warned_layout_ = true;
LOG_WARN << "camera cam" << cam_id_ << ": pixel format 0x" << std::hex LOG_WARN << "camera cam" << cam_id_ << ": pixel format 0x" << std::hex
<< pf << std::dec << " is not one the program handles " << pf << std::dec << " is not one the program handles "
<< "(Mono8, RGB8, BayerRG8, BayerRG12p)"; << "(Mono8, RGB8, BayerRG8, BayerRG12p, BayerRG12Packed)";
} }
} }
f.data.assign(buf, buf + bytes); f.data.assign(buf, buf + bytes);
@ -188,12 +190,37 @@ private:
bool has_ = false; bool has_ = false;
}; };
// The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) predate
// the SFNC names the program uses and the Alvium answers to. Where a camera has
// no feature by the program's name, it is looked up by the older one. The
// IntensityController pair is the Alvium's auto-exposure target and tolerance;
// the Manta's ExposureAuto* pair means the same, in percent too.
const char* legacyFeatureName(const std::string& name) {
static const std::pair<const char*, const char*> kAliases[] = {
{"ExposureTime", "ExposureTimeAbs"},
{"AcquisitionFrameRate", "AcquisitionFrameRateAbs"},
{"IntensityControllerTarget", "ExposureAutoTarget"},
{"IntensityControllerTolerance", "ExposureAutoAdjustTol"},
};
for (const auto& [sfnc, legacy] : kAliases)
if (name == sfnc) return legacy;
return nullptr;
}
// A feature by the program's name, or by its older name on a camera that has only
// that one.
bool findFeature(const CameraPtr& cam, const std::string& name, FeaturePtr& f) {
if (SP_ACCESS(cam)->GetFeatureByName(name.c_str(), f) == VmbErrorSuccess) return true;
const char* legacy = legacyFeatureName(name);
return legacy && SP_ACCESS(cam)->GetFeatureByName(legacy, f) == VmbErrorSuccess;
}
// Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for // Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for
// telemetry). Numeric takes an integer feature too: some are Int on one model and // telemetry). Numeric takes an integer feature too: some are Int on one model and
// Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c). // Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c).
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) { double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
FeaturePtr f; FeaturePtr f;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback; if (!findFeature(cam, name, f)) return fallback;
double v = fallback; double v = fallback;
if (f->GetValue(v) == VmbErrorSuccess) return v; if (f->GetValue(v) == VmbErrorSuccess) return v;
VmbInt64_t i = 0; VmbInt64_t i = 0;
@ -203,7 +230,7 @@ double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
std::string getString(const CameraPtr& cam, const char* name) { std::string getString(const CameraPtr& cam, const char* name) {
FeaturePtr f; FeaturePtr f;
std::string v; std::string v;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) == VmbErrorSuccess && f->GetValue(v) == VmbErrorSuccess) if (findFeature(cam, name, f) && f->GetValue(v) == VmbErrorSuccess)
return v; return v;
return {}; return {};
} }
@ -649,8 +676,7 @@ bool VimbaCameraSource::setFeature(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
// The value before the first change, so restoreProfile() can put it back // The value before the first change, so restoreProfile() can put it back
// live. Only the first: a second change must not record the test's own value. // live. Only the first: a second change must not record the test's own value.
@ -741,8 +767,7 @@ bool VimbaCameraSource::readFeature(int camera_index, const std::string& name, d
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -760,8 +785,7 @@ bool VimbaCameraSource::featureRange(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -788,7 +812,7 @@ bool VimbaCameraSource::restoreProfile(int camera_index) {
for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) { for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) {
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(it->first.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], it->first, f) ||
f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; } f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; }
VmbErrorType rc = VmbErrorWrongType; VmbErrorType rc = VmbErrorWrongType;
try { try {

View File

@ -30,6 +30,8 @@
#include "fgc/OperatingMode.h" #include "fgc/OperatingMode.h"
#include "fgc/RemoteCommands.h" #include "fgc/RemoteCommands.h"
#include "fgc/TestRunner.h" #include "fgc/TestRunner.h"
#include "fgc/HostMetrics.h"
#include "fgc/ThermalGuard.h"
#include "fgc/mock/MockCameraSource.h" #include "fgc/mock/MockCameraSource.h"
#include "fgc/mock/MockEnvSensor.h" #include "fgc/mock/MockEnvSensor.h"
#include "fgc/mock/MockImuSource.h" #include "fgc/mock/MockImuSource.h"
@ -61,6 +63,9 @@
#include <string> #include <string>
#include <thread> #include <thread>
#include "fgc/CompositeCameraSource.h"
#include "fgc/TarisCameraSource.h"
#if FGC_WITH_MQTT #if FGC_WITH_MQTT
#include "fgc/MqttControlChannel.h" #include "fgc/MqttControlChannel.h"
#endif #endif
@ -297,6 +302,16 @@ struct Application::Impl {
long long fan_seen_ms_ = 0; // last ENV reading checkFan() judged long long fan_seen_ms_ = 0; // last ENV reading checkFan() judged
int fan_stall_count_ = 0; // consecutive stalled readings int fan_stall_count_ = 0; // consecutive stalled readings
bool fan_stalled_ = false; bool fan_stalled_ = false;
// The CPU guard (ThermalGuard.h): sampled every [Thermal] sample_ms from the
// control loop; it slows or pauses the scan when the computer runs hot.
ThermalGuard thermal_;
long long thermal_next_ms_ = 0; // steady ms of the next sample
hostmetrics::CpuTimes cpu_prev_; // for busy % between samples
hostmetrics::CpuFreqInfo cpu_freq_; // last sample, for the UI
hostmetrics::LoadInfo cpu_load_;
bool thermal_camera_stopped_ = false; // the guard stopped the stream
long long health_sent_ms_ = 0; // steady ms of the last Health
std::string health_sent_key_; // state+throttling last published
std::vector<HeadingSighting> heading_marks_; // `gimbal heading mark`s this setup std::vector<HeadingSighting> heading_marks_; // `gimbal heading mark`s this setup
std::atomic<bool> running{true}; std::atomic<bool> running{true};
@ -408,6 +423,9 @@ struct Application::Impl {
#endif #endif
} }
// One source for the colour cameras (Vimba, or the mock), one per thermal
// camera, behind a CompositeCameraSource when there is more than one. The
// colour source leads (CameraConfig::cameras puts its cameras first).
std::unique_ptr<ICameraSource> makeCamera() { std::unique_ptr<ICameraSource> makeCamera() {
bool mock = !realCameraWanted(); bool mock = !realCameraWanted();
if (mock && !cfg.features.enable_camera) { if (mock && !cfg.features.enable_camera) {
@ -419,10 +437,34 @@ struct Application::Impl {
LOG_WARN << "Camera requested but binary built without Vimba X; using mock camera"; LOG_WARN << "Camera requested but binary built without Vimba X; using mock camera";
} }
#endif #endif
const auto active = cfg.camera.active(); std::vector<const CameraSpec*> colour, thermal;
for (const auto* c : cfg.camera.active()) (c->thermal() ? thermal : colour).push_back(c);
// With the camera count each source is configured with: Vimba reports
// none until open(), and the thermal camera's index must not move.
std::vector<std::unique_ptr<ICameraSource>> sources;
std::vector<int> counts;
if (!colour.empty() || thermal.empty()) {
sources.push_back(makeColourCamera(colour, mock));
counts.push_back(colour.empty() ? 1 : static_cast<int>(colour.size()));
}
for (const auto* c : thermal) {
sources.push_back(makeThermalCamera(*c));
counts.push_back(1);
}
if (sources.size() == 1) return std::move(sources.front());
return std::make_unique<CompositeCameraSource>(std::move(sources), std::move(counts));
}
std::unique_ptr<ICameraSource> makeColourCamera(const std::vector<const CameraSpec*>& active,
bool mock) {
if (mock) { if (mock) {
return std::make_unique<MockCameraSource>( const int n = active.empty() ? 1 : static_cast<int>(active.size());
active.empty() ? 1 : static_cast<int>(active.size())); if (opts.mock_camera_full.value_or(cfg.features.mock_camera_full)) {
LOG_INFO << "[mock] camera frames are tower-size: 5328x3040 12-bit raw";
return std::make_unique<MockCameraSource>(n, 5328, 3040, PixelLayout::BayerRG12p);
}
return std::make_unique<MockCameraSource>(n);
} }
#if FGC_WITH_VIMBA #if FGC_WITH_VIMBA
std::vector<CameraSpec> specs; std::vector<CameraSpec> specs;
@ -437,6 +479,19 @@ struct Application::Impl {
#endif #endif
} }
// A thermal camera needs no build option: its SDK is loaded at run time, and
// a tower without it just has that camera offline. Only a mock or disabled
// camera run gets synthetic counts instead.
std::unique_ptr<ICameraSource> makeThermalCamera(const CameraSpec& c) {
if (!cfg.features.enable_camera || opts.mock_camera.value_or(cfg.features.mock_camera)) {
LOG_INFO << "[mock] thermal camera '" << c.name << "': synthetic 640x480 counts";
return std::make_unique<MockCameraSource>(1, 640, 480, PixelLayout::Mono16);
}
LOG_INFO << "thermal camera '" << c.name << "': TarisIR " << c.id << ", profile "
<< c.calib_profile << ", " << c.frame_rate_hz << " Hz";
return std::make_unique<TarisCameraSource>(TarisCameraSource::fromSpec(c));
}
// Load every profile of every camera up front. Two reasons: a profile nobody // Load every profile of every camera up front. Two reasons: a profile nobody
// is using today still fails at startup rather than the first time someone // is using today still fails at startup rather than the first time someone
// switches to it, and a settings XML edited in the Viewer is detected and // switches to it, and a settings XML edited in the Viewer is detected and
@ -448,6 +503,14 @@ struct Application::Impl {
for (const auto& c : cfg.camera.cameras) { for (const auto& c : cfg.camera.cameras) {
if (!c.enabled) continue; if (!c.enabled) continue;
if (c.thermal()) {
// No settings file: the thermal camera is set up from its config
// keys, and its profile only names a timing block.
active_profile[c.name] = c.profile;
LOG_INFO << "camera '" << c.name << "': thermal, no settings XML; timing '"
<< c.profile << "'";
continue;
}
std::map<std::string, fgc::ProfileLoadResult> results; std::map<std::string, fgc::ProfileLoadResult> results;
auto profiles = fgc::loadCameraProfiles(c.profile_dir, results); auto profiles = fgc::loadCameraProfiles(c.profile_dir, results);
@ -484,6 +547,26 @@ struct Application::Impl {
// Switch one camera's profile by name. With no camera named, applies to the // Switch one camera's profile by name. With no camera named, applies to the
// single configured camera. Returns false (and warns) if unknown. // single configured camera. Returns false (and warns) if unknown.
bool applyProfile(const std::string& cam_name, const std::string& name) { bool applyProfile(const std::string& cam_name, const std::string& name) {
for (const auto& c : cfg.camera.cameras) {
if (c.name != cam_name || !c.thermal()) continue;
// A thermal camera's profile is its timing block and nothing else.
if (!c.enabled) {
LOG_WARN << "camera '" << cam_name << "' is configured but not enabled";
return false;
}
if (name != c.profile && c.timing.find(name) == c.timing.end()) {
std::string known = c.profile;
for (const auto& [n, t] : c.timing)
if (n != c.profile) known += ", " + n;
LOG_WARN << "unknown profile '" << name << "' for camera '" << cam_name
<< "' (available: " << known << ')';
return false;
}
active_profile[cam_name] = name;
configureSettle();
LOG_INFO << "camera '" << cam_name << "' profile -> " << name << " (timing only)";
return true;
}
auto ci = camera_profiles.find(cam_name); auto ci = camera_profiles.find(cam_name);
if (ci == camera_profiles.end()) { if (ci == camera_profiles.end()) {
std::string known; std::string known;
@ -535,6 +618,15 @@ struct Application::Impl {
rule = rule || t.settle_rule == "brightness"; rule = rule || t.settle_rule == "brightness";
} }
scheduler->setSettleWait(settle, timeout); scheduler->setSettleWait(settle, timeout);
// The thermal camera hands over the newest frame it has, up to
// max_frame_age_ms old: older than the settle wait, and it may have been
// taken while the gimbal still moved.
for (const auto* c : cfg.camera.active())
if (c->thermal() && c->max_frame_age_ms > settle)
LOG_WARN << "camera '" << c->name << "': a frame may be up to "
<< c->max_frame_age_ms << " ms old, but the scan waits only " << settle
<< " ms after standstill - lower max_frame_age_ms, or the thermal "
"image may be blurred by the last move";
SettleRule::Params p; SettleRule::Params p;
if (rule && camera) { if (rule && camera) {
double v = 0; double v = 0;
@ -903,6 +995,80 @@ struct Application::Impl {
} }
} }
// --- The computer's CPU (the guard's last sample; no /sys reads here) ---
if (thermal_.haveReading()) {
const HostReading& r = thermal_.last();
const ThermalParams& p = thermal_.params();
HostView& h = s.host;
h.present = true;
h.temp_ok = r.temp_ok;
h.cpu_c = r.cpu_c;
h.zone = r.zone;
h.clock_ok = cpu_freq_.ok;
h.mean_mhz = cpu_freq_.mean_mhz;
h.core_mhz = cpu_freq_.core_mhz;
h.hw_max_mhz = cpu_freq_.hw_max_mhz;
h.cap_mhz = cpu_freq_.cap_mhz;
h.busy_pct = r.busy_pct;
h.load1 = cpu_load_.load1;
h.load5 = cpu_load_.load5;
h.load15 = cpu_load_.load15;
h.cpus = cpu_load_.cpus;
h.throttling = thermal_.throttle().throttling();
h.throttle_cause = thermal_.throttle().cause();
h.counters_ok = r.count_ok;
h.throttle_total = thermal_.throttle().eventsTotal();
h.throttle_last_min = thermal_.throttle().eventsLastMinute();
h.guard_on = thermal_.enabled();
h.state = guardStateName(thermal_.state());
h.reason = thermal_.reason();
h.slow_c = p.slow_c;
h.pause_c = p.pause_c;
h.resume_c = p.resume_c;
h.interval_s = scheduler ? scheduler->intervalMs() / 1000.0 : 0.0;
h.held_s = thermal_.heldForSaveMs() / 1000;
h.paused_s = thermal_.pausedForMs() / 1000;
if (pipeline) {
const auto t = pipeline->saveTiming();
h.save_count = t.count;
h.save_mean_ms = t.mean_ms;
h.save_max_ms = t.max_ms;
}
auto& g = s.sensors.host;
g.present = true;
g.status = "live";
auto set = [](SensorField& f, double v, int decimals) {
char b[32];
std::snprintf(b, sizeof(b), "%.*f", decimals, v);
f.value = b;
f.present = true;
};
if (g.fields.size() >= 4) {
if (r.temp_ok) set(g.fields[0], r.cpu_c, 1);
if (cpu_freq_.ok) set(g.fields[1], cpu_freq_.mean_mhz / 1000.0, 1);
if (r.busy_pct >= 0.0) set(g.fields[2], r.busy_pct, 0);
const long long n = thermal_.throttle().eventsLastMinute();
g.fields[3].value = !h.throttling ? "no"
: n > 0 ? "yes, " + std::to_string(n) + "x last min"
: "yes (clock)";
g.fields[3].present = true;
}
// The loudest thing that is true: paused, slowed, throttling, hot.
if (thermal_.state() == GuardState::Paused) {
s.sensors.cpu_badge = "PAUSED";
s.sensors.cpu_badge_red = true;
} else if (thermal_.state() == GuardState::Slowed) {
s.sensors.cpu_badge = "SLOWED";
} else if (h.throttling) {
s.sensors.cpu_badge = "THROTTLED";
} else if (r.temp_ok && r.cpu_c >= p.slow_c) {
s.sensors.cpu_badge = "HOT";
}
if (!thermal_.enabled() && !s.sensors.cpu_badge.empty())
s.sensors.cpu_badge += " (guard off)";
}
// --- Camera / capture --- // --- Camera / capture ---
s.capture.present = true; s.capture.present = true;
s.capture.continuous = scheduler && scheduler->continuous(); s.capture.continuous = scheduler && scheduler->continuous();
@ -947,8 +1113,10 @@ struct Application::Impl {
// Live per-camera identity/telemetry/stats. // Live per-camera identity/telemetry/stats.
s.capture.devices.clear(); s.capture.devices.clear();
if (camera) { if (camera) {
int index = 0;
for (const auto& d : camera->deviceInfo()) { for (const auto& d : camera->deviceInfo()) {
CameraDeviceView v; CameraDeviceView v;
v.offline = camera->offline(index++);
v.id = d.id; v.model = d.model; v.serial = d.serial; v.streaming = d.streaming; v.id = d.id; v.model = d.model; v.serial = d.serial; v.streaming = d.streaming;
v.width = d.width; v.height = d.height; v.payload_bytes = d.payload_bytes; v.width = d.width; v.height = d.height; v.payload_bytes = d.payload_bytes;
v.exposure_us = d.exposure_us; v.gain_db = d.gain_db; v.exposure_us = d.exposure_us; v.gain_db = d.gain_db;
@ -977,6 +1145,14 @@ struct Application::Impl {
s.capture.last_pitch_deg = ev->pitch_decideg / 10.0; s.capture.last_pitch_deg = ev->pitch_decideg / 10.0;
s.capture.last_ts_ms = ev->timestamp_ms; s.capture.last_ts_ms = ev->timestamp_ms;
} }
if (auto ev = pipeline->lastThermalEvent(); ev && ev->thermal) {
s.capture.has_thermal = true;
s.capture.thermal_cal = ev->thermal->cal;
s.capture.thermal_stats_ok = ev->thermal->stats_ok;
s.capture.thermal_max_c = ev->thermal->tmax_decic / 10.0;
s.capture.thermal_med_c = ev->thermal->tmed_decic / 10.0;
s.capture.thermal_ts_ms = ev->timestamp_ms;
}
} }
// --- Connectivity --- // --- Connectivity ---
@ -1143,6 +1319,192 @@ struct Application::Impl {
fan_stalled_ = stalled; fan_stalled_ = stalled;
} }
static long long steadyNowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
static void logGuard(const std::vector<GuardLog>& lines) {
for (const auto& l : lines) {
if (l.first == GuardLevel::Warn) LOG_WARN << l.second;
else LOG_INFO << l.second;
}
}
// Every [Thermal] sample_ms: read the CPU's temperature, clock, throttle
// counters and busy time, hand them to the guard, log what it says, and
// publish Health when anything changed (or once a minute).
void watchHost() {
const long long now = steadyNowMs();
if (now < thermal_next_ms_) return;
thermal_next_ms_ = now + thermal_.params().sample_ms;
const std::string& root = thermal_.params().sysfs_root;
const auto th = hostmetrics::readThermal(root);
cpu_freq_ = hostmetrics::readCpuFreq(root);
const auto count = hostmetrics::readThrottleCount(root);
const auto times = hostmetrics::readCpuTimes(root);
cpu_load_ = hostmetrics::readLoad();
HostReading r;
r.now_ms = now;
r.temp_ok = th.ok;
r.cpu_c = th.cpu_temp_c;
r.zone = th.cpu_zone;
r.clock_ok = cpu_freq_.ok;
r.mhz = cpu_freq_.mean_mhz;
r.hw_max_mhz = cpu_freq_.hw_max_mhz;
r.count_ok = count.ok;
r.throttle_count = count.events;
r.busy_pct = hostmetrics::busyPercent(cpu_prev_, times);
cpu_prev_ = times;
// While paused the scan is not waiting for saving - it is parked.
r.held_for_save = scheduler && scheduler->holdingForSave() && !scheduler->paused();
r.held_total_ms = scheduler ? scheduler->heldForSaveTotalMs() : 0;
r.save_pending = pipeline ? static_cast<int>(pipeline->pending()) : 0;
r.base_interval_ms = cfg.image_rate() > 0.0 ? 1000.0 / cfg.image_rate() : 0.0;
r.save_ms_mean = pipeline ? pipeline->saveTiming().mean_ms : 0.0;
r.save_workers = cfg.hardware.save_workers;
logGuard(thermal_.update(r));
applyGuard();
publishHealth(false);
}
// The guard's verdict, applied to the scan - every control-loop pass, since
// the camera may only stop once the waypoint under way has finished.
void applyGuard() {
if (!scheduler) return;
scheduler->setIntervalScale(thermal_.intervalScale());
scheduler->setPaused(thermal_.pauseCapture());
if (!camera) return;
// Paused and parked: stop the camera stream, so the CPU all but idles (it
// copies every frame it streams). Never under a running test or homing,
// which may need the camera.
const bool shooting = scheduler->mode() != Mode::Manual || scheduler->continuous();
if (scheduler->pausedAtRest() && shooting && !thermal_camera_stopped_ &&
!procedureBusy()) {
camera->stop();
thermal_camera_stopped_ = true;
LOG_INFO << "camera stream stopped while pictures are paused";
} else if (!scheduler->paused() && thermal_camera_stopped_) {
thermal_camera_stopped_ = false;
// Back on only where something will take pictures: a scan, or
// continuous capture. In plain manual it stays off, as setMode leaves it.
if (scheduler->mode() != Mode::Manual || scheduler->continuous()) {
camera->start();
LOG_INFO << "camera stream started again";
}
}
}
HealthReport healthReport() const {
HealthReport h;
const HostReading& r = thermal_.last();
h.tower = cfg.general.tower_name;
h.state = guardStateName(thermal_.state());
h.reason = thermal_.reason();
h.guard_on = thermal_.enabled();
h.cpu_c_ok = r.temp_ok;
h.cpu_c = r.cpu_c;
h.cpu_mhz_ok = r.clock_ok;
h.cpu_mhz = r.mhz;
h.cpu_busy_pct = r.busy_pct;
h.throttling = thermal_.throttle().throttling();
h.throttle_events_last_min = thermal_.throttle().eventsLastMinute();
if (env) {
if (auto m = env->sample()) {
h.enclosure_c_ok = true;
h.enclosure_c = m->temp_c;
}
}
if (pipeline) {
const auto t = pipeline->saveTiming();
h.save_count_last_min = t.count;
h.save_ms_mean = t.mean_ms;
h.save_ms_max = t.max_ms;
}
if (camera) {
// Only a camera the scan can do without is ever offline (the
// thermal one); the colour camera failing stops the run instead.
const auto active = cfg.camera.active();
for (int i = 0; i < camera->cameraCount() && i < static_cast<int>(active.size()); ++i)
if (!camera->offline(i).empty())
h.cams_offline += (h.cams_offline.empty() ? "" : ",") + active[i]->label;
}
h.held_s = thermal_.heldForSaveMs() / 1000;
h.interval_s = scheduler ? scheduler->intervalMs() / 1000.0 : 0.0;
h.timestamp_ms = nowEpochMs();
return h;
}
// On a change of state or throttling, or once a minute; `force` for a command.
void publishHealth(bool force) {
if (!channel || !thermal_.haveReading()) return;
const long long now = steadyNowMs();
const std::string key = std::string(guardStateName(thermal_.state())) +
(thermal_.throttle().throttling() ? "+t" : "") +
(thermal_.enabled() ? "" : "+off");
if (!force && key == health_sent_key_ && now - health_sent_ms_ < 60000) return;
health_sent_key_ = key;
health_sent_ms_ = now;
channel->publishHealth(healthReport());
}
// `thermal [resume|off|on]`: the CPU's heat and the scan's pace for it.
void handleThermal(const std::string& what) {
const long long now = steadyNowMs();
if (what == "resume") {
logGuard(thermal_.forceResume(now));
} else if (what == "off" || what == "on") {
logGuard(thermal_.setEnabled(what == "on", now));
} else if (!what.empty()) {
LOG_WARN << "usage: thermal [resume|off|on]";
return;
} else {
const HostReading& r = thermal_.last();
const ThermalParams& p = thermal_.params();
if (!thermal_.haveReading()) {
LOG_INFO << "CPU: no reading yet";
} else {
LOG_INFO << "CPU: "
<< (r.temp_ok ? celsiusText(r.cpu_c) + " (" + r.zone + ")"
: std::string("temperature unknown"))
<< ", " << (r.clock_ok ? ghzText(r.mhz) : std::string("clock unknown"))
<< (r.hw_max_mhz > 0.0 ? " of " + ghzText(r.hw_max_mhz) : std::string())
<< ", " << (r.busy_pct >= 0.0 ? std::to_string(static_cast<int>(r.busy_pct + 0.5)) + "% busy"
: std::string("busy unknown"));
LOG_INFO << "throttling: "
<< (thermal_.throttle().throttling()
? "yes (" + thermal_.throttle().cause() + ")"
: std::string("no"))
<< ", " << thermal_.throttle().eventsLastMinute()
<< " in the last minute, " << thermal_.throttle().eventsTotal()
<< " since start";
}
char every[48] = "";
if (scheduler)
std::snprintf(every, sizeof(every), ", one picture every %.1f s",
scheduler->intervalMs() / 1000.0);
LOG_INFO << "scan pace: " << (thermal_.enabled() ? guardStateName(thermal_.state())
: "guard off")
<< (thermal_.reason().empty() ? "" : " - " + thermal_.reason()) << every;
if (pipeline) {
const auto t = pipeline->saveTiming();
if (t.count > 0)
LOG_INFO << "saving: " << t.count << " in the last minute, "
<< static_cast<long>(t.mean_ms + 0.5) << " ms each (longest "
<< static_cast<long>(t.max_ms + 0.5) << " ms)";
}
LOG_INFO << "limits: slows at " << celsiusText(p.slow_c) << ", pauses at "
<< celsiusText(p.pause_c) << " for " << p.pause_after_s
<< " s, resumes at " << celsiusText(p.resume_c) << " after at least "
<< p.min_pause_s << " s";
return;
}
applyGuard();
publishHealth(true);
}
void checkTrackingError() { void checkTrackingError() {
const long limit = cfg.enc_error_warn_counts; const long limit = cfg.enc_error_warn_counts;
if (limit <= 0) return; // disabled if (limit <= 0) return; // disabled
@ -1809,12 +2171,77 @@ struct Application::Impl {
// profile. For a stream that has stopped delivering and will not restart - // profile. For a stream that has stopped delivering and will not restart -
// the state tower 111's camera was in on 2026-09-24, which only this cleared. // the state tower 111's camera was in on 2026-09-24, which only this cleared.
// Manual mode only (allowedIn), so there is no scan to pause around it. // Manual mode only (allowedIn), so there is no scan to pause around it.
void resetCameras() { // With a camera named (block name or label), only that one: the thermal
// camera reconnects without rebooting the colour one.
void resetCameras(const std::string& which = "") {
const auto active = cfg.camera.active();
int only = -1;
auto lowered = [](std::string v) {
std::transform(v.begin(), v.end(), v.begin(),
[](unsigned char ch) { return std::tolower(ch); });
return v;
};
if (!which.empty()) {
for (size_t i = 0; i < active.size(); ++i)
if (lowered(active[i]->name) == lowered(which) ||
lowered(active[i]->label) == lowered(which))
only = static_cast<int>(i);
if (only < 0) {
LOG_WARN << "camera reset: no active camera '" << which << "'";
return;
}
}
camera->stop(); camera->stop();
for (int i = 0; i < camera->cameraCount(); ++i) for (int i = 0; i < camera->cameraCount(); ++i) {
LOG_INFO << "camera " << i << (camera->resetCamera(i) ? ": reset, back on its profile" if (only >= 0 && i != only) continue;
const std::string name = i < static_cast<int>(active.size())
? active[i]->name
: std::to_string(i);
LOG_INFO << "camera " << name
<< (camera->resetCamera(i) ? ": reset, back on its profile"
: ": reset FAILED (see above)"); : ": reset FAILED (see above)");
} }
}
// `camera calib [<profile>]`: the thermal camera's calibration profile,
// which sets the scale of its counts. Every frame says which it was taken
// on, so switching is safe for the archive; it is manual-mode only because
// the camera delivers nothing for the ~3 s it takes.
void handleCalib(const Command& c) {
int index = -1, i = 0;
for (const auto* s : cfg.camera.active()) {
if (s->thermal()) { index = i; break; }
++i;
}
if (index < 0) {
LOG_WARN << "camera calib: no thermal camera configured";
return;
}
const std::string why = camera->offline(index);
if (!why.empty()) {
LOG_WARN << "camera calib: the thermal camera is offline (" << why << ")";
return;
}
if (!c.has_value) {
double v = 0, k = 0;
if (camera->readFeature(index, "CalibProfile", v) &&
camera->readFeature(index, "KelvinPerCount", k))
LOG_INFO << "thermal calibration profile " << static_cast<int>(v) << ", " << k
<< " K per count";
else
LOG_INFO << "thermal calibration profile: no frame yet";
return;
}
const int profile = static_cast<int>(c.value);
LOG_INFO << "thermal camera: switching to calibration profile " << profile << " ...";
if (camera->setFeature(index, "CalibProfile", std::to_string(profile)))
LOG_INFO << "thermal camera on calibration profile " << profile
<< " (for this run; [Camera." << cfg.camera.active()[index]->name
<< "] calib_profile sets it at startup)";
else
LOG_WARN << "thermal camera: switching to calibration profile " << profile
<< " FAILED (see above)";
}
// The one way the mode changes - `mode ...`, the a / p / m keys, [Startup] mode, and a // The one way the mode changes - `mode ...`, the a / p / m keys, [Startup] mode, and a
// test pausing the scan all come here. Returns false, having said why and // test pausing the scan all come here. Returns false, having said why and
@ -1848,7 +2275,11 @@ struct Application::Impl {
if (scheduler->continuous()) LOG_INFO << "continuous capture off (mode change)"; if (scheduler->continuous()) LOG_INFO << "continuous capture off (mode change)";
if (m == Mode::Manual) camera->stop(); if (m == Mode::Manual) camera->stop();
else camera->start(); else camera->start();
// The stream is as the mode left it; the CPU guard stops it again if a
// pause still holds (applyGuard).
thermal_camera_stopped_ = false;
scheduler->setMode(m, pan); scheduler->setMode(m, pan);
if (m == Mode::Manual && was != Mode::Manual) stopScanMove();
if (m == Mode::Pan) { if (m == Mode::Pan) {
const auto& h = scheduler->panHeadingsNow(); const auto& h = scheduler->panHeadingsNow();
LOG_INFO << "mode pan: " << panText(pan) << " - " << h.size() LOG_INFO << "mode pan: " << panText(pan) << " - " << h.size()
@ -1861,6 +2292,22 @@ struct Application::Impl {
return true; return true;
} }
// Leaving a scan: stop the move it last sent, which the firmware would
// otherwise finish - a slow return to the first waypoint can take many
// seconds the operator spends waiting for the gimbal to answer. The firmware
// ramps the axis down and holds where it comes to rest (it does not resume
// the move), and the next MOVE or jog supersedes a stop still ramping down.
// Sent whether or not telemetry shows motion, since a MOVE sent this tick
// is not in it yet. Never during a homing, which a STOP ends in ERROR.
void stopScanMove() {
const MotorTelemetry t = motor->telemetry();
if (t.yaw.state == AxisState::Homing ||
(t.pitch_present && t.pitch.state == AxisState::Homing))
return;
if (anyLeftoverMove()) LOG_INFO << "stopping the gimbal where it is";
motor->sendCommand("STOP ALL");
}
// A pan the gimbal can actually do: its pitch inside the homed window (not // A pan the gimbal can actually do: its pitch inside the homed window (not
// clamped to it - that would image an elevation nobody asked for), and at // clamped to it - that would image an elevation nobody asked for), and at
// least one of its headings inside the reachable yaw arc. // least one of its headings inside the reachable yaw arc.
@ -2872,12 +3319,15 @@ struct Application::Impl {
} else if (c.verb == "exit") { } else if (c.verb == "exit") {
running = false; running = false;
} else if (c.verb == "camera") { } else if (c.verb == "camera") {
if (c.device == "reset") resetCameras(); if (c.device == "reset") resetCameras(c.option);
else LOG_WARN << "usage: camera reset"; else if (c.device == "calib") handleCalib(c);
else LOG_WARN << "usage: camera reset [<camera>] | camera calib [<profile>]";
} else if (c.verb == "snapshot") { } else if (c.verb == "snapshot") {
takeSnapshot(); takeSnapshot();
} else if (c.verb == "capture") { } else if (c.verb == "capture") {
handleCapture(c.device); handleCapture(c.device);
} else if (c.verb == "thermal") {
handleThermal(c.device);
} else if (c.verb == "mode") { } else if (c.verb == "mode") {
handleMode(line); handleMode(line);
} else if (c.verb == "debug") { } else if (c.verb == "debug") {
@ -3144,6 +3594,19 @@ struct Application::Impl {
// the wait is the longest across them, not the first one's. // the wait is the longest across them, not the first one's.
configureSettle(); configureSettle();
thermal_ = ThermalGuard(cfg.thermal);
if (cfg.thermal.enabled)
LOG_INFO << "CPU guard: pictures slow down at " << celsiusText(cfg.thermal.slow_c)
<< " or when the CPU throttles, and pause at "
<< celsiusText(cfg.thermal.pause_c) << " (resume at "
<< celsiusText(cfg.thermal.resume_c) << ")"
<< (cfg.thermal.sysfs_root != "/" ? "; readings from the simulated tree " +
cfg.thermal.sysfs_root
: std::string());
else
LOG_WARN << "CPU guard off ([Thermal] enabled = false): the scan keeps full pace "
"however hot the CPU runs";
// Before the firmware is talked to: the registers are wanted by the very // Before the firmware is talked to: the registers are wanted by the very
// first sendAnchorConfig(), on both the homing and the no-homing startup path. // first sendAnchorConfig(), on both the homing and the no-homing startup path.
planMotorCurrents(); planMotorCurrents();
@ -3164,6 +3627,12 @@ struct Application::Impl {
return 3; // distinct from the config (1) and lock (2) exits return 3; // distinct from the config (1) and lock (2) exits
} }
// Again, now the camera is open: the keep rule reads the camera's own
// auto-exposure target, tolerance and limits, and before open() it got
// none of them and judged every frame against its 50 +- 5 % default -
// until the first profile switch. Rietschen's cameras aim at 35 %.
configureSettle();
pipeline->start(); pipeline->start();
channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise
@ -3216,6 +3685,8 @@ struct Application::Impl {
scheduler->tick(); scheduler->tick();
checkTrackingError(); // warn on live encoder-error excursions checkTrackingError(); // warn on live encoder-error excursions
checkFan(); // a legacy tower's fan, driven but not turning checkFan(); // a legacy tower's fan, driven but not turning
watchHost(); // CPU heat and load: slow or pause the scan for it
applyGuard();
publishSnapshot(); publishSnapshot();
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }

View File

@ -42,10 +42,23 @@ CaptureScheduler::CaptureScheduler(IMotorController& motor, ICameraSource& camer
grid_(grid), grid_(grid),
now_ms_(now_ms ? std::move(now_ms) : steadyNowMs), now_ms_(now_ms ? std::move(now_ms) : steadyNowMs),
image_rate_(image_rate), image_rate_(image_rate),
interval_ms_(image_rate > 0.0 ? 1000.0 / image_rate : 0.0) { base_interval_ms_(image_rate > 0.0 ? 1000.0 / image_rate : 0.0),
interval_ms_(base_interval_ms_) {
timer_start_ = now_ms_(); timer_start_ = now_ms_();
} }
void CaptureScheduler::setIntervalScale(double scale) {
interval_scale_ = scale > 1.0 ? scale : 1.0;
interval_ms_ = base_interval_ms_ * interval_scale_;
}
void CaptureScheduler::setPaused(bool paused) {
if (paused == paused_) return;
paused_ = paused;
// The wait is announced by the guard, so the first move after it is no overrun.
if (paused) save_held_ = true;
}
void CaptureScheduler::setMode(Mode mode, const PanSpec& pan) { void CaptureScheduler::setMode(Mode mode, const PanSpec& pan) {
mode_ = mode; mode_ = mode;
pan_ = pan; pan_ = pan;
@ -97,6 +110,8 @@ void CaptureScheduler::sendMove() {
// form ("ERR axis not present") and moves nothing, so address yaw alone. // form ("ERR axis not present") and moves nothing, so address yaw alone.
motor_.sendCommand(moveCommand(yaw_target_, pitch_present_ ? std::optional<long>(pitch_target_) motor_.sendCommand(moveCommand(yaw_target_, pitch_present_ ? std::optional<long>(pitch_target_)
: std::nullopt)); : std::nullopt));
// The time the move and the settle wait take is free for the cameras.
camera_.moving();
} }
bool CaptureScheduler::settledAt(const MotorTelemetry& t) const { bool CaptureScheduler::settledAt(const MotorTelemetry& t) const {
@ -109,16 +124,28 @@ bool CaptureScheduler::settledAt(const MotorTelemetry& t) const {
bool CaptureScheduler::roomToSave() { bool CaptureScheduler::roomToSave() {
const bool room = !can_save_ || can_save_(); const bool room = !can_save_ || can_save_();
const long long now = now_ms_();
if (!room && !holding_) { if (!room && !holding_) {
holding_ = true; holding_ = true;
LOG_WARN << "saving is behind: holding the scan here until it catches up"; hold_started_ms_ = now;
// Said once, not at every waypoint: a saver that is a little too slow
// holds the scan briefly each time, and the CPU guard (ThermalGuard) is
// what speaks for that. A hold a minute after the last one is news again.
hold_logged_ = last_hold_end_ms_ < 0 || now - last_hold_end_ms_ >= 60000;
if (hold_logged_) LOG_WARN << "saving is behind: holding the scan here until it catches up";
} else if (room && holding_) { } else if (room && holding_) {
holding_ = false; holding_ = false;
LOG_INFO << "saving caught up: the scan carries on"; held_total_ms_ += now - hold_started_ms_;
last_hold_end_ms_ = now;
if (hold_logged_) LOG_INFO << "saving caught up: the scan carries on";
} }
return room; return room;
} }
long long CaptureScheduler::heldForSaveTotalMs() const {
return held_total_ms_ + (holding_ ? now_ms_() - hold_started_ms_ : 0);
}
const char* CaptureScheduler::phase() const { const char* CaptureScheduler::phase() const {
if (!moving_ || hold_.settled_ms == 0) return "moving"; if (!moving_ || hold_.settled_ms == 0) return "moving";
return hold_.ready_ms == 0 ? "settling" : "waiting"; return hold_.ready_ms == 0 ? "settling" : "waiting";
@ -343,7 +370,7 @@ void CaptureScheduler::tick() {
// on its own clock afterwards. // on its own clock afterwards.
if (mode_ == Mode::Manual) { if (mode_ == Mode::Manual) {
if (snapshot_pending_) serviceSnapshot(t); if (snapshot_pending_) serviceSnapshot(t);
else if (continuous_) serviceContinuous(t); else if (continuous_ && (!paused_ || continuous_hold_.due_ms != 0)) serviceContinuous(t);
return; return;
} }
@ -352,6 +379,7 @@ void CaptureScheduler::tick() {
// and wait out what is left of the interval at the end. // and wait out what is left of the interval at the end.
if (interval_ms_ <= 0.0) return; if (interval_ms_ <= 0.0) return;
if (!moving_) { if (!moving_) {
if (paused_) return; // the CPU guard: park here (setPaused)
if (!roomToSave()) { if (!roomToSave()) {
save_held_ = true; save_held_ = true;
return; return;

View File

@ -1,6 +1,7 @@
#include "fgc/ChannelTopics.h" #include "fgc/ChannelTopics.h"
#include <cctype> #include <cctype>
#include <cmath>
#include <cstdio> #include <cstdio>
namespace fgc { namespace fgc {
@ -9,11 +10,65 @@ std::string towerTopic(const std::string& tower, const std::string& leaf) {
return "GGS/FWT/" + tower + "/" + leaf; return "GGS/FWT/" + tower + "/" + leaf;
} }
namespace {
// Fixed-point, with a '.' whatever the C locale says. printf would follow
// LC_NUMERIC, and the thermal camera's SDK sets that to a comma locale when it
// loads (seen on tower 111: "hotx":0,4336) - which is no longer JSON.
std::string num(double v, int decimals) {
if (!std::isfinite(v)) return "null";
long long scale = 1;
for (int i = 0; i < decimals; ++i) scale *= 10;
const long long n = std::llround(v * static_cast<double>(scale));
const bool neg = n < 0;
const unsigned long long a = neg ? 0ULL - static_cast<unsigned long long>(n)
: static_cast<unsigned long long>(n);
std::string out = (neg ? "-" : "") + std::to_string(a / static_cast<unsigned long long>(scale));
if (decimals > 0) {
std::string frac = std::to_string(a % static_cast<unsigned long long>(scale));
out += "." + std::string(static_cast<size_t>(decimals) - frac.size(), '0') + frac;
}
return out;
}
} // namespace
std::string camEventJson(const CamEvent& e) { std::string camEventJson(const CamEvent& e) {
return "{ \"fwt\":\"" + e.tower + "\", \"cam\":\"" + e.camera + std::string out = "{ \"fwt\":" + jsonString(e.tower) + ", \"cam\":" + jsonString(e.camera) +
"\", \"hdg\":" + std::to_string(e.heading_decideg) + ", \"hdg\":" + std::to_string(e.heading_decideg) +
", \"pit\":" + std::to_string(e.pitch_decideg) + ", \"pit\":" + std::to_string(e.pitch_decideg) +
", \"time\":" + std::to_string(e.timestamp_ms) + " }"; ", \"time\":" + std::to_string(e.timestamp_ms);
if (e.thermal) {
const ThermalEventFields& t = *e.thermal;
auto opt = [&t](const std::string& v) { return t.stats_ok ? v : std::string("null"); };
out += ", \"cal\":" + std::to_string(t.cal) +
", \"missed\":" + std::to_string(t.missed) +
", \"tmax\":" + opt(std::to_string(t.tmax_decic)) +
", \"tmed\":" + opt(std::to_string(t.tmed_decic)) +
", \"hotx\":" + opt(num(t.hotx, 4)) +
", \"hoty\":" + opt(num(t.hoty, 4));
}
return out + " }";
}
std::string healthJson(const HealthReport& h) {
auto opt = [](bool ok, double v, int d) { return ok ? num(v, d) : std::string("null"); };
return "{\"fwt\":" + jsonString(h.tower) + ",\"state\":" + jsonString(h.state) +
",\"reason\":" + jsonString(h.reason) +
",\"guard\":" + (h.guard_on ? "true" : "false") +
",\"cpu_c\":" + opt(h.cpu_c_ok, h.cpu_c, 1) +
",\"cpu_mhz\":" + opt(h.cpu_mhz_ok, h.cpu_mhz, 0) +
",\"cpu_busy_pct\":" + opt(h.cpu_busy_pct >= 0.0, h.cpu_busy_pct, 0) +
",\"throttling\":" + (h.throttling ? "true" : "false") +
",\"throttle_events_last_min\":" + std::to_string(h.throttle_events_last_min) +
",\"enclosure_c\":" + opt(h.enclosure_c_ok, h.enclosure_c, 1) +
",\"saved_last_min\":" + std::to_string(h.save_count_last_min) +
",\"save_ms_mean\":" + opt(h.save_count_last_min > 0, h.save_ms_mean, 0) +
",\"save_ms_max\":" + opt(h.save_count_last_min > 0, h.save_ms_max, 0) +
",\"cams_offline\":" + jsonString(h.cams_offline) +
",\"held_s\":" + std::to_string(h.held_s) +
",\"interval_s\":" + num(h.interval_s, 1) +
",\"time\":" + std::to_string(h.timestamp_ms) + "}";
} }
std::string jsonString(const std::string& s) { std::string jsonString(const std::string& s) {

View File

@ -289,6 +289,48 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
if (cfg.hardware.save_queue_max < 1 || cfg.hardware.save_queue_max > 32) if (cfg.hardware.save_queue_max < 1 || cfg.hardware.save_queue_max > 32)
throw std::runtime_error("Hardware.save_queue_max must be in 1..32 (each raw frame " throw std::runtime_error("Hardware.save_queue_max must be in 1..32 (each raw frame "
"waiting holds ~24 MB)"); "waiting holds ~24 MB)");
// [Thermal]: the CPU guard (ThermalGuard.h). Per computer, so normally in
// hardware.ini; config.ini works too (hardware.ini wins).
{
ThermalParams& t = cfg.thermal;
t.enabled = getBool(kv, "Thermal.enabled", t.enabled);
t.sysfs_root = get(kv, "Thermal.sysfs_root", t.sysfs_root);
if (t.sysfs_root.empty()) t.sysfs_root = "/";
t.sample_ms = getInt(kv, "Thermal.sample_ms", t.sample_ms);
t.slow_c = getDouble(kv, "Thermal.slow_c", t.slow_c);
t.pause_c = getDouble(kv, "Thermal.pause_c", t.pause_c);
t.resume_c = getDouble(kv, "Thermal.resume_c", t.resume_c);
t.hysteresis_c = getDouble(kv, "Thermal.hysteresis_c", t.hysteresis_c);
t.pause_after_s = getInt(kv, "Thermal.pause_after_s", t.pause_after_s);
t.calm_s = getInt(kv, "Thermal.calm_s", t.calm_s);
t.min_pause_s = getInt(kv, "Thermal.min_pause_s", t.min_pause_s);
t.held_slow_s = getInt(kv, "Thermal.held_slow_s", t.held_slow_s);
t.held_pause_s = getInt(kv, "Thermal.held_pause_s", t.held_pause_s);
t.slow_factor = getDouble(kv, "Thermal.slow_factor", t.slow_factor);
t.held_share_pct = getDouble(kv, "Thermal.held_share_pct", t.held_share_pct);
if (t.held_share_pct <= 0.0 || t.held_share_pct > 100.0)
throw std::runtime_error("Thermal.held_share_pct must be in 0..100 (exclusive of 0)");
t.clock_low_pct = getDouble(kv, "Thermal.clock_low_pct", t.clock_low_pct);
t.busy_high_pct = getDouble(kv, "Thermal.busy_high_pct", t.busy_high_pct);
if (t.sample_ms < 500 || t.sample_ms > 60000)
throw std::runtime_error("Thermal.sample_ms must be in 500..60000");
if (!(t.resume_c < t.slow_c && t.slow_c < t.pause_c))
throw std::runtime_error("Thermal: need resume_c < slow_c < pause_c (degrees C)");
if (t.hysteresis_c < 0.0)
throw std::runtime_error("Thermal.hysteresis_c must be >= 0");
if (t.slow_factor < 1.0 || t.slow_factor > 10.0)
throw std::runtime_error("Thermal.slow_factor must be in 1..10 (times the interval)");
if (t.pause_after_s < 0 || t.calm_s < 0 || t.min_pause_s < 0 || t.held_slow_s < 1 ||
t.held_pause_s < 1)
throw std::runtime_error("Thermal: the *_s durations must be >= 0 (held_*_s >= 1)");
if (t.held_pause_s < t.held_slow_s)
throw std::runtime_error("Thermal: held_pause_s must be >= held_slow_s");
if (t.clock_low_pct <= 0.0 || t.clock_low_pct >= 100.0 || t.busy_high_pct < 0.0 ||
t.busy_high_pct > 100.0)
throw std::runtime_error("Thermal: clock_low_pct must be in 0..100 (exclusive), "
"busy_high_pct in 0..100");
}
for (const auto& a : cfg.hardware.axes) for (const auto& a : cfg.hardware.axes)
if (a != "yaw" && a != "pitch") if (a != "yaw" && a != "pitch")
throw std::runtime_error("Hardware.axes: unknown axis '" + a + throw std::runtime_error("Hardware.axes: unknown axis '" + a +
@ -341,9 +383,18 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
c.profile_dir = get(kv, pre + "profile_dir"); c.profile_dir = get(kv, pre + "profile_dir");
c.profile = get(kv, pre + "profile", c.profile); c.profile = get(kv, pre + "profile", c.profile);
c.rotation_deg = getInt(kv, pre + "rotation_deg", c.rotation_deg); c.rotation_deg = getInt(kv, pre + "rotation_deg", c.rotation_deg);
c.jxl_distance = getDouble(kv, pre + "jxl_distance", cfg.camera.jxl_distance); // A thermal camera's counts are measurements: lossless unless someone
// asks otherwise, and asking is refused below.
c.jxl_distance = getDouble(kv, pre + "jxl_distance",
c.driver == "taris" ? 0.0 : cfg.camera.jxl_distance);
c.jxl_effort = getInt(kv, pre + "jxl_effort", cfg.camera.jxl_effort); c.jxl_effort = getInt(kv, pre + "jxl_effort", cfg.camera.jxl_effort);
c.demosaic = lowered(get(kv, pre + "demosaic", cfg.camera.demosaic)); c.demosaic = lowered(get(kv, pre + "demosaic", cfg.camera.demosaic));
c.calib_profile = getInt(kv, pre + "calib_profile", c.calib_profile);
c.frame_rate_hz = getDouble(kv, pre + "frame_rate_hz", c.frame_rate_hz);
c.max_frame_age_ms = getInt(kv, pre + "max_frame_age_ms", c.max_frame_age_ms);
c.library = get(kv, pre + "library", c.library);
c.nuc_interval_s = getInt(kv, pre + "nuc_interval_s", c.nuc_interval_s);
c.interface = get(kv, pre + "interface");
// Per-profile timing: Camera.<name>.<profile>.settle_wait_ms // Per-profile timing: Camera.<name>.<profile>.settle_wait_ms
for (const auto& [key, val] : kv) { for (const auto& [key, val] : kv) {
@ -365,13 +416,31 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
"'fixed' or 'brightness'"); "'fixed' or 'brightness'");
} }
if (c.driver != "vimba") if (c.driver != "vimba" && c.driver != "taris")
throw std::runtime_error("Camera." + name + ".driver must be 'vimba' (got '" + throw std::runtime_error("Camera." + name + ".driver must be 'vimba' or 'taris' (got '" +
c.driver + "'); no other backend is implemented yet"); c.driver + "')");
if (c.enabled && c.id.empty()) if (c.enabled && c.id.empty())
throw std::runtime_error("Camera." + name + ".id is required (USB DEV_ id or GigE IP)"); throw std::runtime_error("Camera." + name + ".id is required (" +
if (c.profile_dir.empty()) (c.thermal() ? "the camera's serial number"
: "USB DEV_ id or GigE IP") + ")");
if (!c.thermal() && c.profile_dir.empty())
throw std::runtime_error("Camera." + name + ".profile_dir is required"); throw std::runtime_error("Camera." + name + ".profile_dir is required");
if (c.thermal()) {
if (c.jxl_distance != 0.0)
throw std::runtime_error("Camera." + name + ".jxl_distance must be 0: a thermal "
"camera's counts are always saved lossless");
if (c.calib_profile < 0)
throw std::runtime_error("Camera." + name + ".calib_profile must be >= 0");
if (!(c.frame_rate_hz > 0.0 && c.frame_rate_hz <= 50.0))
throw std::runtime_error("Camera." + name + ".frame_rate_hz must be in (0, 50]");
if (c.max_frame_age_ms <= 0)
throw std::runtime_error("Camera." + name + ".max_frame_age_ms must be > 0");
if (c.nuc_interval_s < 0 || c.nuc_interval_s > 3600)
throw std::runtime_error("Camera." + name + ".nuc_interval_s must be in 0..3600 "
"(0 = the camera's own timer)");
if (c.library.empty())
throw std::runtime_error("Camera." + name + ".library must not be empty");
}
if (c.jxl_distance < 0.0) if (c.jxl_distance < 0.0)
throw std::runtime_error("Camera." + name + ".jxl_distance must be >= 0 (0 = lossless)"); throw std::runtime_error("Camera." + name + ".jxl_distance must be >= 0 (0 = lossless)");
if (c.jxl_effort < 1 || c.jxl_effort > 9) if (c.jxl_effort < 1 || c.jxl_effort > 9)
@ -389,6 +458,27 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
} }
cfg.camera.cameras.push_back(std::move(c)); cfg.camera.cameras.push_back(std::move(c));
} }
// A thermal camera takes the newest frame it already has, so it needs no
// settle wait of its own; without a timing block it must not hold the scan
// to the 1000 ms default (the scan waits for the longest camera).
for (auto& c : cfg.camera.cameras)
if (c.thermal() && c.timing.find(c.profile) == c.timing.end())
c.timing[c.profile] = CameraProfileTiming{0, 0, "fixed"};
// Colour cameras first (see CameraConfig::cameras): the keep rule reads camera
// 0, and a thermal camera has none of the features it needs.
std::stable_partition(cfg.camera.cameras.begin(), cfg.camera.cameras.end(),
[](const CameraSpec& c) { return !c.thermal(); });
// The label is the output folder, and the cameras of one picture share its
// timestamp: two with one label would write the same file.
for (size_t i = 0; i < cfg.camera.cameras.size(); ++i)
for (size_t j = i + 1; j < cfg.camera.cameras.size(); ++j) {
const auto& a = cfg.camera.cameras[i];
const auto& b = cfg.camera.cameras[j];
if (a.enabled && b.enabled && a.label == b.label)
throw std::runtime_error("Camera." + a.name + " and Camera." + b.name +
" both have label '" + a.label +
"': each camera needs its own (its output folder)");
}
std::string out = get(kv, "Paths.output_dir"); std::string out = get(kv, "Paths.output_dir");
cfg.paths.output_dir = out.empty() ? paths::defaultOutputDir() : paths::expandUser(out); cfg.paths.output_dir = out.empty() ? paths::defaultOutputDir() : paths::expandUser(out);
@ -398,6 +488,12 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
cfg.features.enable_serial = getBool(kv, "Features.enable_serial", cfg.features.enable_serial); cfg.features.enable_serial = getBool(kv, "Features.enable_serial", cfg.features.enable_serial);
cfg.features.enable_imu = getBool(kv, "Features.enable_imu", cfg.features.enable_imu); cfg.features.enable_imu = getBool(kv, "Features.enable_imu", cfg.features.enable_imu);
cfg.features.mock_camera = getBool(kv, "Features.mock_camera", cfg.features.mock_camera); cfg.features.mock_camera = getBool(kv, "Features.mock_camera", cfg.features.mock_camera);
{
const std::string size = get(kv, "Features.mock_camera_size", "small");
if (size != "small" && size != "full")
throw std::runtime_error("Features.mock_camera_size must be small or full");
cfg.features.mock_camera_full = size == "full";
}
cfg.features.mock_serial = getBool(kv, "Features.mock_serial", cfg.features.mock_serial); cfg.features.mock_serial = getBool(kv, "Features.mock_serial", cfg.features.mock_serial);
cfg.features.mock_imu = getBool(kv, "Features.mock_imu", cfg.features.mock_imu); cfg.features.mock_imu = getBool(kv, "Features.mock_imu", cfg.features.mock_imu);
cfg.features.enable_env = getBool(kv, "Features.enable_env", cfg.features.enable_env); cfg.features.enable_env = getBool(kv, "Features.enable_env", cfg.features.enable_env);
@ -722,6 +818,7 @@ AppConfig ConfigLoader::loadFromFile(const std::string& path, const std::string&
} }
// Same for each camera's profile directory. // Same for each camera's profile directory.
for (auto& c : cfg.camera.cameras) { for (auto& c : cfg.camera.cameras) {
if (c.profile_dir.empty()) continue; // a thermal camera has none
std::filesystem::path pd(paths::expandUser(c.profile_dir)); std::filesystem::path pd(paths::expandUser(c.profile_dir));
if (pd.is_relative()) pd = std::filesystem::path(path).parent_path() / pd; if (pd.is_relative()) pd = std::filesystem::path(path).parent_path() / pd;
c.profile_dir = pd.lexically_normal().string(); c.profile_dir = pd.lexically_normal().string();

View File

@ -35,7 +35,7 @@ const std::vector<HelpSection>& helpCatalog() {
"Pitch defaults to 0 (level); a yaw-only tower ignores it. Key p pans", "Pitch defaults to 0 (level); a yaw-only tower ignores it. Key p pans",
"30 deg around the current aim.", "30 deg around the current aim.",
"Example: mode pan 90 30"}}, "Example: mode pan 90 30"}},
{"mode manual", "Stop scanning; aim by hand (key m). Cancels a running procedure.", {}}, {"mode manual", "Stop scanning, and the gimbal where it is; aim by hand (key m). Cancels a running procedure.", {}},
}}, }},
{"Capture", "Images and cameras. Manual mode, homed gimbal.", { {"Capture", "Images and cameras. Manual mode, homed gimbal.", {
{"snapshot", "Take one image now (key Space).", { {"snapshot", "Take one image now (key Space).", {
@ -49,10 +49,17 @@ const std::vector<HelpSection>& helpCatalog() {
"setup or test."}}, "setup or test."}},
{"set camera profile [<camera>] <name>", "Switch a camera's VimbaX settings profile.", { {"set camera profile [<camera>] <name>", "Switch a camera's VimbaX settings profile.", {
"<name> is an XML stem in that camera's profile_dir. The camera may be", "<name> is an XML stem in that camera's profile_dir. The camera may be",
"omitted when only one is configured.", "omitted when only one colour camera is configured. For the thermal",
"camera it only picks the timing block.",
"Example: set camera profile night"}}, "Example: set camera profile night"}},
{"camera reset", "Reboot the cameras and restore their profiles.", { {"camera reset [<camera>]", "Reboot the cameras and restore their profiles.", {
"For a stream that has stopped and will not restart. Takes seconds."}}, "For a stream that has stopped and will not restart. Takes seconds.",
"Name one camera (block name or label) to reset only that one."}},
{"camera calib [<profile>]", "The thermal camera's calibration profile.", {
"Bare: which profile, and its K per count. With a number: switch (about",
"3 s, no thermal frames meanwhile). Profile 0 is -40..120 degC at",
"0.01 K per count, 1 is 0..600 degC at 0.02 K. Every CamEvent says",
"which one its image was taken on (cal)."}},
}}, }},
{"Positioning", "Yaw is a true bearing (0 = north); pitch is elevation (0 = level).", { {"Positioning", "Yaw is a true bearing (0 = north); pitch is elevation (0 = level).", {
{"gimbal move <yaw>[,<pitch>]", "Point at a bearing/elevation in degrees.", { {"gimbal move <yaw>[,<pitch>]", "Point at a bearing/elevation in degrees.", {
@ -122,6 +129,9 @@ const std::vector<HelpSection>& helpCatalog() {
{"help [topic]", "This list; 'help <topic>' adds detail.", { {"help [topic]", "This list; 'help <topic>' adds detail.", {
"Example: help capture / help positioning"}}, "Example: help capture / help positioning"}},
{"cancel", "Cancel the running test, homing or calibration.", {}}, {"cancel", "Cancel the running test, homing or calibration.", {}},
{"thermal [resume|off|on]", "CPU heat, and the scan slowed or paused for it.", {
"Bare: temperature, clock, throttling, and the scan's pace. Any mode.",
"resume: back to full pace now; off/on: the guard, until restart."}},
{"debug", "Toggle debug-level logging.", {}}, {"debug", "Toggle debug-level logging.", {}},
{"trace <serial|mqtt|camera|control|all|off> [on|off]", "Toggle wire-trace categories.", { {"trace <serial|mqtt|camera|control|all|off> [on|off]", "Toggle wire-trace categories.", {
"Example: trace serial on"}}, "Example: trace serial on"}},

View File

@ -1,5 +1,6 @@
#include "fgc/HostMetrics.h" #include "fgc/HostMetrics.h"
#include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@ -57,25 +58,160 @@ MemInfo parseMeminfo(const std::string& text) {
return m; return m;
} }
ThermalInfo readThermal() { CpuTimes parseProcStatCpu(const std::string& text) {
CpuTimes t;
std::istringstream in(text);
std::string line;
while (std::getline(in, line)) {
std::istringstream ls(line);
std::string key;
if (!(ls >> key) || key != "cpu") continue;
// user nice system idle iowait irq softirq steal [guest guest_nice]
// guest time is already inside user/nice, so it is not added again.
long long v[8] = {0, 0, 0, 0, 0, 0, 0, 0};
int n = 0;
while (n < 8 && (ls >> v[n])) ++n;
if (n < 4) return t;
const long long idle = v[3] + v[4];
long long total = 0;
for (int i = 0; i < n; ++i) total += v[i];
t.ok = true;
t.total = total;
t.busy = total - idle;
return t;
}
return t;
}
double busyPercent(const CpuTimes& before, const CpuTimes& after) {
if (!before.ok || !after.ok) return -1.0;
const long long dt = after.total - before.total;
if (dt <= 0) return -1.0;
const double pct = 100.0 * static_cast<double>(after.busy - before.busy) / static_cast<double>(dt);
return pct < 0.0 ? 0.0 : (pct > 100.0 ? 100.0 : pct);
}
namespace {
fs::path under(const std::string& root, const char* path) {
return fs::path(root.empty() ? "/" : root) / path;
}
// cpu0, cpu1, ... in number order - not cpufreq/, cpuidle/ and the rest.
std::vector<fs::path> cpuDirs(const std::string& root) {
std::vector<std::pair<int, fs::path>> found;
std::error_code ec;
for (auto& e : fs::directory_iterator(under(root, "sys/devices/system/cpu"), ec)) {
const auto name = e.path().filename().string();
if (name.size() < 4 || name.rfind("cpu", 0) != 0) continue;
bool digits = true;
for (size_t i = 3; i < name.size(); ++i)
if (name[i] < '0' || name[i] > '9') digits = false;
if (digits) found.emplace_back(std::stoi(name.substr(3)), e.path());
}
std::sort(found.begin(), found.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
std::vector<fs::path> out;
for (auto& f : found) out.push_back(f.second);
return out;
}
// A sysfs number, or -1 when the file is missing or unreadable.
long long readNumber(const fs::path& p) {
std::string s = readFile(p);
if (s.empty()) return -1;
try {
return std::stoll(s);
} catch (...) {
return -1;
}
}
} // namespace
CpuFreqInfo readCpuFreq(const std::string& root) {
CpuFreqInfo f;
double sum = 0.0;
for (const auto& dir : cpuDirs(root)) {
const long long cur = readNumber(dir / "cpufreq/scaling_cur_freq"); // kHz
if (cur < 0) continue;
f.core_mhz.push_back(static_cast<double>(cur) / 1000.0);
sum += static_cast<double>(cur) / 1000.0;
const long long hw = readNumber(dir / "cpufreq/cpuinfo_max_freq");
if (hw > 0) f.hw_max_mhz = std::max(f.hw_max_mhz, static_cast<double>(hw) / 1000.0);
const long long cap = readNumber(dir / "cpufreq/scaling_max_freq");
if (cap > 0) {
const double mhz = static_cast<double>(cap) / 1000.0;
f.cap_mhz = f.cap_mhz > 0.0 ? std::min(f.cap_mhz, mhz) : mhz;
}
}
if (!f.core_mhz.empty()) {
f.ok = true;
f.mean_mhz = sum / static_cast<double>(f.core_mhz.size());
}
return f;
}
ThrottleCount readThrottleCount(const std::string& root) {
ThrottleCount c;
bool package_seen = false;
for (const auto& dir : cpuDirs(root)) {
// Every core shows the same package count; take it once.
if (!package_seen) {
const long long pkg = readNumber(dir / "thermal_throttle/package_throttle_count");
if (pkg >= 0) {
package_seen = true;
c.ok = true;
c.events += pkg;
}
}
const long long core = readNumber(dir / "thermal_throttle/core_throttle_count");
if (core >= 0) {
c.ok = true;
c.events += core;
}
}
return c;
}
CpuTimes readCpuTimes(const std::string& root) {
// A fake tree without its own proc/stat uses the real one: how busy the CPU
// is stays real in a simulation (raise it with tools/thermal-sim/heat-load.sh).
const fs::path p = under(root, "proc/stat");
std::error_code ec;
return parseProcStatCpu(readFile(fs::exists(p, ec) ? p : fs::path("/proc/stat")));
}
ThermalInfo readThermal(const std::string& root_dir) {
ThermalInfo t; ThermalInfo t;
const fs::path root = "/sys/class/thermal"; const fs::path root = under(root_dir, "sys/class/thermal");
std::error_code ec; std::error_code ec;
if (!fs::exists(root, ec)) return t; if (!fs::exists(root, ec)) return t;
bool have_pkg = false;
for (auto& e : fs::directory_iterator(root, ec)) { for (auto& e : fs::directory_iterator(root, ec)) {
const auto name = e.path().filename().string(); const auto name = e.path().filename().string();
if (name.rfind("thermal_zone", 0) != 0) continue; if (name.rfind("thermal_zone", 0) != 0) continue;
std::string temp_s = readFile(e.path() / "temp"); std::string temp_s = readFile(e.path() / "temp");
if (temp_s.empty()) continue; if (temp_s.empty()) continue;
double c = parseMilliCelsius(temp_s); double c = parseMilliCelsius(temp_s);
if (!t.ok || c > t.max_temp_c) {
t.ok = true;
t.max_temp_c = c;
std::string type = readFile(e.path() / "type"); std::string type = readFile(e.path() / "type");
// strip trailing newline // strip trailing newline
while (!type.empty() && (type.back() == '\n' || type.back() == '\r')) type.pop_back(); while (!type.empty() && (type.back() == '\n' || type.back() == '\r')) type.pop_back();
t.hottest_zone = type.empty() ? name : type; if (type.empty()) type = name;
if (type == "x86_pkg_temp" && (!have_pkg || c > t.cpu_temp_c)) {
have_pkg = true;
t.cpu_temp_c = c;
t.cpu_zone = type;
} }
if (!t.ok || c > t.max_temp_c) {
t.ok = true;
t.max_temp_c = c;
t.hottest_zone = type;
}
}
if (t.ok && !have_pkg) {
t.cpu_temp_c = t.max_temp_c;
t.cpu_zone = t.hottest_zone;
} }
return t; return t;
} }

View File

@ -106,7 +106,7 @@ bool allowedIn(Mode mode, const std::string& line) {
const std::string& verb = w[0]; const std::string& verb = w[0];
const std::string sub = w.size() > 1 ? w[1] : ""; const std::string sub = w.size() > 1 ? w[1] : "";
if (verb == "help" || verb == "mode" || verb == "cancel" || verb == "exit" || if (verb == "help" || verb == "mode" || verb == "cancel" || verb == "exit" ||
verb == "debug" || verb == "trace") verb == "debug" || verb == "trace" || verb == "thermal")
return true; return true;
if (verb == "gimbal") return sub == "status" || sub == "dump"; if (verb == "gimbal") return sub == "status" || sub == "dump";
if (verb == "imu") return sub == "status" || sub == "config"; if (verb == "imu") return sub == "status" || sub == "config";

View File

@ -1,5 +1,7 @@
#include "fgc/RawImage.h" #include "fgc/RawImage.h"
#include <cstring>
namespace fgc { namespace fgc {
const char* layoutName(PixelLayout layout) { const char* layoutName(PixelLayout layout) {
@ -8,6 +10,8 @@ const char* layoutName(PixelLayout layout) {
case PixelLayout::RGB8: return "RGB8"; case PixelLayout::RGB8: return "RGB8";
case PixelLayout::BayerRG8: return "BayerRG8"; case PixelLayout::BayerRG8: return "BayerRG8";
case PixelLayout::BayerRG12p: return "BayerRG12p"; case PixelLayout::BayerRG12p: return "BayerRG12p";
case PixelLayout::BayerRG12Packed: return "BayerRG12Packed";
case PixelLayout::Mono16: return "Mono16";
case PixelLayout::Unknown: break; case PixelLayout::Unknown: break;
} }
return "Unknown"; return "Unknown";
@ -15,13 +19,15 @@ const char* layoutName(PixelLayout layout) {
PixelLayout layoutFromName(const std::string& name) { PixelLayout layoutFromName(const std::string& name) {
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8, for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8,
PixelLayout::BayerRG12p}) PixelLayout::BayerRG12p, PixelLayout::BayerRG12Packed,
PixelLayout::Mono16})
if (name == layoutName(l)) return l; if (name == layoutName(l)) return l;
return PixelLayout::Unknown; return PixelLayout::Unknown;
} }
bool layoutIsBayer(PixelLayout layout) { bool layoutIsBayer(PixelLayout layout) {
return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p; return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p ||
layout == PixelLayout::BayerRG12Packed;
} }
int layoutBits(PixelLayout layout) { int layoutBits(PixelLayout layout) {
@ -29,7 +35,9 @@ int layoutBits(PixelLayout layout) {
case PixelLayout::Mono8: case PixelLayout::Mono8:
case PixelLayout::RGB8: case PixelLayout::RGB8:
case PixelLayout::BayerRG8: return 8; case PixelLayout::BayerRG8: return 8;
case PixelLayout::BayerRG12p: return 12; case PixelLayout::BayerRG12p:
case PixelLayout::BayerRG12Packed: return 12;
case PixelLayout::Mono16: return 16;
case PixelLayout::Unknown: break; case PixelLayout::Unknown: break;
} }
return 0; return 0;
@ -41,7 +49,9 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height) {
case PixelLayout::Mono8: case PixelLayout::Mono8:
case PixelLayout::BayerRG8: return n; case PixelLayout::BayerRG8: return n;
case PixelLayout::RGB8: return n * 3; case PixelLayout::RGB8: return n * 3;
case PixelLayout::BayerRG12p: return (n * 3 + 1) / 2; case PixelLayout::BayerRG12p:
case PixelLayout::BayerRG12Packed: return (n * 3 + 1) / 2;
case PixelLayout::Mono16: return n * 2;
case PixelLayout::Unknown: break; case PixelLayout::Unknown: break;
} }
return 0; return 0;
@ -49,12 +59,24 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height) {
namespace { namespace {
inline uint16_t px16(const uint8_t* in, size_t i) {
uint16_t v;
std::memcpy(&v, in + i * 2, sizeof v); // host order, possibly unaligned
return v;
}
inline uint16_t px12p(const uint8_t* in, size_t i) { inline uint16_t px12p(const uint8_t* in, size_t i) {
const uint8_t* p = in + (i >> 1) * 3; const uint8_t* p = in + (i >> 1) * 3;
return (i & 1) == 0 ? static_cast<uint16_t>(p[0] | ((p[1] & 0x0F) << 8)) return (i & 1) == 0 ? static_cast<uint16_t>(p[0] | ((p[1] & 0x0F) << 8))
: static_cast<uint16_t>((p[1] >> 4) | (p[2] << 4)); : static_cast<uint16_t>((p[1] >> 4) | (p[2] << 4));
} }
inline uint16_t px12Packed(const uint8_t* in, size_t i) {
const uint8_t* p = in + (i >> 1) * 3;
return (i & 1) == 0 ? static_cast<uint16_t>((p[0] << 4) | (p[1] & 0x0F))
: static_cast<uint16_t>((p[2] << 4) | (p[1] >> 4));
}
} // namespace } // namespace
void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) { void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) {
@ -66,6 +88,20 @@ void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) {
if (pixels & 1) out[0] = static_cast<uint16_t>(in[0] | ((in[1] & 0x0F) << 8)); if (pixels & 1) out[0] = static_cast<uint16_t>(in[0] | ((in[1] & 0x0F) << 8));
} }
void unpack12Packed(const uint8_t* in, uint16_t* out, size_t pixels) {
const size_t pairs = pixels / 2;
for (size_t k = 0; k < pairs; ++k, in += 3, out += 2) {
out[0] = static_cast<uint16_t>((in[0] << 4) | (in[1] & 0x0F));
out[1] = static_cast<uint16_t>((in[2] << 4) | (in[1] >> 4));
}
if (pixels & 1) out[0] = static_cast<uint16_t>((in[0] << 4) | (in[1] & 0x0F));
}
void unpack12(PixelLayout layout, const uint8_t* in, uint16_t* out, size_t pixels) {
if (layout == PixelLayout::BayerRG12Packed) unpack12Packed(in, out, pixels);
else unpack12p(in, out, pixels);
}
BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayout layout, BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayout layout,
uint32_t width, uint32_t height, size_t stride) { uint32_t width, uint32_t height, size_t stride) {
BrightnessSample s; BrightnessSample s;
@ -79,7 +115,10 @@ BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayou
double sum = 0; double sum = 0;
size_t hi = 0; size_t hi = 0;
for (size_t i = 0; i < values; i += stride) { for (size_t i = 0; i < values; i += stride) {
double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0 : frame[i]; double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0
: layout == PixelLayout::BayerRG12Packed ? px12Packed(frame, i) / 16.0
: layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0
: frame[i];
sum += v; sum += v;
hi += v >= 250.0; hi += v >= 250.0;
++s.samples; ++s.samples;

View File

@ -1097,13 +1097,26 @@ void TestRunner::testHwEnv(TestReport& r) {
} }
setProgress(s.id, 2, 2, "reading sensor"); setProgress(s.id, 2, 2, "reading sensor");
std::optional<EnvSample> sample = hw_.env ? hw_.env->sample() : std::nullopt;
// The firmware reads its DHT11 every 10 s, the first time 2 s after it
// starts, so a run straight after the host connected may have no reading
// yet: install-tower --verify failed on rietschen 1.4 s into its run, 36 s
// after the same test had passed. Wait one read period before calling it
// missing.
if (hw_.env && !sample && on_mcu) {
const auto deadline = clock_t_::now() + 12s;
while (!sample && clock_t_::now() < deadline && !cancelled()) {
std::this_thread::sleep_for(200ms);
sample = hw_.env->sample();
}
}
CheckResult m; CheckResult m;
if (!hw_.env) { if (!hw_.env) {
m.pass = false; m.pass = false;
m.detail = "declared as '" + hw_.declared->env + m.detail = "declared as '" + hw_.declared->env +
"' but no sensor was built -- check [Env] i2c_device; the declaration itself is " "' but no sensor was built -- check [Env] i2c_device; the declaration itself is "
"what enables it"; "what enables it";
} else if (auto sample = hw_.env->sample()) { } else if (sample) {
m.detail = "CRC-valid reading from " + hw_.env->name(); m.detail = "CRC-valid reading from " + hw_.env->name();
} else if (on_mcu) { } else if (on_mcu) {
m.pass = false; m.pass = false;
@ -1298,8 +1311,12 @@ void TestRunner::testCamera(TestReport& r) {
f.channels != run.channels) { f.channels != run.channels) {
run.consistent = false; run.consistent = false;
} }
if (f.data.size() != static_cast<size_t>(f.width) * f.height * f.channels) // By layout where it is known: packed 12-bit and 16-bit counts are
run.consistent = false; // not one byte per value.
const size_t expect = f.layout != PixelLayout::Unknown
? layoutFrameBytes(f.layout, f.width, f.height)
: static_cast<size_t>(f.width) * f.height * f.channels;
if (f.data.size() != expect) run.consistent = false;
++run.got; ++run.got;
} }
const double secs = std::chrono::duration<double>( const double secs = std::chrono::duration<double>(
@ -1316,6 +1333,17 @@ void TestRunner::testCamera(TestReport& r) {
if (!want_dark && !want_bright) continue; if (!want_dark && !want_bright) continue;
// A thermal camera has no exposure to force: dark and bright are checks
// for a colour camera.
{
double elo = 0, ehi = 0;
if (!cam->featureRange(i, "ExposureTime", elo, ehi)) {
if (want_dark) sd.notes.push_back(cn + ": not applicable (no exposure control)");
if (want_bright) sb.notes.push_back(cn + ": not applicable (no exposure control)");
continue;
}
}
// From here the camera is off its profile. It is put back below on every // From here the camera is off its profile. It is put back below on every
// path out of these phases, and a failure to put it back is a failure of // path out of these phases, and a failure to put it back is a failure of
// the test. // the test.

321
src/core/ThermalGuard.cpp Normal file
View File

@ -0,0 +1,321 @@
#include "fgc/ThermalGuard.h"
#include <algorithm>
#include <cstdio>
namespace fgc {
namespace {
constexpr long long kMinuteMs = 60000;
// How much longer than one save the slowed interval is: the saver then runs
// about two-thirds busy, not flat out.
constexpr double kSaveHeadroom = 1.5;
std::string seconds(long long ms) {
const long long s = (ms + 500) / 1000;
if (s < 120) return std::to_string(s) + " s";
return std::to_string((s + 30) / 60) + " min";
}
std::string factorText(double f) {
char b[16];
std::snprintf(b, sizeof(b), "%g", f);
return b;
}
} // namespace
std::string celsiusText(double c) {
char b[24];
std::snprintf(b, sizeof(b), "%.0f \xC2\xB0""C", c);
return b;
}
std::string ghzText(double mhz) {
char b[24];
std::snprintf(b, sizeof(b), "%.1f GHz", mhz / 1000.0);
return b;
}
const char* guardStateName(GuardState s) {
switch (s) {
case GuardState::Normal: return "normal";
case GuardState::Slowed: return "slowed";
case GuardState::Paused: return "paused";
}
return "?";
}
// ---- ThrottleWatch -----------------------------------------------------------
long long ThrottleWatch::eventsLastMinute() const {
long long n = 0;
for (const auto& e : recent_) n += e.second;
return n;
}
void ThrottleWatch::update(const HostReading& r, std::vector<GuardLog>& log) {
// Counter rise since the last sample. A counter that went backwards (the
// driver reloaded, or a simulation reset it) is a fresh start, not throttling.
long long rise = 0;
if (r.count_ok) {
if (have_count_ && r.throttle_count > last_count_) rise = r.throttle_count - last_count_;
last_count_ = r.throttle_count;
have_count_ = true;
}
// A low clock while busy, two samples running: one low sample is as likely
// the governor catching up with a burst.
const bool low_clock = r.clock_ok && r.hw_max_mhz > 0.0 && r.busy_pct >= 0.0 &&
r.mhz < r.hw_max_mhz * p_.clock_low_pct / 100.0 &&
r.busy_pct >= p_.busy_high_pct;
low_clock_run_ = low_clock ? low_clock_run_ + 1 : 0;
const bool by_clock = low_clock_run_ >= 2;
while (!recent_.empty() && r.now_ms - recent_.front().first >= kMinuteMs) recent_.pop_front();
if (rise > 0 || by_clock) {
events_total_ += rise;
recent_.emplace_back(r.now_ms, rise);
last_seen_ms_ = r.now_ms;
cause_ = rise > 0 && by_clock ? "counters and clock" : (rise > 0 ? "counters" : "clock");
const std::string where =
(r.temp_ok ? celsiusText(r.cpu_c) + ", " : std::string()) +
(r.clock_ok ? "running at " + ghzText(r.mhz) +
(r.hw_max_mhz > 0.0 ? " of " + ghzText(r.hw_max_mhz) : std::string())
: std::string());
if (!throttling_) {
throttling_ = true;
last_summary_ms_ = r.now_ms;
log.emplace_back(GuardLevel::Warn,
rise > 0 ? "CPU is slowing itself to stay cool: " + where
: "CPU is running slow while busy (judged from its clock, "
"not the kernel's count): " + where);
} else if (r.now_ms - last_summary_ms_ >= kMinuteMs) {
last_summary_ms_ = r.now_ms;
const long long n = eventsLastMinute();
log.emplace_back(GuardLevel::Warn,
"CPU still slowed: " +
(n > 0 ? std::to_string(n) + " times in the last minute"
: std::string("clock low all minute")) +
", " + where);
}
} else if (throttling_ && r.now_ms - last_seen_ms_ >= kMinuteMs) {
throttling_ = false;
// No clock here: a CPU with nothing to do idles at a low clock, and
// "back to full speed: 0.8 GHz" (seen on tower 111) reads as a contradiction.
log.emplace_back(GuardLevel::Info,
"CPU no longer slowed" +
(r.temp_ok ? ": " + celsiusText(r.cpu_c) : std::string()));
}
}
// ---- ThermalGuard ------------------------------------------------------------
ThermalGuard::ThermalGuard(const ThermalParams& p) : p_(p), watch_(p), enabled_(p.enabled) {}
long long ThermalGuard::heldForSaveMs() const {
return held_since_ms_ >= 0 && have_last_ ? last_.now_ms - held_since_ms_ : 0;
}
long long ThermalGuard::pausedForMs() const {
return state_ == GuardState::Paused && have_last_ ? last_.now_ms - paused_at_ms_ : 0;
}
double ThermalGuard::heldSharePct() const {
if (held_hist_.size() < 2) return -1.0;
const auto& a = held_hist_.front();
const auto& b = held_hist_.back();
const long long span = b.first - a.first;
if (span < kMinuteMs / 2) return -1.0;
const double pct = 100.0 * static_cast<double>(b.second - a.second) / static_cast<double>(span);
return pct < 0.0 ? 0.0 : (pct > 100.0 ? 100.0 : pct);
}
// Slowed: slow_factor, or kSaveHeadroom x what one picture takes to save per worker,
// whichever is longer - so a slow saver gets headroom, not a scan that waits
// on it at every waypoint. The last save time seen is kept: after a pause there
// is none for a while, and the pace should not forget why it was slowed.
void ThermalGuard::updateScale(const HostReading& r) {
if (r.save_ms_mean > 0.0) last_save_ms_ = r.save_ms_mean;
need_scale_ = 0.0;
if (r.base_interval_ms > 0.0 && last_save_ms_ > 0.0)
need_scale_ = kSaveHeadroom * last_save_ms_ / std::max(1, r.save_workers) / r.base_interval_ms;
scale_ = std::min(std::max(p_.slow_factor, need_scale_), 10.0);
}
std::string ThermalGuard::paceText() const {
if (last_.base_interval_ms > 0.0) {
char b[64];
std::snprintf(b, sizeof(b), "one picture every %.1f s instead of %.1f",
last_.base_interval_ms * scale_ / 1000.0, last_.base_interval_ms / 1000.0);
return b;
}
return "pictures " + factorText(scale_) + "x further apart";
}
void ThermalGuard::enter(GuardState s, const std::string& why, std::vector<GuardLog>& log) {
const GuardState was = state_;
state_ = s;
reason_ = s == GuardState::Normal ? std::string() : why;
calm_since_ms_ = -1;
overload_since_ms_ = -1;
const long long now = last_.now_ms;
const std::string temp = last_.temp_ok ? "CPU at " + celsiusText(last_.cpu_c) : "CPU";
switch (s) {
case GuardState::Slowed:
slowed_at_ms_ = now;
if (was == GuardState::Paused)
log.emplace_back(GuardLevel::Info,
temp + ": pictures resume, " + paceText() +
" until it has been calm for " + seconds(p_.calm_s * 1000LL));
else if (why.rfind("Saving", 0) == 0)
log.emplace_back(GuardLevel::Warn,
why + ": " + paceText() + " until saving is fast enough for "
"the normal pace, for " + seconds(p_.calm_s * 1000LL));
else
log.emplace_back(GuardLevel::Warn,
why + ": " + paceText() + " until the CPU is below " +
celsiusText(p_.slow_c - p_.hysteresis_c) +
" and keeping up for " + seconds(p_.calm_s * 1000LL));
break;
case GuardState::Paused:
paused_at_ms_ = now;
log.emplace_back(GuardLevel::Warn,
why + ": pausing pictures for at least " +
seconds(p_.min_pause_s * 1000LL) + ", until the CPU is at " +
celsiusText(p_.resume_c) + " or below and saving has caught up");
break;
case GuardState::Normal:
log.emplace_back(GuardLevel::Info, temp + ": back to the normal picture pace");
break;
}
}
std::vector<GuardLog> ThermalGuard::update(const HostReading& r) {
std::vector<GuardLog> log;
last_ = r;
have_last_ = true;
watch_.update(r, log);
const long long now = r.now_ms;
hot_since_ms_ = r.temp_ok && r.cpu_c >= p_.pause_c ? (hot_since_ms_ >= 0 ? hot_since_ms_ : now) : -1;
held_since_ms_ = r.held_for_save ? (held_since_ms_ >= 0 ? held_since_ms_ : now) : -1;
// The waiting total only rises; one that fell is a restarted count.
if (!held_hist_.empty() && r.held_total_ms < held_hist_.back().second) held_hist_.clear();
held_hist_.emplace_back(now, r.held_total_ms);
while (held_hist_.size() > 1 && now - held_hist_.front().first > kMinuteMs) held_hist_.pop_front();
updateScale(r);
if (!enabled_) return log;
const long long hot_ms = hot_since_ms_ >= 0 ? now - hot_since_ms_ : 0;
const long long held_ms = heldForSaveMs();
const double share = heldSharePct();
const bool bottleneck = share >= p_.held_share_pct;
auto shareText = [&] {
return "Saving kept the scan waiting " + std::to_string(static_cast<int>(share + 0.5)) +
"% of the last minute";
};
// Pause: from either running state.
if (state_ != GuardState::Paused) {
if (hot_since_ms_ >= 0 && hot_ms >= p_.pause_after_s * 1000LL) {
enter(GuardState::Paused,
"CPU at " + celsiusText(r.cpu_c) + " for " + seconds(hot_ms), log);
return log;
}
if (held_since_ms_ >= 0 && held_ms >= p_.held_pause_s * 1000LL) {
enter(GuardState::Paused,
"Saving has kept the scan waiting for " + seconds(held_ms), log);
return log;
}
// Slowed, and saving still the bottleneck once the minute it is judged
// over holds only slowed pictures: the slower pace did not help.
if (state_ == GuardState::Slowed) {
const bool judged = now - slowed_at_ms_ >= kMinuteMs;
overload_since_ms_ = judged && bottleneck
? (overload_since_ms_ >= 0 ? overload_since_ms_ : now)
: -1;
if (overload_since_ms_ >= 0 && now - overload_since_ms_ >= p_.held_pause_s * 1000LL) {
enter(GuardState::Paused, shareText() + ", even slowed", log);
return log;
}
}
}
switch (state_) {
case GuardState::Normal: {
std::string why;
if (r.temp_ok && r.cpu_c >= p_.slow_c) why = "CPU at " + celsiusText(r.cpu_c);
else if (watch_.throttling()) why = "CPU is slowing itself down";
else if (held_since_ms_ >= 0 && held_ms >= p_.held_slow_s * 1000LL)
why = "Saving has kept the scan waiting for " + seconds(held_ms);
else if (bottleneck) why = shareText();
if (!why.empty()) enter(GuardState::Slowed, why, log);
break;
}
case GuardState::Slowed: {
// Calm: cool enough, not throttling, not waiting for saving now, and
// under half of held_share_pct over the minute - a brief wait now and
// then is a saver with headroom, as it should be.
// And the normal pace must fit what saving takes now: a saver that is
// simply too slow for it would overload again at once, and flap.
const bool calm = !(r.temp_ok && r.cpu_c >= p_.slow_c - p_.hysteresis_c) &&
!watch_.throttling() && held_since_ms_ < 0 &&
!(share >= p_.held_share_pct / 2.0) && need_scale_ <= 1.0;
if (!calm) {
calm_since_ms_ = -1;
} else {
if (calm_since_ms_ < 0) calm_since_ms_ = now;
if (now - calm_since_ms_ >= p_.calm_s * 1000LL) enter(GuardState::Normal, "", log);
}
break;
}
case GuardState::Paused: {
// Saving drains while paused; resume once it has and the CPU cooled.
const bool cool = !r.temp_ok || r.cpu_c <= p_.resume_c;
if (now - paused_at_ms_ >= p_.min_pause_s * 1000LL && cool && r.save_pending == 0)
enter(GuardState::Slowed, "Resuming after a pause", log);
break;
}
}
return log;
}
std::vector<GuardLog> ThermalGuard::forceResume(long long now_ms) {
std::vector<GuardLog> log;
hot_since_ms_ = held_since_ms_ = calm_since_ms_ = overload_since_ms_ = -1;
held_hist_.clear();
if (have_last_) last_.now_ms = now_ms;
if (state_ != GuardState::Normal) {
state_ = GuardState::Normal;
reason_.clear();
log.emplace_back(GuardLevel::Info,
"thermal resume: back to the normal picture pace (the CPU is still "
"watched; it will slow or pause again if it stays hot)");
} else {
log.emplace_back(GuardLevel::Info, "thermal resume: already at the normal picture pace");
}
return log;
}
std::vector<GuardLog> ThermalGuard::setEnabled(bool on, long long now_ms) {
std::vector<GuardLog> log;
if (on == enabled_) {
log.emplace_back(GuardLevel::Info, std::string("CPU guard already ") + (on ? "on" : "off"));
return log;
}
enabled_ = on;
hot_since_ms_ = held_since_ms_ = calm_since_ms_ = overload_since_ms_ = -1;
held_hist_.clear();
if (have_last_) last_.now_ms = now_ms;
state_ = GuardState::Normal;
reason_.clear();
log.emplace_back(on ? GuardLevel::Info : GuardLevel::Warn,
on ? "CPU guard on: the scan slows or pauses when the CPU runs hot"
: "CPU guard off until restart: the scan keeps full pace however hot the "
"CPU runs (temperature and throttling are still shown and logged)");
return log;
}
} // namespace fgc

51
src/core/ThermalStats.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "fgc/ThermalStats.h"
#include <cmath>
#include <vector>
namespace fgc {
int countsToDeciC(uint32_t count, double kelvin_per_count) {
return static_cast<int>(std::lround((count * kelvin_per_count - 273.15) * 10.0));
}
ThermalStats thermalStats(const uint16_t* counts, uint32_t width, uint32_t height,
const ThermalFrameInfo& info) {
ThermalStats s;
if (!counts || width == 0 || height == 0) return s;
// A histogram gives the median in one pass with no copy of the frame, and
// 64k bins are cheaper than sorting 300k values.
std::vector<uint32_t> hist(65536, 0);
const size_t n = static_cast<size_t>(width) * height;
size_t at = 0;
uint16_t max = 0;
for (size_t i = 0; i < n; ++i) {
const uint16_t c = counts[i];
if (c == 0 || c > info.valid_count_max) continue;
++hist[c];
++s.valid_pixels;
if (c > max) { max = c; at = i; }
}
if (s.valid_pixels == 0) return s;
// The lower median: the ((n-1)/2)-th value in sorted order.
const size_t want = (s.valid_pixels - 1) / 2;
size_t seen = 0;
uint32_t med = 0;
for (uint32_t c = 1; c < hist.size(); ++c) {
seen += hist[c];
if (seen > want) { med = c; break; }
}
s.valid = true;
s.max_count = max;
s.median_count = static_cast<uint16_t>(med);
s.tmax_decic = countsToDeciC(max, info.kelvin_per_count);
s.tmed_decic = countsToDeciC(med, info.kelvin_per_count);
s.hotx = (static_cast<double>(at % width) + 0.5) / width;
s.hoty = (static_cast<double>(at / width) + 0.5) / height;
return s;
}
} // namespace fgc

View File

@ -12,6 +12,8 @@ namespace {
constexpr int kQos = 1; constexpr int kQos = 1;
const auto kConnectTimeout = std::chrono::seconds(5); const auto kConnectTimeout = std::chrono::seconds(5);
const auto kPublishTimeout = std::chrono::seconds(3); const auto kPublishTimeout = std::chrono::seconds(3);
constexpr size_t kOutboxMax = 64;
const auto kOutboxFlush = std::chrono::seconds(5);
} // namespace } // namespace
MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std::string& tower, MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std::string& tower,
@ -20,6 +22,7 @@ MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std:
tower_(tower), tower_(tower),
topic_status_(towerTopic(tower, kTopicStatusCode)), topic_status_(towerTopic(tower, kTopicStatusCode)),
topic_cam_event_(towerTopic(tower, kTopicCamEvent)), topic_cam_event_(towerTopic(tower, kTopicCamEvent)),
topic_health_(towerTopic(tower, kTopicHealth)),
topic_command_(towerTopic(tower, kTopicCommand)), topic_command_(towerTopic(tower, kTopicCommand)),
topic_command_reply_(towerTopic(tower, kTopicCommandReply)), topic_command_reply_(towerTopic(tower, kTopicCommandReply)),
client_(broker_uri, tower) { client_(broker_uri, tower) {
@ -29,9 +32,60 @@ MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std:
if (!user.empty()) conn_opts_.set_user_name(user); if (!user.empty()) conn_opts_.set_user_name(user);
if (!password.empty()) conn_opts_.set_password(password); if (!password.empty()) conn_opts_.set_password(password);
client_.set_callback(*this); client_.set_callback(*this);
outbox_thread_ = std::thread(&MqttControlChannel::runOutbox, this);
} }
MqttControlChannel::~MqttControlChannel() { disconnect(); } MqttControlChannel::~MqttControlChannel() {
disconnect();
stopOutbox();
}
void MqttControlChannel::post(const std::string& topic, const std::string& payload) {
{
std::lock_guard<std::mutex> lock(outbox_mutex_);
if (outbox_.size() >= kOutboxMax) {
history_.record(/*outbound=*/true, Delivery::Dropped, outbox_.front().first,
outbox_.front().second);
outbox_.pop_front();
}
outbox_.emplace_back(topic, payload);
}
outbox_cv_.notify_one();
}
void MqttControlChannel::runOutbox() {
std::unique_lock<std::mutex> lock(outbox_mutex_);
for (;;) {
outbox_cv_.wait(lock, [this] { return outbox_stop_ || !outbox_.empty(); });
if (outbox_.empty()) return; // stopping, and nothing left
auto m = std::move(outbox_.front());
outbox_.pop_front();
lock.unlock();
publish(m.first, m.second);
lock.lock();
}
}
void MqttControlChannel::stopOutbox() {
if (!outbox_thread_.joinable()) return;
// Give what is waiting a few seconds to go out; drop (and record) the rest.
const auto deadline = std::chrono::steady_clock::now() + kOutboxFlush;
for (;;) {
{
std::lock_guard<std::mutex> lock(outbox_mutex_);
if (outbox_.empty() || std::chrono::steady_clock::now() >= deadline) {
for (const auto& m : outbox_)
history_.record(/*outbound=*/true, Delivery::Dropped, m.first, m.second);
outbox_.clear();
outbox_stop_ = true;
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
outbox_cv_.notify_all();
outbox_thread_.join();
}
bool MqttControlChannel::connect() { bool MqttControlChannel::connect() {
try { try {
@ -47,6 +101,7 @@ bool MqttControlChannel::connect() {
} }
void MqttControlChannel::disconnect() { void MqttControlChannel::disconnect() {
stopOutbox(); // the last pictures' events go out before the link closes
if (!client_.is_connected()) return; if (!client_.is_connected()) return;
try { try {
client_.disconnect()->wait(); client_.disconnect()->wait();
@ -66,22 +121,28 @@ void MqttControlChannel::connected(const std::string& /*cause*/) {
// Say the mode again: whoever watches StatusCode may have missed changes // Say the mode again: whoever watches StatusCode may have missed changes
// while the link was down. Not waited on - this runs on Paho's callback // while the link was down. Not waited on - this runs on Paho's callback
// thread, which is the one that would deliver the acknowledgement. // thread, which is the one that would deliver the acknowledgement.
std::string status; // Health likewise: a paused tower must not look healthy to a page that
// subscribed while the link was down.
std::string status, health;
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
status = last_status_; status = last_status_;
health = last_health_;
} }
if (!status.empty()) { auto republish = [this](const std::string& topic, const std::string& payload) {
if (payload.empty()) return;
try { try {
auto msg = mqtt::make_message(topic_status_, status); auto msg = mqtt::make_message(topic, payload);
msg->set_qos(kQos); msg->set_qos(kQos);
msg->set_retained(true); msg->set_retained(true);
client_.publish(msg); client_.publish(msg);
history_.record(/*outbound=*/true, Delivery::Ok, topic_status_, status); history_.record(/*outbound=*/true, Delivery::Ok, topic, payload);
} catch (const mqtt::exception& e) { } catch (const mqtt::exception& e) {
LOG_WARN << "MQTT status republish failed: " << e.what(); LOG_WARN << "MQTT republish of " << topic << " failed: " << e.what();
}
} }
};
republish(topic_status_, status);
republish(topic_health_, health);
} }
void MqttControlChannel::connection_lost(const std::string& cause) { void MqttControlChannel::connection_lost(const std::string& cause) {
@ -160,7 +221,16 @@ void MqttControlChannel::publishStatus(int code) {
} }
void MqttControlChannel::publishCamEvent(const CamEvent& e) { void MqttControlChannel::publishCamEvent(const CamEvent& e) {
publish(topic_cam_event_, camEventJson(e)); post(topic_cam_event_, camEventJson(e));
}
void MqttControlChannel::publishHealth(const HealthReport& h) {
const std::string payload = healthJson(h);
{
std::lock_guard<std::mutex> lock(mutex_);
last_health_ = payload;
}
post(topic_health_, payload);
} }
} // namespace fgc } // namespace fgc

View File

@ -369,9 +369,23 @@ Element sensorsPanel(const SensorsView& s) {
? (text(" IMU CFG ") | color(Color::Red) | bold) ? (text(" IMU CFG ") | color(Color::Red) | bold)
: text(""); : text("");
Element fan_badge = s.fan_alert ? (text(" FAN ") | color(Color::Red) | bold) : text(""); Element fan_badge = s.fan_alert ? (text(" FAN ") | color(Color::Red) | bold) : text("");
Element cpu_badge = s.cpu_badge.empty()
? text("")
: (text(" " + s.cpu_badge + " ") |
color(s.cpu_badge_red ? Color::Red : Color::Yellow) | bold);
return panel("[s] SENSORS", Color::Magenta, return panel("[s] SENSORS", Color::Magenta,
vbox({group(s.imu, std::move(imu_badge)), separator(), vbox({group(s.imu, std::move(imu_badge)), separator(),
group(s.env, std::move(fan_badge))})); group(s.env, std::move(fan_badge)), separator(),
group(s.host, std::move(cpu_badge))}));
}
// "max 301.5 °C median 18.7 °C cal 0" for the last thermal picture.
Element thermalLine(const CaptureView& c) {
if (!c.thermal_stats_ok)
return text("no valid pixels cal " + std::to_string(c.thermal_cal)) | color(Color::Yellow);
return hbox({text("max " + num(c.thermal_max_c, 1) + " \xC2\xB0" "C") | bold,
text(" median " + num(c.thermal_med_c, 1) + " \xC2\xB0" "C") | dim,
text(" cal " + std::to_string(c.thermal_cal)) | dim});
} }
Element cameraPanel(const CaptureView& c) { Element cameraPanel(const CaptureView& c) {
@ -412,6 +426,12 @@ Element cameraPanel(const CaptureView& c) {
} else { } else {
rows.push_back(labelRow("last", text("\xE2\x80\x94") | dim)); rows.push_back(labelRow("last", text("\xE2\x80\x94") | dim));
} }
if (c.has_thermal) rows.push_back(labelRow("thermal", thermalLine(c)));
for (size_t i = 0; i < c.devices.size(); ++i)
if (!c.devices[i].offline.empty())
rows.push_back(labelRow("offline", text((i < c.labels.size() ? c.labels[i] : "?") +
" " + c.devices[i].offline) |
color(Color::Yellow)));
return panel("[c] CAMERAS", Color::Blue, vbox(std::move(rows))); return panel("[c] CAMERAS", Color::Blue, vbox(std::move(rows)));
} }
@ -437,12 +457,15 @@ Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
for (size_t i = 0; i < c.devices.size(); ++i) { for (size_t i = 0; i < c.devices.size(); ++i) {
const CameraDeviceView& d = c.devices[i]; const CameraDeviceView& d = c.devices[i];
std::string label = i < c.labels.size() ? c.labels[i] : ("cam" + std::to_string(i)); std::string label = i < c.labels.size() ? c.labels[i] : ("cam" + std::to_string(i));
Element st = d.streaming ? (text("streaming") | color(Color::Green)) Element st = !d.offline.empty() ? (text("offline") | color(Color::Red))
: d.streaming ? (text("streaming") | color(Color::Green))
: (text("stopped") | color(Color::Yellow)); : (text("stopped") | color(Color::Yellow));
rows.push_back(hbox({text(label + " ") | bold, rows.push_back(hbox({text(label + " ") | bold,
text(d.model.empty() ? "?" : d.model) | color(Color::Cyan), text(d.model.empty() ? "?" : d.model) | color(Color::Cyan),
text(" " + d.id) | dim, filler(), st})); text(" " + d.id) | dim, filler(), st}));
rows.push_back(row(" serial", text(d.serial.empty() ? "—" : d.serial) | dim)); rows.push_back(row(" serial", text(d.serial.empty() ? "—" : d.serial) | dim));
if (!d.offline.empty())
rows.push_back(row(" offline", text(d.offline) | color(Color::Red)));
rows.push_back(row(" frame", text(std::to_string(d.width) + "x" + std::to_string(d.height) + rows.push_back(row(" frame", text(std::to_string(d.width) + "x" + std::to_string(d.height) +
" " + num(d.payload_bytes / 1048576.0, 1) + " MB"))); " " + num(d.payload_bytes / 1048576.0, 1) + " MB")));
rows.push_back(row(" exposure", text(num(d.exposure_us, 0) + " us gain " + rows.push_back(row(" exposure", text(num(d.exposure_us, 0) + " us gain " +
@ -460,8 +483,17 @@ Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
// --- Cameras: one block each, everything explicit --- // --- Cameras: one block each, everything explicit ---
const CameraConfigView& cf = c.config; const CameraConfigView& cf = c.config;
rows.push_back(separator()); rows.push_back(separator());
{
// Which backends: "Vimba X", "Vimba X + TarisIR".
std::string backends;
auto add = [&backends](const std::string& b) {
if (backends.find(b) == std::string::npos) backends += (backends.empty() ? "" : " + ") + b;
};
for (const auto& v : cf.cameras)
if (v.enabled) add(v.driver == "taris" ? "TarisIR" : "Vimba X");
rows.push_back(hbox({text("CAMERAS") | bold | color(Color::Blue), rows.push_back(hbox({text("CAMERAS") | bold | color(Color::Blue),
text(cf.mock ? " MOCK (simulated)" : " Vimba X") | dim})); text(cf.mock ? " MOCK (simulated)" : " " + backends) | dim}));
}
if (cf.cameras.empty()) rows.push_back(text("(no cameras configured)") | dim); if (cf.cameras.empty()) rows.push_back(text("(no cameras configured)") | dim);
for (const auto& v : cf.cameras) { for (const auto& v : cf.cameras) {
rows.push_back(hbox({ rows.push_back(hbox({
@ -481,7 +513,11 @@ Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
} }
rows.push_back(row(" settle", text(std::to_string(v.settle_wait_ms) + rows.push_back(row(" settle", text(std::to_string(v.settle_wait_ms) +
" ms after standstill before keeping a frame"))); " ms after standstill before keeping a frame")));
rows.push_back(row(" encoding", text("JPEG XL distance " + num(v.jxl_distance, 2) + rows.push_back(row(" encoding",
text(v.driver == "taris"
? "JPEG XL lossless 16-bit counts effort " +
std::to_string(v.jxl_effort)
: "JPEG XL distance " + num(v.jxl_distance, 2) +
" effort " + std::to_string(v.jxl_effort)))); " effort " + std::to_string(v.jxl_effort))));
rows.push_back(row(" rotation", text(v.rotation_deg == 0 rows.push_back(row(" rotation", text(v.rotation_deg == 0
? std::string("none") ? std::string("none")
@ -498,6 +534,7 @@ Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
formatDegrees(c.last_pitch_deg) + " " + formatDegrees(c.last_pitch_deg) + " " +
formatTimeAgo(now, c.last_ts_ms)))); formatTimeAgo(now, c.last_ts_ms))));
} }
if (c.has_thermal) rows.push_back(row("last thermal", thermalLine(c)));
// --- Scan grid --- // --- Scan grid ---
rows.push_back(separator()); rows.push_back(separator());
@ -967,7 +1004,8 @@ Element gimbalDetailPanel(const GimbalView& g, const DumpView& dump, const Calib
// then the ambient sensor. The two devices get their own headed sections, and // then the ambient sensor. The two devices get their own headed sections, and
// each temperature is labelled with its provenance ("internal" vs "ambient"), // each temperature is labelled with its provenance ("internal" vs "ambient"),
// so the reading the main window shows is never confused with the MTi's. // so the reading the main window shows is never confused with the MTi's.
Element sensorsDetailPanel(const ImuView& v, const EnvView& e, ScrollState& st) { Element sensorsDetailPanel(const ImuView& v, const EnvView& e, const HostView& h,
ScrollState& st) {
// Fixed-width, right-aligned to 2 decimals. The constant width keeps the sign // Fixed-width, right-aligned to 2 decimals. The constant width keeps the sign
// column and decimal point from jumping as values cross zero or change digit // column and decimal point from jumping as values cross zero or change digit
// count, so the readout stays steady instead of flickering. The width is kept // count, so the readout stays steady instead of flickering. The width is kept
@ -1085,6 +1123,79 @@ Element sensorsDetailPanel(const ImuView& v, const EnvView& e, ScrollState& st)
} }
} }
// LATTEPANDA CPU section: the computer's own heat and load, and how the CPU
// guard (ThermalGuard) is pacing the scan for it. Live like the two above,
// so it sits above the static IMU CONFIG block too.
{
auto row = [](const std::string& k, Element val) {
return hbox({text(k) | dim | size(WIDTH, EQUAL, 16), std::move(val)});
};
auto fx = [](double x, int d) {
char b[32];
std::snprintf(b, sizeof(b), "%.*f", d, x);
return std::string(b);
};
body.push_back(separator());
body.push_back(text("LattePanda \xE2\x80\x94 CPU") | bold | color(Color::Magenta));
if (!h.present) {
body.push_back(text("(no reading yet)") | dim);
} else {
const bool hot = h.temp_ok && h.cpu_c >= h.slow_c;
Element temp = h.temp_ok ? text(fx(h.cpu_c, 1) + " \xC2\xB0""C") | bold
: text("unknown") | dim;
if (hot) temp = temp | color(h.cpu_c >= h.pause_c ? Color::Red : Color::Yellow);
body.push_back(row("CPU temp", hbox({temp, text(" " + h.zone) | dim})));
if (h.clock_ok) {
std::string cores;
for (double m : h.core_mhz) cores += (cores.empty() ? "" : " ") + fx(m / 1000.0, 1);
std::string cap;
if (h.cap_mhz > 0.0 && h.hw_max_mhz > 0.0 && h.cap_mhz < h.hw_max_mhz - 1.0)
cap = ", capped at " + fx(h.cap_mhz / 1000.0, 1);
body.push_back(row("Clock", hbox({text(fx(h.mean_mhz / 1000.0, 2) + " GHz") | bold,
text(" of " + fx(h.hw_max_mhz / 1000.0, 1) +
cap + " (cores " + cores + ")") | dim})));
} else {
body.push_back(row("Clock", text("unknown") | dim));
}
body.push_back(row("Load",
hbox({text(h.busy_pct >= 0.0 ? fx(h.busy_pct, 0) + " % busy"
: std::string("-- % busy")) | bold,
text(" load " + fx(h.load1, 2) + " / " + fx(h.load5, 2) +
" / " + fx(h.load15, 2) + " on " +
std::to_string(h.cpus) + " cores") | dim})));
std::string thr = h.throttling ? "yes, now (" + h.throttle_cause + ")" : "no";
std::string thr_detail = h.counters_ok
? " " + std::to_string(h.throttle_last_min) + " in the last minute, " +
std::to_string(h.throttle_total) + " since start"
: " (the kernel does not count it here; judged from the clock)";
body.push_back(row("Throttled",
hbox({h.throttling ? text(thr) | color(Color::Red) | bold
: text(thr) | bold,
text(thr_detail) | dim})));
if (h.save_count > 0)
body.push_back(row("Saving", hbox({text(fx(h.save_mean_ms, 0) + " ms") | bold,
text(" per picture, longest " +
fx(h.save_max_ms, 0) + " ms, " +
std::to_string(h.save_count) +
" in the last minute") | dim})));
Element st = text(h.state);
if (!h.guard_on) st = text("off (full pace whatever the heat)") | color(Color::Yellow) | bold;
else if (h.state == "paused") st = st | color(Color::Red) | bold;
else if (h.state == "slowed") st = st | color(Color::Yellow) | bold;
else st = st | color(Color::Green);
std::string pace = " one picture every " + fx(h.interval_s, 1) + " s";
if (h.state == "paused") pace = " for " + std::to_string(h.paused_s) + " s";
body.push_back(row("Scan pace", hbox({st, text(pace) | dim})));
if (!h.reason.empty()) body.push_back(row("", text(h.reason) | dim));
if (h.held_s > 0)
body.push_back(row("", text("waiting " + std::to_string(h.held_s) +
" s for saving to catch up") | color(Color::Yellow)));
body.push_back(text("slows at " + fx(h.slow_c, 0) + " \xC2\xB0""C, pauses at " +
fx(h.pause_c, 0) + ", resumes at " + fx(h.resume_c, 0) +
" ('thermal' to see, 'thermal off|on|resume')") | dim);
}
}
// IMU CONFIG section (device configuration read back at startup). Shown // IMU CONFIG section (device configuration read back at startup). Shown
// whenever it is known, even if the live stream is offline. // whenever it is known, even if the live stream is offline.
const auto& c = v.config; const auto& c = v.config;
@ -1370,7 +1481,7 @@ void TuiUi::uiLoop() {
bottom}); bottom});
case Overlay::Sensors: case Overlay::Sensors:
return vbox({header, separator(), return vbox({header, separator(),
sensorsDetailPanel(s.imu, s.env, sensors_scroll) | flex, bottom}); sensorsDetailPanel(s.imu, s.env, s.host, sensors_scroll) | flex, bottom});
case Overlay::Cameras: case Overlay::Cameras:
return vbox({header, separator(), cameraDetailPanel(s.capture, cam_scroll) | flex, bottom}); return vbox({header, separator(), cameraDetailPanel(s.capture, cam_scroll) | flex, bottom});
case Overlay::Conn: case Overlay::Conn:

View File

@ -99,6 +99,15 @@ SensorsView pendingSensorsView() {
v.env.status = "pending"; v.env.status = "pending";
v.env.fields.push_back({"Temp", "--.-", "\xC2\xB0""C", false}); v.env.fields.push_back({"Temp", "--.-", "\xC2\xB0""C", false});
v.env.fields.push_back({"Humid", "--", "%RH", false}); v.env.fields.push_back({"Humid", "--", "%RH", false});
// The computer itself: its CPU's own temperature (not the box's - that is
// the ambient Temp above), clock, how busy it is, and whether it is
// slowing itself down to stay cool.
v.host.title = "LattePanda CPU";
v.host.status = "pending";
v.host.fields.push_back({"CPU temp", "--.-", "\xC2\xB0""C", false});
v.host.fields.push_back({"Clock", "-.-", "GHz", false});
v.host.fields.push_back({"Load", "--", "%", false});
v.host.fields.push_back({"Throttled", "--", "", false});
return v; return v;
} }

View File

@ -58,7 +58,11 @@ add_executable(fgc_tests
test_hostmetrics.cpp test_hostmetrics.cpp
test_testrunner.cpp test_testrunner.cpp
test_rawimage.cpp test_rawimage.cpp
test_thermalstats.cpp
test_composite.cpp
test_tariscamerasource.cpp
test_settlerule.cpp test_settlerule.cpp
test_thermalguard.cpp
) )
target_link_libraries(fgc_tests PRIVATE fgc_core doctest::doctest) target_link_libraries(fgc_tests PRIVATE fgc_core doctest::doctest)
# So a test can read the shipped config/config.example.ini and hold it to what # So a test can read the shipped config/config.example.ini and hold it to what

View File

@ -1,5 +1,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include <clocale>
#include "fgc/ChannelHistory.h" #include "fgc/ChannelHistory.h"
#include "fgc/ChannelTopics.h" #include "fgc/ChannelTopics.h"
@ -82,4 +84,52 @@ TEST_CASE("topic and payload builders match the documented wire format") {
CHECK(j.find("\"time\":1719312345678") != std::string::npos); CHECK(j.find("\"time\":1719312345678") != std::string::npos);
// The quality gate is gone; nothing should still be publishing a verdict. // The quality gate is gone; nothing should still be publishing a verdict.
CHECK(j.find("degraded") == std::string::npos); CHECK(j.find("degraded") == std::string::npos);
// A colour camera's event carries none of the thermal fields.
CHECK(j.find("\"cal\"") == std::string::npos);
CHECK(j == "{ \"fwt\":\"Tower\", \"cam\":\"RGB\", \"hdg\":1373, \"pit\":300, "
"\"time\":1719312345678 }");
}
TEST_CASE("a thermal CamEvent carries profile, packets, temperatures and hot spot") {
CamEvent c;
c.tower = "Tower"; c.camera = "TIR";
c.heading_decideg = 1234; c.pitch_decideg = -30; c.timestamp_ms = 1791234567890LL;
ThermalEventFields t;
t.cal = 0; t.missed = 2; t.stats_ok = true;
t.tmax_decic = 3015; t.tmed_decic = 187; t.hotx = 0.73594; t.hoty = 0.5833;
c.thermal = t;
CHECK(camEventJson(c) ==
"{ \"fwt\":\"Tower\", \"cam\":\"TIR\", \"hdg\":1234, \"pit\":-30, "
"\"time\":1791234567890, \"cal\":0, \"missed\":2, \"tmax\":3015, \"tmed\":187, "
"\"hotx\":0.7359, \"hoty\":0.5833 }");
// Without valid pixels the profile and packets still go out, the rest as null.
c.thermal->stats_ok = false;
const std::string j = camEventJson(c);
CHECK(j.find("\"cal\":0, \"missed\":2, \"tmax\":null, \"tmed\":null, \"hotx\":null, "
"\"hoty\":null }") != std::string::npos);
}
TEST_CASE("numbers in the JSON keep their '.' under a comma locale") {
// The thermal camera's SDK switches the C locale to one with a decimal comma.
const std::string before = std::setlocale(LC_NUMERIC, nullptr);
const bool comma = std::setlocale(LC_NUMERIC, "de_DE.UTF-8") != nullptr;
CamEvent c;
c.tower = "T"; c.camera = "TIR";
ThermalEventFields t;
t.stats_ok = true; t.hotx = 0.43359; t.hoty = 1.0;
c.thermal = t;
const std::string j = camEventJson(c);
std::setlocale(LC_NUMERIC, before.c_str());
CHECK(j.find("\"hotx\":0.4336, \"hoty\":1.0000") != std::string::npos);
if (!comma) MESSAGE("no de_DE locale here: checked under the C locale only");
HealthReport h;
h.interval_s = -2.25;
CHECK(healthJson(h).find("\"interval_s\":-2.3") != std::string::npos); // rounds half away
}
TEST_CASE("CamEvent names are escaped") {
CamEvent c;
c.tower = "a\"b"; c.camera = "RGB";
CHECK(camEventJson(c).find("\"fwt\":\"a\\\"b\"") != std::string::npos);
} }

239
tests/test_composite.cpp Normal file
View File

@ -0,0 +1,239 @@
#include "fgc/CompositeCameraSource.h"
#include <doctest/doctest.h>
#include <chrono>
#include <stdexcept>
#include <thread>
#include <vector>
using namespace fgc;
namespace {
// A source of `count` cameras whose trigger() delivers one frame per camera,
// stamped `ts`, when `ready` - and reports nothing otherwise.
struct FakeSource : ICameraSource {
int count;
long long ts;
bool counts_after_open = false; // as Vimba: no cameras until open()
bool opened = false;
bool ready = true;
bool throws_on_open = false;
int triggers = 0;
bool started = false;
bool frame_ready = true; // frameReady()
int moves = 0;
std::string why_offline;
FrameCallback cb;
FakeSource(int n, long long t) : count(n), ts(t) {}
void open() override {
if (throws_on_open) throw std::runtime_error("no camera");
opened = true;
}
void close() override {}
void start() override { started = true; }
void stop() override { started = false; }
void moving() override { ++moves; }
bool frameReady() override { return frame_ready; }
bool trigger() override {
++triggers;
if (!ready) return false;
for (int i = 0; i < count; ++i) {
Frame f;
f.cam_id = i;
f.timestamp_ms = ts + i;
cb(f);
}
return true;
}
void setFrameCallback(FrameCallback c) override { cb = std::move(c); }
int cameraCount() const override { return counts_after_open && !opened ? 0 : count; }
bool readFeature(int i, const std::string& name, double& v) override {
v = i * 100 + ts;
return name == "X";
}
std::string offline(int) override { return why_offline; }
std::vector<CameraDeviceInfo> deviceInfo() override {
std::vector<CameraDeviceInfo> out(count);
for (int i = 0; i < count; ++i) out[i].id = std::to_string(ts) + ":" + std::to_string(i);
return out;
}
bool grabFrame(int i, Frame& out, int) override {
out = Frame{};
out.cam_id = i;
out.timestamp_ms = ts + 50;
return true;
}
};
struct Rig {
FakeSource* colour = nullptr;
FakeSource* thermal = nullptr;
std::unique_ptr<CompositeCameraSource> src;
std::vector<Frame> frames;
explicit Rig(int colour_count = 1) {
auto c = std::make_unique<FakeSource>(colour_count, 1000);
auto t = std::make_unique<FakeSource>(1, 5000);
colour = c.get();
thermal = t.get();
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::move(c));
v.push_back(std::move(t));
src = std::make_unique<CompositeCameraSource>(std::move(v));
src->setFrameCallback([this](const Frame& f) { frames.push_back(f); });
}
};
} // namespace
TEST_CASE("the composite numbers the cameras leader first, and counts them all") {
Rig r(2);
CHECK(r.src->cameraCount() == 3);
REQUIRE(r.src->trigger());
REQUIRE(r.frames.size() == 3);
CHECK(r.frames[0].cam_id == 0);
CHECK(r.frames[1].cam_id == 1);
CHECK(r.frames[2].cam_id == 2); // the thermal camera
}
TEST_CASE("every frame keeps the time it arrived, the follower's too") {
Rig r;
REQUIRE(r.src->trigger());
REQUIRE(r.frames.size() == 2);
CHECK(r.frames[0].timestamp_ms == 1000);
CHECK(r.frames[1].timestamp_ms == 5000); // its own, not the leader's
}
TEST_CASE("the followers are asked only once the leader has delivered") {
Rig r;
r.colour->ready = false;
CHECK_FALSE(r.src->trigger());
CHECK_FALSE(r.src->trigger());
CHECK(r.thermal->triggers == 0); // no thermal picture without its colour one
CHECK(r.frames.empty());
r.colour->ready = true;
CHECK(r.src->trigger());
CHECK(r.thermal->triggers == 1);
}
TEST_CASE("a follower with nothing to give costs only its own picture") {
Rig r;
r.thermal->ready = false;
CHECK(r.src->trigger());
REQUIRE(r.frames.size() == 1);
CHECK(r.frames[0].cam_id == 0);
}
TEST_CASE("calls about one camera reach the source that has it") {
Rig r(2);
double v = 0;
REQUIRE(r.src->readFeature(1, "X", v));
CHECK(v == 1100); // colour source, its camera 1
REQUIRE(r.src->readFeature(2, "X", v));
CHECK(v == 5000); // thermal source, its camera 0
CHECK_FALSE(r.src->readFeature(3, "X", v));
r.thermal->why_offline = "unplugged";
CHECK(r.src->offline(0).empty());
CHECK(r.src->offline(2) == "unplugged");
const auto d = r.src->deviceInfo();
REQUIRE(d.size() == 3);
CHECK(d[2].id == "5000:0");
// A grab outside a trigger keeps its own time, under the global index.
Frame f;
REQUIRE(r.src->grabFrame(2, f, 10));
CHECK(f.cam_id == 2);
CHECK(f.timestamp_ms == 5050);
}
TEST_CASE("the indices are the configured ones, also before the leader has opened") {
// On tower 111 the thermal frame came out as camera 0 - "RGB", overwriting
// the colour file of the same time - because Vimba counts no camera before
// open() and the offsets were taken from that.
auto c = std::make_unique<FakeSource>(1, 1000);
auto t = std::make_unique<FakeSource>(1, 5000);
c->counts_after_open = true;
FakeSource* colour = c.get();
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::move(c));
v.push_back(std::move(t));
CompositeCameraSource src(std::move(v), {1, 1});
std::vector<Frame> frames;
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
CHECK(src.cameraCount() == 1); // what is open: the thermal camera only
src.open();
CHECK(colour->opened);
CHECK(src.cameraCount() == 2);
REQUIRE(src.trigger());
REQUIRE(frames.size() == 2);
CHECK(frames[0].cam_id == 0);
CHECK(frames[1].cam_id == 1);
CHECK(src.offline(1).empty());
CHECK_THROWS(CompositeCameraSource(std::vector<std::unique_ptr<ICameraSource>>{}, {}));
}
TEST_CASE("a picture waits a moment for a follower between frames, then goes without it") {
auto c = std::make_unique<FakeSource>(1, 1000);
auto t = std::make_unique<FakeSource>(1, 5000);
FakeSource* colour = c.get();
FakeSource* thermal = t.get();
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::move(c));
v.push_back(std::move(t));
CompositeCameraSource src(std::move(v), {1, 1}, 60);
std::vector<Frame> frames;
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
// The thermal camera is in its NUC: the picture is held, the leader not asked.
thermal->frame_ready = false;
CHECK_FALSE(src.trigger());
CHECK(colour->triggers == 0);
CHECK_FALSE(src.frameReady());
// It comes back within the wait: one whole pair.
thermal->frame_ready = true;
REQUIRE(src.trigger());
CHECK(frames.size() == 2);
// It does not come back: after the wait the colour picture goes alone.
frames.clear();
thermal->frame_ready = false;
CHECK_FALSE(src.trigger());
std::this_thread::sleep_for(std::chrono::milliseconds(80));
REQUIRE(src.trigger());
CHECK(colour->triggers == 2);
}
TEST_CASE("every source hears that the gimbal moves") {
Rig r;
r.src->moving();
CHECK(r.colour->moves == 1);
CHECK(r.thermal->moves == 1);
}
TEST_CASE("start and stop reach every source") {
Rig r;
r.src->start();
CHECK(r.colour->started);
CHECK(r.thermal->started);
r.src->stop();
CHECK_FALSE(r.thermal->started);
}
TEST_CASE("a follower that will not open does not stop the run; the leader does") {
{
Rig r;
r.thermal->throws_on_open = true;
CHECK_NOTHROW(r.src->open());
}
{
Rig r;
r.colour->throws_on_open = true;
CHECK_THROWS(r.src->open());
}
}

View File

@ -275,7 +275,7 @@ TEST_CASE("Camera blocks reject what cannot work") {
}; };
CHECK_NOTHROW(ConfigLoader::fromMap(base({}))); CHECK_NOTHROW(ConfigLoader::fromMap(base({})));
// No second backend exists yet; accepting the key silently would be worse. // An unknown backend; accepting the key silently would be worse.
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.rgb.driver", "thermal"}}))); CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.rgb.driver", "thermal"}})));
// An enabled camera with no id cannot be opened. // An enabled camera with no id cannot be opened.
CHECK_THROWS(ConfigLoader::fromMap({{"Camera.rgb.profile_dir", "/tmp/p"}})); CHECK_THROWS(ConfigLoader::fromMap({{"Camera.rgb.profile_dir", "/tmp/p"}}));
@ -288,6 +288,96 @@ TEST_CASE("Camera blocks reject what cannot work") {
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.rgb.standard.settle_wait", "500"}}))); CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.rgb.standard.settle_wait", "500"}})));
} }
TEST_CASE("A thermal camera needs no settings XML and is always lossless") {
AppConfig c = ConfigLoader::fromMap({
{"Camera.jxl_distance", "0.8"},
{"Camera.rgb.id", "DEV_1"},
{"Camera.rgb.profile_dir", "/tmp/p"},
{"Camera.tir.driver", "taris"},
{"Camera.tir.id", "2017744"},
{"Camera.tir.label", "TIR"},
{"Camera.tir.calib_profile", "1"},
{"Camera.tir.frame_rate_hz", "9"},
{"Camera.tir.interface", "enp1s0"},
{"Camera.tir.standard.settle_wait_ms", "200"},
});
REQUIRE(c.camera.cameras.size() == 2);
const CameraSpec& tir = c.camera.cameras[1];
CHECK(tir.thermal());
CHECK_FALSE(c.camera.cameras[0].thermal());
CHECK(tir.label == "TIR");
CHECK(tir.profile_dir.empty());
CHECK(tir.calib_profile == 1);
CHECK(tir.frame_rate_hz == doctest::Approx(9.0));
CHECK(tir.max_frame_age_ms == 400);
CHECK(tir.library == "libirbgrab_x86_64-linux.so");
CHECK(tir.interface == "enp1s0");
CHECK(tir.nuc_interval_s == 30);
// [Camera] jxl_distance is for colour cameras; counts default to lossless.
CHECK(tir.jxl_distance == 0.0);
CHECK(c.camera.cameras[0].jxl_distance == doctest::Approx(0.8));
CHECK(c.camera.maxSettleWaitMs() == 1000); // the colour camera's default is longer
}
TEST_CASE("A thermal camera without a timing block does not slow the scan") {
AppConfig c = ConfigLoader::fromMap({
{"Camera.rgb.id", "DEV_1"},
{"Camera.rgb.profile_dir", "/tmp/p"},
{"Camera.rgb.standard.settle_wait_ms", "500"},
{"Camera.rgb.standard.frame_timeout_ms", "700"},
{"Camera.tir.driver", "taris"},
{"Camera.tir.id", "2017744"},
});
CHECK(c.camera.maxSettleWaitMs() == 500);
CHECK(c.camera.maxFrameTimeoutMs() == 700);
}
TEST_CASE("Colour cameras come first whatever the block names") {
// Camera 0 is the one the keep rule reads; a thermal camera cannot be it.
AppConfig c = ConfigLoader::fromMap({
{"Camera.a_tir.driver", "taris"},
{"Camera.a_tir.id", "2017744"},
{"Camera.rgb.id", "DEV_1"},
{"Camera.rgb.profile_dir", "/tmp/p"},
{"Camera.nir.id", "DEV_2"},
{"Camera.nir.profile_dir", "/tmp/p"},
});
REQUIRE(c.camera.cameras.size() == 3);
CHECK(c.camera.cameras[0].name == "nir");
CHECK(c.camera.cameras[1].name == "rgb");
CHECK(c.camera.cameras[2].name == "a_tir");
}
TEST_CASE("Thermal camera blocks reject what cannot work") {
auto base = [](std::map<std::string, std::string> extra) {
std::map<std::string, std::string> kv = {{"Camera.rgb.id", "DEV_1"},
{"Camera.rgb.profile_dir", "/tmp/p"},
{"Camera.tir.driver", "taris"},
{"Camera.tir.id", "2017744"}};
for (auto& [k, v] : extra) kv[k] = v;
return kv;
};
CHECK_NOTHROW(ConfigLoader::fromMap(base({})));
// Counts are measurements: lossy coding would change them.
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.jxl_distance", "0.5"}})));
CHECK_NOTHROW(ConfigLoader::fromMap(base({{"Camera.tir.jxl_distance", "0"}})));
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.id", ""}})));
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.calib_profile", "-1"}})));
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.frame_rate_hz", "0"}})));
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.frame_rate_hz", "60"}})));
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.max_frame_age_ms", "0"}})));
// An empty value means the default, as for every key.
CHECK(ConfigLoader::fromMap(base({{"Camera.tir.library", ""}})).camera.cameras[1].library ==
"libirbgrab_x86_64-linux.so");
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.tir.nuc_interval_s", "-1"}})));
CHECK_NOTHROW(ConfigLoader::fromMap(base({{"Camera.tir.nuc_interval_s", "0"}})));
// Two cameras, one output folder: their pictures would overwrite each other.
CHECK_THROWS(ConfigLoader::fromMap(base({{"Camera.rgb.label", "X"}, {"Camera.tir.label", "X"}})));
// A disabled thermal camera needs no serial, like any other.
CHECK_NOTHROW(ConfigLoader::fromMap(
base({{"Camera.tir.id", ""}, {"Camera.tir.enabled", "false"}})));
}
TEST_CASE("updateIniSectionKeys replaces in-section keys, leaving others intact") { TEST_CASE("updateIniSectionKeys replaces in-section keys, leaving others intact") {
const std::string in = const std::string in =
"[General]\n" "[General]\n"

View File

@ -5,6 +5,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include <chrono> #include <chrono>
#include <cstring>
#include <filesystem> #include <filesystem>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@ -65,6 +66,24 @@ Frame bayerFrame(PixelLayout layout, uint32_t w, uint32_t h, int r, int g, int b
return f; return f;
} }
// A thermal frame of counts: `room` everywhere, `hot` at (hx, hy).
Frame thermalFrame(uint32_t w, uint32_t h, uint16_t room, uint16_t hot, uint32_t hx, uint32_t hy,
long long ts, bool with_info = true) {
Frame f;
f.width = w; f.height = h; f.layout = PixelLayout::Mono16; f.channels = 1; f.timestamp_ms = ts;
std::vector<uint16_t> px(static_cast<size_t>(w) * h, room);
px[static_cast<size_t>(hy) * w + hx] = hot;
f.data.resize(px.size() * 2);
std::memcpy(f.data.data(), px.data(), f.data.size());
if (with_info) {
ThermalFrameInfo info;
info.cal = 0;
info.missed_packets = 3;
f.thermal = info;
}
return f;
}
bool waitSaved(const ImagePipeline& p, long long n, int ms = 20000) { bool waitSaved(const ImagePipeline& p, long long n, int ms = 20000) {
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
while (p.imagesSaved() < n && std::chrono::steady_clock::now() < end) while (p.imagesSaved() < n && std::chrono::steady_clock::now() < end)
@ -167,6 +186,86 @@ TEST_CASE("rotation applies to raw frames too") {
CHECK(interiorMean(d, 48, 64, 0) == doctest::Approx(3200).epsilon(0.01)); CHECK(interiorMean(d, 48, 64, 0) == doctest::Approx(3200).epsilon(0.01));
} }
TEST_CASE("thermal counts are saved 16-bit lossless with their temperatures in the event") {
TempDir t;
FakeChannel ch;
auto p = params(t, 0.8); // a lossy setting must not reach the counts
p.cameras = {{"TIR", 0, 0.8, 1, true}};
ImagePipeline pipe(ch, [] { return Orientation{123.4f, -3.0f}; }, p);
pipe.start();
const Frame f = thermalFrame(64, 48, 29315, 57315, 40, 12, 4242);
pipe.submit(f);
REQUIRE(waitSaved(pipe, 1));
pipe.stop();
const std::string file = onlyFile(t);
CHECK(fs::path(file).parent_path().filename() == "TIR");
CHECK(fs::path(file).filename() == "4242.jxl");
const auto d = decode(file, 1);
CHECK(d.bits == 16);
CHECK(d.channels == 1);
REQUIRE(d.px.size() == 64u * 48);
CHECK(std::memcmp(d.px.data(), f.data.data(), f.data.size()) == 0); // bit-exact
REQUIRE(ch.count() == 1);
const CamEvent& e = ch.events[0];
CHECK(e.camera == "TIR");
CHECK(e.heading_decideg == 1234);
REQUIRE(e.thermal);
CHECK(e.thermal->cal == 0);
CHECK(e.thermal->missed == 3);
CHECK(e.thermal->stats_ok);
CHECK(e.thermal->tmax_decic == 3000);
CHECK(e.thermal->tmed_decic == 200);
CHECK(e.thermal->hotx == doctest::Approx(40.5 / 64));
CHECK(e.thermal->hoty == doctest::Approx(12.5 / 48));
}
TEST_CASE("the thermal hot spot is where it is in the stored, rotated image") {
TempDir t;
FakeChannel ch;
auto p = params(t);
p.cameras = {{"TIR", 90, 0.0, 1, true}};
ImagePipeline pipe(ch, nullptr, p);
pipe.start();
// 64 x 48, hot at (40, 12). Rotated 90 deg clockwise it is 48 x 64, and a
// pixel at (x, y) moves to (H-1-y, x) = (35, 40).
pipe.submit(thermalFrame(64, 48, 29315, 40000, 40, 12, 5));
REQUIRE(waitSaved(pipe, 1));
pipe.stop();
const auto d = decode(onlyFile(t), 1);
REQUIRE(d.px.size() == 48u * 64);
CHECK(d.px[40 * 48 + 35] == 40000);
REQUIRE(ch.events[0].thermal);
CHECK(ch.events[0].thermal->hotx == doctest::Approx(35.5 / 48));
CHECK(ch.events[0].thermal->hoty == doctest::Approx(40.5 / 64));
}
TEST_CASE("a Mono16 frame without thermal info is saved, but its event has no thermal fields") {
TempDir t;
FakeChannel ch;
ImagePipeline pipe(ch, nullptr, params(t));
pipe.start();
pipe.submit(thermalFrame(32, 16, 1000, 2000, 1, 1, 6, false));
REQUIRE(waitSaved(pipe, 1));
pipe.stop();
CHECK(decode(onlyFile(t), 1).bits == 16);
REQUIRE(ch.count() == 1);
CHECK_FALSE(ch.events[0].thermal);
}
TEST_CASE("an all-zero thermal frame is saved, its temperatures unknown") {
TempDir t;
FakeChannel ch;
ImagePipeline pipe(ch, nullptr, params(t));
pipe.start();
pipe.submit(thermalFrame(32, 16, 0, 0, 0, 0, 7));
REQUIRE(waitSaved(pipe, 1));
pipe.stop();
REQUIRE(ch.events[0].thermal);
CHECK_FALSE(ch.events[0].thermal->stats_ok);
}
TEST_CASE("several workers save every frame") { TEST_CASE("several workers save every frame") {
TempDir t; TempDir t;
FakeChannel ch; FakeChannel ch;
@ -185,6 +284,22 @@ TEST_CASE("several workers save every frame") {
CHECK(files == 9); CHECK(files == 9);
} }
TEST_CASE("save timing counts the pictures saved in the last minute and how long they took") {
TempDir t;
FakeChannel ch;
ImagePipeline pipe(ch, nullptr, params(t, 1.0));
CHECK(pipe.saveTiming().count == 0);
pipe.start();
for (int i = 0; i < 3; ++i)
pipe.submit(bayerFrame(PixelLayout::BayerRG12p, 64, 48, 3000, 1500, 700, 7000 + i));
REQUIRE(waitSaved(pipe, 3));
pipe.stop();
const auto s = pipe.saveTiming();
CHECK(s.count == 3);
CHECK(s.mean_ms > 0.0);
CHECK(s.max_ms >= s.mean_ms);
}
TEST_CASE("canAccept says no once queue_max frames are waiting, yes once they are saved") { TEST_CASE("canAccept says no once queue_max frames are waiting, yes once they are saved") {
TempDir t; TempDir t;
FakeChannel ch; FakeChannel ch;

View File

@ -91,6 +91,21 @@ TEST_CASE("12-bit lossless decodes exactly, extremes included") {
} }
} }
TEST_CASE("16-bit single-channel lossless decodes exactly (thermal counts)") {
TempDir t;
// Counts around room temperature, plus the full range's ends.
auto src = image(96, 64, 1, 65535, 11);
for (size_t i = 0; i < src.size(); i += 3) src[i] = static_cast<uint16_t>(28000 + i % 2500);
src.front() = 0;
src.back() = 65535;
REQUIRE(JpegXlEncoder::encodeToFile(t.file("ll16.jxl"), src.data(), 96, 64, 1, 16, 0.0f, 1, 2));
const auto d = decode(t.file("ll16.jxl"), 1);
CHECK(d.bits == 16);
CHECK(d.channels == 1);
CHECK(d.original_profile);
CHECK(d.px == src);
}
TEST_CASE("lossy keeps the bit depth and stays close") { TEST_CASE("lossy keeps the bit depth and stays close") {
TempDir t; TempDir t;
SUBCASE("12-bit colour") { SUBCASE("12-bit colour") {

View File

@ -22,11 +22,26 @@ std::vector<uint8_t> pack12p(const std::vector<uint16_t>& v) {
return out; return out;
} }
// Pack 12-bit values the GigE Vision 1.x way (BayerRG12Packed, the Manta's):
// the first byte carries p0's high bits.
std::vector<uint8_t> pack12Packed(const std::vector<uint16_t>& v) {
std::vector<uint8_t> out((v.size() * 3 + 1) / 2);
for (size_t i = 0, o = 0; i < v.size(); i += 2, o += 3) {
const uint16_t p0 = v[i];
const uint16_t p1 = i + 1 < v.size() ? v[i + 1] : 0;
out[o] = static_cast<uint8_t>(p0 >> 4);
out[o + 1] = static_cast<uint8_t>((p0 & 0x0F) | ((p1 & 0x0F) << 4));
if (o + 2 < out.size()) out[o + 2] = static_cast<uint8_t>(p1 >> 4);
}
return out;
}
} // namespace } // namespace
TEST_CASE("layout names round-trip, and unknown formats are refused") { TEST_CASE("layout names round-trip, and unknown formats are refused") {
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8, for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8,
PixelLayout::BayerRG12p}) PixelLayout::BayerRG12p, PixelLayout::BayerRG12Packed,
PixelLayout::Mono16})
CHECK(layoutFromName(layoutName(l)) == l); CHECK(layoutFromName(layoutName(l)) == l);
CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled
CHECK(layoutFromName("YCbCr422_8_CbYCrY") == PixelLayout::Unknown); CHECK(layoutFromName("YCbCr422_8_CbYCrY") == PixelLayout::Unknown);
@ -45,6 +60,23 @@ TEST_CASE("layout properties") {
CHECK(layoutFrameBytes(PixelLayout::BayerRG8, 5328, 3040) == 16197120); CHECK(layoutFrameBytes(PixelLayout::BayerRG8, 5328, 3040) == 16197120);
CHECK(layoutFrameBytes(PixelLayout::RGB8, 5328, 3040) == 48591360); CHECK(layoutFrameBytes(PixelLayout::RGB8, 5328, 3040) == 48591360);
CHECK(layoutFrameBytes(PixelLayout::BayerRG12p, 3, 1) == 5); // odd pixel count rounds up CHECK(layoutFrameBytes(PixelLayout::BayerRG12p, 3, 1) == 5); // odd pixel count rounds up
// The Manta G-1236C's full frame in BayerRG12Packed: its PayloadSize.
CHECK(layoutIsBayer(PixelLayout::BayerRG12Packed));
CHECK(layoutBits(PixelLayout::BayerRG12Packed) == 12);
CHECK(layoutFrameBytes(PixelLayout::BayerRG12Packed, 4112, 3008) == 18553344);
// The thermal camera: 640 x 480 counts, two bytes each.
CHECK_FALSE(layoutIsBayer(PixelLayout::Mono16));
CHECK(layoutBits(PixelLayout::Mono16) == 16);
CHECK(layoutFrameBytes(PixelLayout::Mono16, 640, 480) == 614400);
}
TEST_CASE("brightness sample: 16-bit is reported on the 0..255 scale") {
std::vector<uint16_t> v(640 * 480, 0x8000);
const auto* b = reinterpret_cast<const uint8_t*>(v.data());
const auto s = sampleBrightness(b, v.size() * 2, PixelLayout::Mono16, 640, 480);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(128.0));
CHECK(s.clipped == doctest::Approx(0.0));
} }
TEST_CASE("unpack12p matches GenICam 12p, by hand") { TEST_CASE("unpack12p matches GenICam 12p, by hand") {
@ -72,6 +104,36 @@ TEST_CASE("unpack12p round-trips every value, odd counts included") {
} }
} }
TEST_CASE("unpack12Packed matches GigE Vision 12Packed, by hand") {
// p0 = 0xABC, p1 = 0x123: bytes AB, 3C, 12.
const uint8_t in[3] = {0xAB, 0x3C, 0x12};
uint16_t out[2] = {};
unpack12Packed(in, out, 2);
CHECK(out[0] == 0xABC);
CHECK(out[1] == 0x123);
// The same bytes read as 12p are other values: the two must not be confused.
unpack12p(in, out, 2);
CHECK(out[0] != 0xABC);
}
TEST_CASE("unpack12Packed round-trips every value, odd counts included") {
std::mt19937 rng(1236);
std::vector<uint16_t> src(1001);
for (auto& v : src) v = static_cast<uint16_t>(rng() % 4096);
src[0] = 0;
src[1] = 4095;
for (size_t n : {size_t(1000), size_t(1001)}) {
CAPTURE(n);
std::vector<uint16_t> part(src.begin(), src.begin() + n);
const auto packed = pack12Packed(part);
std::vector<uint16_t> out(n, 0xFFFF);
unpack12(PixelLayout::BayerRG12Packed, packed.data(), out.data(), n);
CHECK(out == part);
unpack12(PixelLayout::BayerRG12p, pack12p(part).data(), out.data(), n);
CHECK(out == part);
}
}
TEST_CASE("brightness sample: 8-bit layouts") { TEST_CASE("brightness sample: 8-bit layouts") {
const uint32_t w = 400, h = 300; const uint32_t w = 400, h = 300;
SUBCASE("mid grey") { SUBCASE("mid grey") {
@ -104,6 +166,12 @@ TEST_CASE("brightness sample: 12-bit is reported on the 0..255 scale") {
s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12p, w, h); s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12p, w, h);
CHECK(s.mean == doctest::Approx(4095 / 16.0)); CHECK(s.mean == doctest::Approx(4095 / 16.0));
CHECK(s.clipped == 1); CHECK(s.clipped == 1);
// GigE 12Packed, with values that read differently as 12p: 0x640 = 1600.
std::fill(v.begin(), v.end(), 1600);
f = pack12Packed(v);
s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12Packed, w, h);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(100));
} }
TEST_CASE("brightness sample refuses what it cannot read") { TEST_CASE("brightness sample refuses what it cannot read") {

View File

@ -26,6 +26,7 @@ struct FakeMotor : IMotorController {
struct FakeCamera : ICameraSource { struct FakeCamera : ICameraSource {
int triggers = 0; int triggers = 0;
int moves = 0;
bool ok = true; bool ok = true;
// A stream of frames for the keep rule; empty = a camera with no frame data. // A stream of frames for the keep rule; empty = a camera with no frame data.
std::vector<FrameStats> stream; std::vector<FrameStats> stream;
@ -44,6 +45,7 @@ struct FakeCamera : ICameraSource {
void start() override {} void start() override {}
void stop() override {} void stop() override {}
bool trigger() override { ++triggers; return ok; } bool trigger() override { ++triggers; return ok; }
void moving() override { ++moves; }
void setFrameCallback(FrameCallback) override {} void setFrameCallback(FrameCallback) override {}
int cameraCount() const override { return 1; } int cameraCount() const override { return 1; }
}; };
@ -85,6 +87,7 @@ TEST_CASE("CaptureScheduler sweeps the scan grid with MOVE + settle + trigger")
CHECK(motor.cmds[0] == "MOVE 0,0"); CHECK(motor.cmds[0] == "MOVE 0,0");
CHECK(sch.moving()); CHECK(sch.moving());
CHECK(cam.triggers == 0); // not yet settled CHECK(cam.triggers == 0); // not yet settled
CHECK(cam.moves == 1); // the cameras hear of every move (the thermal NUC)
// At rest early: the frame waits for the interval, counted from the mode start. // At rest early: the frame waits for the interval, counted from the mode start.
settleAt(motor, 0, 0); settleAt(motor, 0, 0);
@ -103,6 +106,7 @@ TEST_CASE("CaptureScheduler sweeps the scan grid with MOVE + settle + trigger")
clock = 1010; clock = 1010;
sch.tick(); sch.tick();
CHECK(motor.cmds.back() == "MOVE 900,0"); CHECK(motor.cmds.back() == "MOVE 900,0");
CHECK(cam.moves == 2);
CHECK(sch.overruns() == 0); CHECK(sch.overruns() == 0);
} }
@ -914,3 +918,109 @@ TEST_CASE("the keep rule starts afresh at each waypoint") {
} }
CHECK(r.cam.triggers == 2); CHECK(r.cam.triggers == 2);
} }
TEST_CASE("the CPU guard's pause finishes the waypoint under way, parks, and loses nothing") {
long long clock = 0;
FakeMotor motor;
FakeCamera cam;
ScanGrid grid({{0.0, 0.0}, {90.0, 0.0}});
CaptureScheduler sch(motor, cam, 1.0 /*1000 ms*/, testGeometry(), grid,
[&] { return clock; });
sch.setSettleWait(0, 0);
sch.setMode(Mode::Auto);
clock = 1100; sch.tick(); // first move
REQUIRE(motor.cmds.size() == 1);
sch.setPaused(true); // paused mid-waypoint
CHECK_FALSE(sch.pausedAtRest()); // the waypoint is still under way
settleAt(motor, 0, 0);
clock = 1200; sch.tick();
clock = 1300; sch.tick();
CHECK(cam.triggers == 1); // ...and keeps its frame
CHECK(sch.pausedAtRest());
clock = 60000; sch.tick();
CHECK(motor.cmds.size() == 1); // parked: no next move however long
CHECK(cam.triggers == 1);
sch.setPaused(false);
clock = 60100; sch.tick();
REQUIRE(motor.cmds.size() == 2);
CHECK(motor.cmds[1] == "MOVE 900,0"); // the waypoint it was due, not skipped
motor.tel.yaw.standstill = false;
clock = 60200; settleAt(motor, 900, 0); sch.tick();
clock = 60300; sch.tick();
CHECK(cam.triggers == 2);
CHECK(sch.overruns() == 0); // the pause is not an overrun
}
TEST_CASE("the time held for saving adds up, the hold under way included") {
long long clock = 0;
FakeMotor motor;
FakeCamera cam;
ScanGrid grid({{0.0, 0.0}, {0.0, 0.0}});
CaptureScheduler sch(motor, cam, 1.0 /*1000 ms*/, testGeometry(), grid,
[&] { return clock; });
sch.setSettleWait(0, 0);
bool room = false;
sch.setSaveGate([&room] { return room; });
sch.setMode(Mode::Auto);
settleAt(motor, 0, 0);
clock = 1000; sch.tick(); // held from here
clock = 3000; sch.tick();
CHECK(sch.heldForSaveTotalMs() == 2000); // still held: counted so far
room = true;
clock = 3500; sch.tick(); // released after 2.5 s
CHECK(sch.heldForSaveTotalMs() == 2500);
clock = 9000;
CHECK(sch.heldForSaveTotalMs() == 2500); // not held: no more
}
TEST_CASE("the CPU guard's interval scale spaces pictures further apart") {
long long clock = 0;
FakeMotor motor;
FakeCamera cam;
ScanGrid grid({{0.0, 0.0}, {0.0, 0.0}});
CaptureScheduler sch(motor, cam, 1.0 /*1000 ms*/, testGeometry(), grid,
[&] { return clock; });
sch.setSettleWait(0, 0);
sch.setMode(Mode::Auto);
sch.setIntervalScale(2.0);
CHECK(sch.intervalMs() == doctest::Approx(2000.0));
settleAt(motor, 0, 0);
clock = 10; sch.tick(); // move
clock = 100; sch.tick(); // standstill
clock = 1500; sch.tick();
CHECK(cam.triggers == 0); // 1 s would have fired; 2 s has not
clock = 2000; sch.tick();
CHECK(cam.triggers == 1);
sch.setIntervalScale(0.5); // never faster than configured
CHECK(sch.intervalScale() == doctest::Approx(1.0));
CHECK(sch.intervalMs() == doctest::Approx(1000.0));
}
TEST_CASE("the CPU guard's pause stops continuous capture keeping frames; a snapshot still fires") {
long long clock = 0;
FakeMotor motor;
FakeCamera cam;
ScanGrid grid({{0.0, 0.0}});
CaptureScheduler sch(motor, cam, 1.0 /*1000 ms*/, testGeometry(), grid,
[&] { return clock; });
sch.setSettleWait(0, 0);
sch.setContinuous(true);
settleAt(motor, 0, 0);
sch.setPaused(true);
for (clock = 100; clock < 5000; clock += 100) sch.tick();
CHECK(cam.triggers == 0);
sch.requestSnapshot();
for (clock = 5000; clock < 5300; clock += 100) sch.tick();
CHECK(cam.triggers == 1);
sch.setPaused(false);
for (clock = 5300; clock < 7000; clock += 100) sch.tick();
CHECK(cam.triggers >= 2);
}

View File

@ -0,0 +1,478 @@
#include "fgc/TarisCameraSource.h"
#include <doctest/doctest.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace fgc;
using namespace fgc::irbgrab;
namespace {
// A stand-in for the IRBGRAB SDK and one TarisIR mini behind it. The source
// calls it from its worker thread; the test changes it from its own.
struct FakeSdk {
std::mutex m;
std::vector<std::string> search{"SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0"};
std::string accept = "#0"; // connect succeeds for strings ending like this
std::vector<std::string> connect_attempts;
int cal = 0;
int calib_sets = 0;
float rate = 0.f;
bool grabbing = false;
int inits = 0;
int connects = 0;
// The newest frame: counts, and the camera's number for it.
std::vector<uint16_t> px;
int64_t image_num = 0; // ours; the camera reports 0 for every frame
int lut_calls = 0; // the first LUTs after connecting are empty
uint32_t w = 8, h = 4;
Ret no_frame = kErrNotFound; // what getdata says before any frame
std::atomic<bool> frozen{false}; // stop producing frames (stall)
std::atomic<int> nuc_interval{30}; // the camera's own NUC timer, s
std::atomic<int> nucs{0}; // NUC_Perform calls
std::atomic<int> nuc_mode{0};
void push(uint16_t v, uint16_t hot = 0) {
std::lock_guard<std::mutex> l(m);
px.assign(static_cast<size_t>(w) * h, v);
if (hot) px[5] = hot;
++image_num;
}
};
FakeSdk* g = nullptr;
void copyText(const std::string& s, char* buf, int len) {
std::memset(buf, 0, static_cast<size_t>(len));
std::memcpy(buf, s.data(), std::min<size_t>(s.size(), static_cast<size_t>(len) - 1));
}
Ret fInit() { std::lock_guard<std::mutex> l(g->m); ++g->inits; return kSuccess; }
void fUninit() {}
Ret fVersion(char* b, int n) { copyText("4.3.44", b, n); return kSuccess; }
Ret fTypes(char* b, int n) { copyText("Simulator;TARISirmini", b, n); return kSuccess; }
Ret fCreate(Handle* d, int32_t type, char*) { *d = type == 1 ? 42 : 0; return type == 1 ? kSuccess : kErrNotFound; }
Ret fFree(Handle* d) { *d = 0; return kSuccess; }
Ret fSearch(Handle, int32_t* n) {
std::lock_guard<std::mutex> l(g->m);
*n = static_cast<int32_t>(g->search.size());
return kSuccess;
}
Ret fConnect(Handle, char* s) {
std::lock_guard<std::mutex> l(g->m);
const std::string str = s;
g->connect_attempts.push_back(str);
const bool ok = str.size() >= g->accept.size() &&
str.compare(str.size() - g->accept.size(), g->accept.size(), g->accept) == 0;
if (ok) { ++g->connects; g->lut_calls = 0; }
return ok ? kSuccess : kTypeErr | 0x10;
}
Ret fDisconnect(Handle) { return kSuccess; }
Ret fStart(Handle, int32_t) { std::lock_guard<std::mutex> l(g->m); g->grabbing = true; return kSuccess; }
Ret fStop(Handle, int32_t) { std::lock_guard<std::mutex> l(g->m); g->grabbing = false; return kSuccess; }
Ret fGetParam(Handle, uint32_t what, void* data, uint32_t type) {
std::lock_guard<std::mutex> l(g->m);
if (what == param::kSearchString && type == kTypeIdxString) {
auto* p = static_cast<IdxString*>(data);
if (p->index < 0 || p->index >= static_cast<int>(g->search.size())) return kErrNotFound;
copyText(g->search[p->index], p->value.text, p->value.len);
return kSuccess;
}
if (what == param::kCalibActive && type == kTypeIdxInt32) {
static_cast<IdxInt32*>(data)->value = g->cal;
return kSuccess;
}
if (what == param::kCalibCount && type == kTypeInt32) { *static_cast<int32_t*>(data) = 2; return kSuccess; }
if (what == param::kCalibChangeInProgress && type == kTypeInt32) { *static_cast<int32_t*>(data) = 0; return kSuccess; }
if (what == param::kCalibName && type == kTypeIdxString) {
auto* p = static_cast<IdxString*>(data);
copyText(p->index == 0 ? "FF (-40-120C) #4101" : "FF (0-600C) #4102", p->value.text, p->value.len);
return kSuccess;
}
if (what == param::kCameraName && type == kTypeString) {
auto* p = static_cast<String*>(data);
copyText("TARISirmini", p->text, p->len);
return kSuccess;
}
if (what == param::kCameraTempHousing && type == kTypeSingle) { *static_cast<float*>(data) = 308.15f; return kSuccess; }
if (what == param::kNucInterval && type == kTypeInt32) { *static_cast<int32_t*>(data) = g->nuc_interval; return kSuccess; }
return kErrNotSupported;
}
Ret fSetParam(Handle, uint32_t what, void* data, uint32_t type) {
std::lock_guard<std::mutex> l(g->m);
if (what == param::kCalibActive && type == kTypeIdxInt32) {
g->cal = static_cast<IdxInt32*>(data)->value;
++g->calib_sets;
return kSuccess;
}
if (what == param::kFramerateHz && type == kTypeSingle) { g->rate = *static_cast<float*>(data); return kSuccess; }
if (what == param::kPollingEnable) return kSuccess;
if (what == param::kNucInterval && type == kTypeInt32) {
g->nuc_interval = *static_cast<int32_t*>(data);
return kSuccess;
}
if (what == param::kNucPerform && type == kType2Int32) {
// As the camera: the call returns when the NUC is done.
g->nuc_mode = static_cast<Int2*>(data)->value1;
++g->nucs;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
return kSuccess;
}
return kErrNotSupported;
}
// A memory object is a copy of the newest frame.
struct Mem {
std::vector<uint16_t> px;
uint32_t w, h;
int64_t num;
int cal;
};
Ret fGetData(Handle, int32_t what, Handle* mem) {
std::lock_guard<std::mutex> l(g->m);
if (what != kMemIrDigitFrame || !g->grabbing || g->px.empty() || g->frozen) return g->no_frame;
*mem = reinterpret_cast<Handle>(new Mem{g->px, g->w, g->h, g->image_num, g->cal});
return kSuccess;
}
Ret fMemFree(Handle* mem) { delete reinterpret_cast<Mem*>(*mem); *mem = 0; return kSuccess; }
Ret fDim(Handle mem, uint32_t* w, uint32_t* h, uint32_t* t) {
auto* m = reinterpret_cast<Mem*>(mem);
*w = m->w; *h = m->h; *t = 0;
return kSuccess;
}
Ret fPtr(Handle mem, Handle* p, int32_t* bytes) {
auto* m = reinterpret_cast<Mem*>(mem);
*p = reinterpret_cast<Handle>(m->px.data());
*bytes = static_cast<int32_t>(m->px.size() * 2);
return kSuccess;
}
Ret fInfo(Handle mem, void* info) {
// As the TarisIR mini: no frame counter, but each frame has its own time.
auto* mi = static_cast<MemInfo*>(info);
mi->image_num = 0;
mi->timestamp_ms = 200.0 * static_cast<double>(reinterpret_cast<Mem*>(mem)->num);
mi->missed_packets = 1;
return kSuccess;
}
Ret fLut(Handle mem, void* lut, float, float, float, float) {
const double kpc = reinterpret_cast<Mem*>(mem)->cal == 1 ? 0.02 : 0.01;
auto* l = static_cast<float*>(lut);
{
std::lock_guard<std::mutex> lk(g->m);
if (++g->lut_calls <= 2) { // still empty, and "ok", as on the tower
for (int i = 0; i < 65536; ++i) l[i] = 0.f;
return kSuccess;
}
}
for (int i = 0; i < 65536; ++i) l[i] = static_cast<float>(i * kpc);
return kSuccess;
}
std::shared_ptr<const Api> fakeApi() {
auto a = std::make_shared<Api>();
a->dll_init = fInit; a->dll_uninit = fUninit; a->dll_version = fVersion;
a->dll_devicetypenames = fTypes; a->dev_create = fCreate; a->dev_free = fFree;
a->dev_search = fSearch; a->dev_connect = fConnect; a->dev_disconnect = fDisconnect;
a->dev_startgrab = fStart; a->dev_stopgrab = fStop; a->dev_getparam = fGetParam;
a->dev_setparam = fSetParam; a->dev_getdata = fGetData; a->mem_free = fMemFree;
a->mem_getdimension = fDim; a->mem_getdataptr = fPtr; a->mem_getinfo = fInfo;
a->mem_getdvlut = fLut;
return a;
}
TarisCameraSource::Options fastOptions() {
TarisCameraSource::Options o;
o.serial = "2017744";
o.frame_rate_hz = 50; // polls every 10 ms
o.retry_min_ms = 20;
o.retry_max_ms = 40;
o.stall_ms = 2000;
return o;
}
// Each call of pred() counts: trigger() is not idempotent.
template <class F> bool waitFor(F pred, int ms = 3000) {
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
for (;;) {
if (pred()) return true;
if (std::chrono::steady_clock::now() >= end) return false;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
struct Harness {
FakeSdk sdk;
TarisCameraSource cam;
std::mutex m;
std::vector<Frame> frames;
explicit Harness(TarisCameraSource::Options o = fastOptions(), bool net_raw = true)
: cam((g = &sdk, o), fakeApi(), [net_raw] { return net_raw; }) {
cam.setFrameCallback([this](const Frame& f) {
std::lock_guard<std::mutex> l(m);
frames.push_back(f);
});
}
~Harness() { cam.close(); g = nullptr; }
bool online() { return waitFor([this] { return cam.offline(0).empty(); }); }
// Trigger until a frame comes or the time is up.
bool shot(int ms = 2000) { return waitFor([this] { return cam.trigger(); }, ms); }
};
} // namespace
TEST_CASE("the thermal camera connects, trying adapters until one answers") {
Harness t;
t.sdk.accept = "#3"; // the adapter index has moved since the search
t.cam.open();
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");
CHECK(t.sdk.connect_attempts.back().substr(t.sdk.connect_attempts.back().size() - 2) == "#3");
CHECK(t.sdk.rate == doctest::Approx(50.f));
CHECK(t.sdk.grabbing);
CHECK(t.sdk.calib_sets == 0); // already on the wanted profile
}
TEST_CASE("a fresh frame is delivered once, as counts with their scale") {
Harness t;
t.cam.open();
t.cam.start();
t.sdk.push(29315, 57315);
REQUIRE(t.shot());
CHECK_FALSE(t.cam.trigger()); // the same frame is not handed out twice
REQUIRE(t.frames.size() == 1);
const Frame& f = t.frames[0];
CHECK(f.layout == PixelLayout::Mono16);
CHECK(f.width == 8);
CHECK(f.height == 4);
REQUIRE(f.data.size() == 64);
uint16_t px5 = 0;
std::memcpy(&px5, f.data.data() + 10, 2);
CHECK(px5 == 57315);
REQUIRE(f.thermal);
CHECK(f.thermal->cal == 0);
CHECK(f.thermal->kelvin_per_count == doctest::Approx(0.01)); // documented, then measured
CHECK(f.thermal->missed_packets == 1);
t.sdk.push(29400);
CHECK(t.shot());
double v = 0;
CHECK(t.cam.readFeature(0, "CalibProfile", v));
CHECK(v == 0);
CHECK(waitFor([&] { return t.cam.readFeature(0, "HousingTemperature", v); }, 7000));
CHECK(v == doctest::Approx(35.0));
}
TEST_CASE("the zero frames after starting are never delivered") {
Harness t;
t.cam.open();
t.cam.start();
t.sdk.push(0);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
CHECK_FALSE(t.cam.trigger());
t.sdk.push(29315);
CHECK(t.shot());
}
TEST_CASE("a frame older than max_frame_age_ms is not kept") {
auto o = fastOptions();
o.max_frame_age_ms = 50;
Harness t(o);
t.cam.open();
t.cam.start();
t.sdk.push(29315);
REQUIRE(t.online());
t.sdk.frozen = true; // no newer frame arrives
std::this_thread::sleep_for(std::chrono::milliseconds(150));
CHECK_FALSE(t.cam.trigger());
}
TEST_CASE("delivery stops while stopped, the camera keeps grabbing") {
Harness t;
t.cam.open();
t.sdk.push(29315);
REQUIRE(t.online());
CHECK_FALSE(t.cam.trigger()); // not started
t.cam.start();
CHECK(t.cam.trigger());
t.cam.stop();
t.sdk.push(29400);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
CHECK_FALSE(t.cam.trigger());
std::lock_guard<std::mutex> l(t.sdk.m);
CHECK(t.sdk.grabbing);
}
TEST_CASE("the configured calibration profile is set on connect") {
auto o = fastOptions();
o.calib_profile = 1;
Harness t(o);
t.cam.open();
t.cam.start();
t.sdk.push(14658);
REQUIRE(t.shot());
CHECK(t.sdk.calib_sets == 1);
REQUIRE(t.frames[0].thermal);
CHECK(t.frames[0].thermal->cal == 1);
CHECK(t.frames[0].thermal->kelvin_per_count == doctest::Approx(0.02));
}
TEST_CASE("switching the profile while running changes the scale of later frames") {
Harness t;
t.cam.open();
t.cam.start();
t.sdk.push(29315);
REQUIRE(t.shot());
REQUIRE(t.cam.setFeature(0, "CalibProfile", "1"));
CHECK_FALSE(t.cam.setFeature(0, "CalibProfile", "7")); // the camera has two
CHECK_FALSE(t.cam.setFeature(0, "CalibProfile", "x"));
t.sdk.push(14658);
REQUIRE(t.shot());
REQUIRE(t.frames.back().thermal);
CHECK(t.frames.back().thermal->cal == 1);
CHECK(t.frames.back().thermal->kelvin_per_count == doctest::Approx(0.02));
}
TEST_CASE("a camera not on the network is offline, says why, and is found later") {
Harness t;
t.sdk.search = {"SN:999|00:00:00:00:00:01|192.168.0.201#0"};
t.cam.open();
t.cam.start();
t.sdk.push(29315);
CHECK(waitFor([&] { return t.cam.offline(0).find("not found") != std::string::npos; }));
CHECK_FALSE(t.cam.trigger());
{
std::lock_guard<std::mutex> l(t.sdk.m);
t.sdk.search = {"SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0"};
}
CHECK(t.online());
CHECK(t.shot());
}
TEST_CASE("without CAP_NET_RAW the camera stays offline and the SDK is not touched") {
Harness t(fastOptions(), false);
t.cam.open();
CHECK(waitFor([&] { return t.cam.offline(0).find("cap_net_raw") != std::string::npos; }));
CHECK(t.sdk.inits == 0);
CHECK_FALSE(t.cam.trigger());
}
TEST_CASE("a camera that goes quiet is reconnected") {
auto o = fastOptions();
o.stall_ms = 100;
Harness t(o);
t.cam.open();
t.cam.start();
t.sdk.push(29315);
REQUIRE(t.online());
t.sdk.frozen = true;
CHECK(waitFor([&] { return t.cam.deviceInfo()[0].restarts >= 1; }));
t.sdk.frozen = false;
t.sdk.push(29400);
CHECK(t.shot());
std::lock_guard<std::mutex> l(t.sdk.m);
CHECK(t.sdk.connects >= 2);
}
TEST_CASE("the program takes over the camera's NUC timer and hands it back") {
{
Harness t;
t.cam.open();
t.sdk.push(29315);
REQUIRE(t.online());
CHECK(t.sdk.nuc_interval == 0); // the camera's own timer is off
t.cam.close();
CHECK(t.sdk.nuc_interval == 30); // and back as found
}
{
auto o = fastOptions();
o.nuc_interval_ms = 0; // leave it to the camera
Harness t(o);
t.cam.open();
t.sdk.push(29315);
REQUIRE(t.online());
t.cam.moving();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
CHECK(t.sdk.nuc_interval == 30);
CHECK(t.sdk.nucs == 0);
}
}
TEST_CASE("a move runs a due NUC, and no more often than the interval") {
auto o = fastOptions();
o.nuc_interval_ms = 400;
Harness t(o);
t.cam.open();
t.cam.start();
t.sdk.push(29315);
REQUIRE(t.online());
CHECK(t.sdk.nucs == 0);
t.cam.moving(); // the first move after connecting runs one
CHECK(waitFor([&] { return t.sdk.nucs == 1; }, 1000));
CHECK(t.sdk.nuc_mode == kNucMode1Point);
t.cam.moving(); // not due yet
std::this_thread::sleep_for(std::chrono::milliseconds(150));
CHECK(t.sdk.nucs == 1);
std::this_thread::sleep_for(std::chrono::milliseconds(300));
t.cam.moving(); // due again
CHECK(waitFor([&] { return t.sdk.nucs == 2; }, 1000));
}
TEST_CASE("an overdue NUC runs without a move") {
auto o = fastOptions();
o.nuc_interval_ms = 100;
Harness t(o);
t.cam.open();
t.sdk.push(29315);
REQUIRE(t.online());
// Twice the interval with no move at all (manual mode, say).
CHECK(waitFor([&] { return t.sdk.nucs >= 1; }, 1500));
}
TEST_CASE("after a NUC only a frame from after it is taken") {
auto o = fastOptions();
o.nuc_interval_ms = 400;
Harness t(o);
t.cam.open();
t.cam.start();
t.sdk.push(29315);
REQUIRE(t.online());
CHECK(t.cam.frameReady());
t.sdk.frozen = true; // nothing new comes during and after the NUC
t.cam.moving();
REQUIRE(waitFor([&] { return t.sdk.nucs == 1; }, 1000));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
CHECK_FALSE(t.cam.frameReady()); // the frame from before the NUC is stale
CHECK_FALSE(t.cam.trigger());
t.sdk.frozen = false;
t.sdk.push(29400);
CHECK(waitFor([&] { return t.cam.frameReady(); }, 1000));
CHECK(t.shot());
}
TEST_CASE("a missing SDK library leaves the camera offline, not the program down") {
auto o = fastOptions();
o.library = "libdoes_not_exist_irbgrab.so";
TarisCameraSource cam(o, nullptr, [] { return true; });
cam.open();
CHECK(waitFor([&] { return cam.offline(0).find("cannot load") != std::string::npos; }));
CHECK_FALSE(cam.trigger());
cam.close();
}
TEST_CASE("the documented scales") {
CHECK(TarisCameraSource::expectedKelvinPerCount(0) == 0.01);
CHECK(TarisCameraSource::expectedKelvinPerCount(1) == 0.02);
CHECK(TarisCameraSource::expectedKelvinPerCount(2) == 0.0);
}

View File

@ -4,6 +4,7 @@
#include "fgc/HardwareChecks.h" #include "fgc/HardwareChecks.h"
#include <algorithm> #include <algorithm>
#include "fgc/CompositeCameraSource.h"
#include "fgc/mock/MockCameraSource.h" #include "fgc/mock/MockCameraSource.h"
#include "fgc/mock/MockImuSource.h" #include "fgc/mock/MockImuSource.h"
#include "fgc/mock/MockMotorController.h" #include "fgc/mock/MockMotorController.h"
@ -203,6 +204,32 @@ TEST_CASE("a healthy camera passes frames, forced dark and forced bright, and is
CHECK(st.pct_high < 1.0); CHECK(st.pct_high < 1.0);
} }
TEST_CASE("with a thermal camera beside it, both deliver frames and only colour is forced") {
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::make_unique<MockCameraSource>());
v.push_back(std::make_unique<MockCameraSource>(1, 640, 480, PixelLayout::Mono16));
CompositeCameraSource cam(std::move(v));
MockMotorController motor;
motor.start();
HardwareTestContext hw;
hw.camera = &cam;
hw.configured_cameras = {"rgb", "tir"};
hw.frame_timeout_ms = {200, 200};
TestRunner tr(motor, nullptr, unitGeometry(), T_CAM_ALL, fastProfile(), ".", hw);
auto rep = runToCompletion(tr);
REQUIRE(rep);
// 16-bit counts are two bytes a pixel, and that is a whole frame.
CHECK(section(*rep, "cam_frames")->pass);
// No exposure to force on a thermal camera: noted, not failed.
const TestSection* dark = section(*rep, "cam_dark");
REQUIRE(dark);
CHECK(dark->pass);
CHECK(std::any_of(dark->notes.begin(), dark->notes.end(), [](const std::string& n) {
return n.find("tir: not applicable") != std::string::npos;
}));
CHECK(rep->all_pass);
}
TEST_CASE("a camera that delivers nothing fails the frames leaf") { TEST_CASE("a camera that delivers nothing fails the frames leaf") {
MockCameraSource cam; MockCameraSource cam;
cam.setFault(MockCameraSource::Fault::NoFrames); cam.setFault(MockCameraSource::Fault::NoFrames);

486
tests/test_thermalguard.cpp Normal file
View File

@ -0,0 +1,486 @@
#include <doctest/doctest.h>
#include "fgc/ChannelTopics.h"
#include "fgc/Config.h"
#include "fgc/HostMetrics.h"
#include "fgc/OperatingMode.h"
#include "fgc/ThermalGuard.h"
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <unistd.h>
using namespace fgc;
namespace fs = std::filesystem;
namespace {
// A host at `c` degrees, 2.8 of 3.6 GHz, 30% busy, counters at `count`.
HostReading at(long long t_s, double c, long long count = 0) {
HostReading r;
r.now_ms = t_s * 1000;
r.temp_ok = true;
r.cpu_c = c;
r.zone = "x86_pkg_temp";
r.clock_ok = true;
r.mhz = 2800;
r.hw_max_mhz = 3600;
r.count_ok = true;
r.throttle_count = count;
r.busy_pct = 30;
return r;
}
bool logged(const std::vector<GuardLog>& log, const std::string& part) {
for (const auto& l : log)
if (l.second.find(part) != std::string::npos) return true;
return false;
}
// Feed a reading every 2 s from t0 to t1 (inclusive), collecting the log.
std::vector<GuardLog> run(ThermalGuard& g, long long t0, long long t1, double c,
long long count = 0) {
std::vector<GuardLog> all;
for (long long t = t0; t <= t1; t += 2) {
auto l = g.update(at(t, c, count));
all.insert(all.end(), l.begin(), l.end());
}
return all;
}
struct FakeSys {
fs::path root;
FakeSys() {
root = fs::temp_directory_path() / ("fgc_fakesys_" + std::to_string(::getpid()));
fs::remove_all(root);
}
~FakeSys() { fs::remove_all(root); }
void put(const std::string& rel, const std::string& text) {
fs::path p = root / rel;
fs::create_directories(p.parent_path());
std::ofstream(p) << text;
}
};
} // namespace
TEST_CASE("a cool, idle CPU stays at the normal pace") {
ThermalGuard g;
auto log = run(g, 0, 600, 60.0);
CHECK(g.state() == GuardState::Normal);
CHECK(g.intervalScale() == doctest::Approx(1.0));
CHECK_FALSE(g.pauseCapture());
CHECK(log.empty());
}
TEST_CASE("a hot CPU slows the scan, and the slow-down lifts only after it has been calm") {
ThermalParams p;
ThermalGuard g(p);
run(g, 0, 10, 70.0);
auto log = g.update(at(12, 86.0));
CHECK(g.state() == GuardState::Slowed);
CHECK(g.intervalScale() == doctest::Approx(p.slow_factor));
CHECK(logged(log, "CPU at 86 \xC2\xB0""C"));
CHECK(logged(log, "2x further apart"));
// Below slow_c but inside the hysteresis band: stays slowed.
run(g, 14, 400, 82.0);
CHECK(g.state() == GuardState::Slowed);
// Below slow_c - hysteresis, but not yet for calm_s.
run(g, 402, 402 + p.calm_s - 4, 78.0);
CHECK(g.state() == GuardState::Slowed);
// A hot blip restarts the calm clock.
g.update(at(402 + p.calm_s - 2, 81.0));
run(g, 402 + p.calm_s, 402 + 2 * p.calm_s - 2, 78.0);
CHECK(g.state() == GuardState::Slowed);
log = run(g, 402 + 2 * p.calm_s, 402 + 2 * p.calm_s + 4, 78.0);
CHECK(g.state() == GuardState::Normal);
CHECK(logged(log, "back to the normal picture pace"));
}
TEST_CASE("a CPU that stays very hot pauses pictures, and resumes once cool and the pause is served") {
ThermalParams p;
ThermalGuard g(p);
// One hot sample is not enough: pause_after_s of it is.
g.update(at(0, 93.0));
CHECK(g.state() == GuardState::Slowed);
auto log = run(g, 2, p.pause_after_s, 93.0);
CHECK(g.state() == GuardState::Paused);
CHECK(g.pauseCapture());
CHECK(logged(log, "pausing pictures"));
// Cooled quickly, but the pause runs its minimum.
const long long paused_at = p.pause_after_s;
run(g, paused_at + 2, paused_at + p.min_pause_s - 2, 70.0);
CHECK(g.state() == GuardState::Paused);
log = run(g, paused_at + p.min_pause_s, paused_at + p.min_pause_s + 2, 70.0);
CHECK(g.state() == GuardState::Slowed); // resumes slowed, not straight to full pace
CHECK(logged(log, "pictures resume"));
}
TEST_CASE("a pause lasts while the CPU is above resume_c, or saving has not caught up") {
ThermalParams p;
ThermalGuard g(p);
run(g, 0, p.pause_after_s + 2, 95.0);
REQUIRE(g.state() == GuardState::Paused);
run(g, 30, 30 + 2 * p.min_pause_s, p.resume_c + 3);
CHECK(g.state() == GuardState::Paused); // served its time, still too warm
HostReading r = at(1000, 70.0);
r.save_pending = 1;
g.update(r);
CHECK(g.state() == GuardState::Paused); // cool, but a frame is still saving
g.update(at(1002, 70.0));
CHECK(g.state() == GuardState::Slowed);
}
TEST_CASE("saving that keeps the scan waiting slows it, then pauses it") {
ThermalParams p;
ThermalGuard g(p);
auto held = [](long long t) {
HostReading r = at(t, 60.0);
r.held_for_save = true;
r.save_pending = 4;
return r;
};
// Brief holds are normal.
for (long long t = 0; t < p.held_slow_s; t += 2) g.update(held(t));
CHECK(g.state() == GuardState::Normal);
g.update(at(p.held_slow_s, 60.0)); // caught up: the clock restarts
for (long long t = p.held_slow_s + 2; t < 2 * p.held_slow_s; t += 2) g.update(held(t));
CHECK(g.state() == GuardState::Normal);
std::vector<GuardLog> log;
long long t = 100;
for (; t <= 100 + p.held_slow_s; t += 2) {
auto l = g.update(held(t));
log.insert(log.end(), l.begin(), l.end());
}
CHECK(g.state() == GuardState::Slowed);
CHECK(logged(log, "Saving has kept the scan waiting"));
for (; t <= 100 + p.held_pause_s; t += 2) g.update(held(t));
CHECK(g.state() == GuardState::Paused);
}
// A saver too slow for the interval: the scan waits `wait_s` out of every
// `cycle_s`, never long in one go - the overload the desk run with a CPU quota
// showed (save 4.6 s against a 2 s interval). Feeds samples every 2 s from t0
// to t1, keeping the running waiting total in `held`.
std::vector<GuardLog> runSlowSaver(ThermalGuard& g, long long t0, long long t1, double wait_frac,
long long& held, double save_ms = 4600.0) {
std::vector<GuardLog> all;
for (long long t = t0; t <= t1; t += 2) {
HostReading r = at(t, 60.0);
held += static_cast<long long>(2000 * wait_frac);
r.held_total_ms = held;
r.held_for_save = (t / 2) % 2 == 0 && wait_frac > 0.0; // short waits, on and off
r.save_pending = 2;
r.base_interval_ms = 2000;
r.save_ms_mean = save_ms;
r.save_workers = 1;
auto l = g.update(r);
all.insert(all.end(), l.begin(), l.end());
}
return all;
}
TEST_CASE("a saver that holds the scan briefly but often slows it, to a pace saving can keep") {
ThermalParams p;
ThermalGuard g(p);
long long held = 0;
// 10% of the time waiting: a saver with headroom; nothing to do.
runSlowSaver(g, 0, 120, 0.10, held);
CHECK(g.state() == GuardState::Normal);
// 50%: the bottleneck. Judged on half a minute at least, then slowed.
auto log = runSlowSaver(g, 122, 200, 0.50, held);
CHECK(g.state() == GuardState::Slowed);
CHECK(logged(log, "of the last minute"));
// The pace follows what saving takes: 1.5 x 4.6 s = 6.9 s, not 2 x 2 s.
CHECK(g.intervalScale() == doctest::Approx(1.5 * 4600.0 / 2000.0));
CHECK(logged(log, "one picture every 6.9 s instead of 2.0"));
}
TEST_CASE("slowed, but saving still the bottleneck: paused; with headroom: back to normal") {
ThermalParams p;
{
ThermalGuard g(p);
long long held = 0;
long long s = 0; // when it slowed
while (g.state() == GuardState::Normal && s < 120) {
s += 2;
runSlowSaver(g, s, s, 0.5, held);
}
REQUIRE(g.state() == GuardState::Slowed);
// Judged only once a minute of slowed pictures is in the window, then
// held_pause_s of it.
runSlowSaver(g, s + 2, s + 60 + p.held_pause_s - 4, 0.5, held);
CHECK(g.state() == GuardState::Slowed);
auto log = runSlowSaver(g, s + 60 + p.held_pause_s - 2, s + 60 + p.held_pause_s + 4,
0.5, held);
CHECK(g.state() == GuardState::Paused);
CHECK(logged(log, "even slowed"));
}
{
ThermalGuard g(p);
long long held = 0;
runSlowSaver(g, 0, 60, 0.5, held);
REQUIRE(g.state() == GuardState::Slowed);
// The slower pace gave saving its headroom: no waiting at all. But a
// save still 4.6 s long does not fit the 2 s pace, so going back would
// only overload again: it stays slowed, at a pace it can hold, no flapping.
runSlowSaver(g, 62, 62 + 60 + p.calm_s, 0.0, held);
CHECK(g.state() == GuardState::Slowed);
// Saving fast again (the CPU cooled, the load went): back to normal.
long long t = 62 + 60 + p.calm_s + 2;
runSlowSaver(g, t, t + p.calm_s + 4, 0.0, held, 1000.0);
CHECK(g.state() == GuardState::Normal);
}
}
TEST_CASE("the slowed pace never goes past 10x, and remembers the save time through a pause") {
ThermalGuard g;
long long held = 0;
runSlowSaver(g, 0, 60, 0.5, held, 60000.0); // a minute per picture
REQUIRE(g.state() == GuardState::Slowed);
CHECK(g.intervalScale() == doctest::Approx(10.0));
}
TEST_CASE("missing readings are left out of the rules, not taken as zero or as hot") {
ThermalGuard g;
for (long long t = 0; t < 600; t += 2) {
HostReading r;
r.now_ms = t * 1000;
auto log = g.update(r);
CHECK(log.empty());
}
CHECK(g.state() == GuardState::Normal);
}
TEST_CASE("thermal off keeps full pace; thermal resume clears a pause") {
ThermalParams p;
ThermalGuard g(p);
run(g, 0, p.pause_after_s + 2, 95.0);
REQUIRE(g.state() == GuardState::Paused);
auto log = g.forceResume(30000);
CHECK(g.state() == GuardState::Normal);
CHECK_FALSE(log.empty());
// Still hot: the rules apply afresh - slowed at once, paused after pause_after_s.
g.update(at(32, 95.0));
CHECK(g.state() == GuardState::Slowed);
run(g, 34, 32 + p.pause_after_s, 95.0);
CHECK(g.state() == GuardState::Paused);
g.setEnabled(false, 100000);
CHECK_FALSE(g.enabled());
CHECK(g.state() == GuardState::Normal);
run(g, 100, 1000, 99.0);
CHECK(g.state() == GuardState::Normal);
g.setEnabled(true, 1002000);
g.update(at(1004, 99.0));
CHECK(g.state() == GuardState::Slowed);
}
TEST_CASE("throttling is logged when it starts, summarised once a minute, and when it ends") {
ThermalGuard g;
// Counters still: nothing.
auto log = run(g, 0, 10, 80.0, 100);
CHECK_FALSE(g.throttle().throttling());
CHECK(log.empty());
// The counter rises: one start line, with the temperature and clock.
log = g.update(at(12, 90.0, 103));
CHECK(g.throttle().throttling());
CHECK(logged(log, "CPU is slowing itself to stay cool: 90 \xC2\xB0""C, running at 2.8 GHz of 3.6 GHz"));
CHECK(g.state() == GuardState::Slowed);
// Rising every sample for 70 s: exactly one summary.
int summaries = 0;
long long count = 103;
for (long long t = 14; t <= 82; t += 2) {
count += 2;
for (const auto& l : g.update(at(t, 90.0, count)))
if (l.second.find("CPU still slowed") != std::string::npos) ++summaries;
}
CHECK(summaries == 1);
CHECK(g.throttle().eventsLastMinute() > 0);
// Counter stops: the end line comes after a quiet minute, not before.
log = run(g, 84, 140, 80.0, count);
CHECK(g.throttle().throttling());
CHECK_FALSE(logged(log, "no longer slowed"));
log = run(g, 142, 146, 80.0, count);
CHECK_FALSE(g.throttle().throttling());
CHECK(logged(log, "CPU no longer slowed: 80 \xC2\xB0""C"));
}
TEST_CASE("throttling is inferred from a low clock while busy, as a capped or power-limited CPU shows") {
ThermalGuard g;
auto slow = [](long long t, double busy) {
HostReading r = at(t, 70.0);
r.count_ok = false; // no kernel counters on this host
r.mhz = 800;
r.busy_pct = busy;
return r;
};
// Low clock while idle is just an idle CPU.
for (long long t = 0; t < 20; t += 2) g.update(slow(t, 5));
CHECK_FALSE(g.throttle().throttling());
// One busy sample may be the governor catching up; two are not.
auto log = g.update(slow(20, 90));
CHECK_FALSE(g.throttle().throttling());
log = g.update(slow(22, 90));
CHECK(g.throttle().throttling());
CHECK(g.throttle().cause() == "clock");
CHECK(logged(log, "judged from its clock"));
}
TEST_CASE("a throttle counter that goes backwards is a fresh start, not throttling") {
ThermalGuard g;
g.update(at(0, 70.0, 500));
auto log = g.update(at(2, 70.0, 3));
CHECK_FALSE(g.throttle().throttling());
CHECK(log.empty());
}
TEST_CASE("host readers on a fake sysfs tree") {
FakeSys s;
s.put("sys/class/thermal/thermal_zone0/temp", "48000\n");
s.put("sys/class/thermal/thermal_zone0/type", "acpitz\n");
s.put("sys/class/thermal/thermal_zone1/temp", "91500\n");
s.put("sys/class/thermal/thermal_zone1/type", "x86_pkg_temp\n");
for (int c = 0; c < 4; ++c) {
const std::string cpu = "sys/devices/system/cpu/cpu" + std::to_string(c) + "/";
s.put(cpu + "cpufreq/scaling_cur_freq", std::to_string(1000000 + c * 200000) + "\n");
s.put(cpu + "cpufreq/cpuinfo_max_freq", "3600000\n");
s.put(cpu + "cpufreq/scaling_max_freq", c == 2 ? "1600000\n" : "3600000\n");
s.put(cpu + "thermal_throttle/package_throttle_count", "7\n");
s.put(cpu + "thermal_throttle/core_throttle_count", "1\n");
}
s.put("sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq", "999\n"); // not a core
s.put("proc/stat", "cpu 100 0 50 800 50 0 0 0 0 0\ncpu0 1 2 3 4\n");
const std::string root = s.root.string();
auto th = hostmetrics::readThermal(root);
REQUIRE(th.ok);
CHECK(th.max_temp_c == doctest::Approx(91.5));
CHECK(th.hottest_zone == "x86_pkg_temp");
CHECK(th.cpu_temp_c == doctest::Approx(91.5));
// A Wi-Fi card hotter than the CPU (the towers list iwlwifi_1): the hottest
// zone is the card, but the CPU guard still reads the CPU package.
s.put("sys/class/thermal/thermal_zone2/temp", "97000\n");
s.put("sys/class/thermal/thermal_zone2/type", "iwlwifi_1\n");
th = hostmetrics::readThermal(root);
CHECK(th.hottest_zone == "iwlwifi_1");
CHECK(th.cpu_zone == "x86_pkg_temp");
CHECK(th.cpu_temp_c == doctest::Approx(91.5));
auto f = hostmetrics::readCpuFreq(root);
REQUIRE(f.ok);
CHECK(f.core_mhz.size() == 4);
CHECK(f.core_mhz[0] == doctest::Approx(1000.0));
CHECK(f.mean_mhz == doctest::Approx(1300.0));
CHECK(f.hw_max_mhz == doctest::Approx(3600.0));
CHECK(f.cap_mhz == doctest::Approx(1600.0));
auto c = hostmetrics::readThrottleCount(root);
REQUIRE(c.ok);
CHECK(c.events == 7 + 4); // the package once, every core
auto t = hostmetrics::readCpuTimes(root);
REQUIRE(t.ok);
CHECK(t.total == 1000);
CHECK(t.busy == 150);
// A tree without proc/stat reads the real one: busy % stays real in a simulation.
fs::remove(s.root / "proc/stat");
CHECK(hostmetrics::readCpuTimes(root).ok);
// Nothing there: not ok, no throw.
CHECK_FALSE(hostmetrics::readCpuFreq("/nonexistent-fgc").ok);
CHECK_FALSE(hostmetrics::readThrottleCount("/nonexistent-fgc").ok);
CHECK(hostmetrics::readCpuTimes("/nonexistent-fgc").ok); // the real /proc/stat
CHECK_FALSE(hostmetrics::readThermal("/nonexistent-fgc").ok);
}
TEST_CASE("[Thermal] config: defaults, overrides and the order of the limits") {
AppConfig d = ConfigLoader::fromMap({});
CHECK(d.thermal.enabled);
CHECK(d.thermal.sysfs_root == "/");
CHECK(d.thermal.slow_c == doctest::Approx(85.0));
CHECK(d.thermal.pause_c == doctest::Approx(92.0));
CHECK(d.thermal.resume_c == doctest::Approx(75.0));
CHECK_FALSE(d.features.mock_camera_full);
AppConfig c = ConfigLoader::fromMap({{"Thermal.slow_c", "80"},
{"Thermal.pause_c", "88"},
{"Thermal.resume_c", "70"},
{"Thermal.slow_factor", "3"},
{"Thermal.sysfs_root", "/tmp/fake"},
{"Thermal.enabled", "false"},
{"Features.mock_camera_size", "full"}});
CHECK_FALSE(c.thermal.enabled);
CHECK(c.thermal.slow_c == doctest::Approx(80.0));
CHECK(c.thermal.slow_factor == doctest::Approx(3.0));
CHECK(c.thermal.sysfs_root == "/tmp/fake");
CHECK(c.features.mock_camera_full);
CHECK_THROWS(ConfigLoader::fromMap({{"Thermal.slow_c", "95"}})); // above pause_c
CHECK_THROWS(ConfigLoader::fromMap({{"Thermal.resume_c", "90"}})); // above slow_c
CHECK_THROWS(ConfigLoader::fromMap({{"Thermal.slow_factor", "0.5"}}));
CHECK_THROWS(ConfigLoader::fromMap({{"Thermal.held_pause_s", "10"}})); // under held_slow_s
CHECK_THROWS(ConfigLoader::fromMap({{"Thermal.sample_ms", "10"}}));
CHECK_THROWS(ConfigLoader::fromMap({{"Features.mock_camera_size", "huge"}}));
}
TEST_CASE("Health payload: unknown readings are null, numbers rounded") {
HealthReport h;
h.tower = "t121";
h.state = "paused";
h.reason = "CPU at 93 \xC2\xB0""C for 20 s";
h.cpu_c_ok = true;
h.cpu_c = 93.04;
h.cpu_mhz_ok = true;
h.cpu_mhz = 1187.6;
h.throttling = true;
h.throttle_events_last_min = 14;
h.interval_s = 10.0;
h.held_s = 3;
h.timestamp_ms = 1700000000000;
const std::string j = healthJson(h);
CHECK(j.find("\"state\":\"paused\"") != std::string::npos);
CHECK(j.find("\"cpu_c\":93.0") != std::string::npos);
CHECK(j.find("\"cpu_mhz\":1188") != std::string::npos);
CHECK(j.find("\"cpu_busy_pct\":null") != std::string::npos);
CHECK(j.find("\"enclosure_c\":null") != std::string::npos);
CHECK(j.find("\"save_ms_mean\":null") != std::string::npos);
CHECK(j.find("\"throttling\":true") != std::string::npos);
CHECK(j.find("\"throttle_events_last_min\":14") != std::string::npos);
CHECK(j.find("\"interval_s\":10.0") != std::string::npos);
CHECK(j.find("\"time\":1700000000000") != std::string::npos);
CHECK(j.find("\"cams_offline\":\"\"") != std::string::npos); // all cameras delivering
h.cams_offline = "TIR";
CHECK(healthJson(h).find("\"cams_offline\":\"TIR\"") != std::string::npos);
CHECK(j.front() == '{');
CHECK(j.back() == '}');
}
TEST_CASE("the CPU guard's commands work in every mode") {
CHECK(allowedIn(Mode::Auto, "thermal"));
CHECK(allowedIn(Mode::Pan, "thermal resume"));
CHECK(allowedIn(Mode::Auto, "thermal off"));
}
TEST_CASE("busy percent between two /proc/stat reads") {
auto a = hostmetrics::parseProcStatCpu("cpu 100 0 100 700 100 0 0 0\n");
auto b = hostmetrics::parseProcStatCpu("cpu 400 0 100 1000 100 0 0 0\n");
CHECK(hostmetrics::busyPercent(a, b) == doctest::Approx(50.0));
CHECK(hostmetrics::busyPercent(a, a) == doctest::Approx(-1.0));
CHECK(hostmetrics::busyPercent({}, b) == doctest::Approx(-1.0));
CHECK_FALSE(hostmetrics::parseProcStatCpu("intr 1 2 3\n").ok);
}

View File

@ -0,0 +1,92 @@
#include "fgc/ThermalStats.h"
#include <doctest/doctest.h>
#include <vector>
using namespace fgc;
namespace {
// 20.00 degC at the default 0.01 K per count.
constexpr uint16_t kRoom = 29315;
std::vector<uint16_t> flat(uint32_t w, uint32_t h, uint16_t v) {
return std::vector<uint16_t>(static_cast<size_t>(w) * h, v);
}
} // namespace
TEST_CASE("counts convert to tenths of a degree Celsius under either profile") {
CHECK(countsToDeciC(27315, 0.01) == 0); // 273.15 K
CHECK(countsToDeciC(kRoom, 0.01) == 200); // 20.0 degC
CHECK(countsToDeciC(23315, 0.01) == -400); // -40.0 degC, sky
CHECK(countsToDeciC(57315, 0.01) == 3000); // 300.0 degC
// Profile 1 doubles the step: the same temperature is half the count.
CHECK(countsToDeciC(14658, 0.02) == 200); // 293.16 K -> 20.01 rounds to 20.0
CHECK(countsToDeciC(28658, 0.02) == 3000); // 573.16 K
// Rounded to nearest, not truncated: 20.06 degC.
CHECK(countsToDeciC(29321, 0.01) == 201);
CHECK(countsToDeciC(29319, 0.01) == 200); // 20.04 degC
}
TEST_CASE("maximum, median and hot spot of a scene") {
const uint32_t w = 8, h = 4;
auto px = flat(w, h, kRoom);
px[2 * w + 5] = 57315; // 300 degC at (5, 2)
ThermalFrameInfo info;
const auto s = thermalStats(px.data(), w, h, info);
REQUIRE(s.valid);
CHECK(s.valid_pixels == w * h);
CHECK(s.max_count == 57315);
CHECK(s.tmax_decic == 3000);
CHECK(s.median_count == kRoom); // one outlier does not move the median
CHECK(s.tmed_decic == 200);
CHECK(s.hotx == doctest::Approx(5.5 / 8)); // pixel centre, normalised
CHECK(s.hoty == doctest::Approx(2.5 / 4));
}
TEST_CASE("the median is the lower middle value") {
const std::vector<uint16_t> px = {30000, 29000, 31000, 28000}; // sorted 28 29 30 31
const auto s = thermalStats(px.data(), 4, 1, ThermalFrameInfo{});
CHECK(s.median_count == 29000);
}
TEST_CASE("the first occurrence of the maximum is the hot spot") {
auto px = flat(4, 4, kRoom);
px[1 * 4 + 1] = 40000;
px[3 * 4 + 3] = 40000;
const auto s = thermalStats(px.data(), 4, 4, ThermalFrameInfo{});
CHECK(s.hotx == doctest::Approx(1.5 / 4));
CHECK(s.hoty == doctest::Approx(1.5 / 4));
}
TEST_CASE("zero and wrapped counts take no part") {
auto px = flat(4, 2, kRoom);
px[0] = 0; // the camera's start-up frames are zeros
px[1] = 65535; // past the LUT's wrap: not a temperature
px[2] = 65281;
ThermalFrameInfo info;
const auto s = thermalStats(px.data(), 4, 2, info);
REQUIRE(s.valid);
CHECK(s.valid_pixels == 5);
CHECK(s.max_count == kRoom);
// At the limit itself the count still counts.
px[3] = info.valid_count_max;
CHECK(thermalStats(px.data(), 4, 2, info).max_count == info.valid_count_max);
}
TEST_CASE("a frame with no valid pixel has no statistics") {
const auto zeros = flat(4, 4, 0);
CHECK_FALSE(thermalStats(zeros.data(), 4, 4, ThermalFrameInfo{}).valid);
CHECK_FALSE(thermalStats(nullptr, 4, 4, ThermalFrameInfo{}).valid);
CHECK_FALSE(thermalStats(zeros.data(), 0, 4, ThermalFrameInfo{}).valid);
}
TEST_CASE("the scale comes from the frame's profile") {
const auto px = flat(2, 2, 14658); // 20 degC under profile 1
ThermalFrameInfo info;
info.cal = 1;
info.kelvin_per_count = 0.02;
CHECK(thermalStats(px.data(), 2, 2, info).tmed_decic == 200);
}

View File

@ -0,0 +1,62 @@
{"t": 1, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 62.8, "watts": 4.84, "rss_mb": 565, "done": 0, "queue": 0}
{"t": 2, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 65.9, "watts": 4.93, "rss_mb": 534, "done": 0, "queue": 0}
{"t": 3, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 60.1, "watts": 5.00, "rss_mb": 588, "done": 1, "queue": 0}
{"t": 4, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 63.1, "watts": 3.74, "rss_mb": 617, "done": 1, "queue": 0}
{"t": 5, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 57.8, "watts": 4.96, "rss_mb": 473, "done": 2, "queue": 0}
{"t": 6, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.9, "watts": 5.09, "rss_mb": 634, "done": 2, "queue": 0}
{"t": 7, "temp_c": 54.0, "mhz": 1051, "cpu_pct": 63.2, "watts": 5.06, "rss_mb": 398, "done": 2, "queue": 0}
{"t": 8, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.4, "watts": 4.93, "rss_mb": 610, "done": 3, "queue": 0}
{"t": 9, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.9, "watts": 3.77, "rss_mb": 576, "done": 3, "queue": 0}
{"t": 10, "temp_c": 54.0, "mhz": 1000, "cpu_pct": 60.2, "watts": 4.93, "rss_mb": 661, "done": 4, "queue": 0}
{"t": 11, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 69.8, "watts": 5.07, "rss_mb": 680, "done": 4, "queue": 0}
{"t": 12, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 57.1, "watts": 4.76, "rss_mb": 638, "done": 5, "queue": 0}
{"t": 13, "temp_c": 54.0, "mhz": 1000, "cpu_pct": 67.2, "watts": 5.15, "rss_mb": 691, "done": 5, "queue": 0}
{"t": 14, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 61.5, "watts": 4.93, "rss_mb": 562, "done": 6, "queue": 0}
{"t": 15, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.1, "watts": 5.07, "rss_mb": 683, "done": 6, "queue": 0}
{"t": 16, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.2, "watts": 3.80, "rss_mb": 559, "done": 6, "queue": 0}
{"t": 17, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 67.9, "watts": 5.03, "rss_mb": 689, "done": 7, "queue": 0}
{"t": 18, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 61.8, "watts": 5.09, "rss_mb": 690, "done": 7, "queue": 0}
{"t": 19, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 55.8, "watts": 4.77, "rss_mb": 691, "done": 8, "queue": 0}
{"t": 20, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.2, "watts": 5.14, "rss_mb": 693, "done": 8, "queue": 0}
{"t": 21, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 78.1, "watts": 5.65, "rss_mb": 687, "done": 9, "queue": 0}
{"t": 22, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 75.0, "watts": 4.17, "rss_mb": 695, "done": 9, "queue": 0}
{"t": 23, "temp_c": 55.0, "mhz": 1100, "cpu_pct": 60.3, "watts": 5.08, "rss_mb": 589, "done": 10, "queue": 0}
{"t": 24, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 63.9, "watts": 5.08, "rss_mb": 696, "done": 10, "queue": 0}
{"t": 25, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 66.5, "watts": 4.95, "rss_mb": 696, "done": 10, "queue": 0}
{"t": 26, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.6, "watts": 5.07, "rss_mb": 699, "done": 11, "queue": 0}
{"t": 27, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 64.3, "watts": 5.04, "rss_mb": 700, "done": 11, "queue": 0}
{"t": 28, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 61.6, "watts": 3.77, "rss_mb": 702, "done": 12, "queue": 0}
{"t": 29, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 67.6, "watts": 5.10, "rss_mb": 702, "done": 12, "queue": 0}
{"t": 30, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 61.7, "watts": 4.88, "rss_mb": 702, "done": 13, "queue": 0}
{"t": 31, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.6, "watts": 5.06, "rss_mb": 704, "done": 13, "queue": 0}
{"t": 32, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 59.9, "watts": 4.90, "rss_mb": 672, "done": 14, "queue": 0}
{"t": 33, "temp_c": 55.0, "mhz": 1199, "cpu_pct": 66.9, "watts": 5.21, "rss_mb": 704, "done": 14, "queue": 0}
{"t": 34, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 60.1, "watts": 3.73, "rss_mb": 596, "done": 15, "queue": 0}
{"t": 35, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.5, "watts": 5.18, "rss_mb": 705, "done": 15, "queue": 0}
{"t": 36, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 63.3, "watts": 5.06, "rss_mb": 581, "done": 15, "queue": 0}
{"t": 37, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 63.8, "watts": 4.90, "rss_mb": 706, "done": 16, "queue": 0}
{"t": 38, "temp_c": 55.0, "mhz": 1100, "cpu_pct": 64.3, "watts": 5.14, "rss_mb": 706, "done": 16, "queue": 0}
{"t": 39, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 57.3, "watts": 4.79, "rss_mb": 706, "done": 17, "queue": 0}
{"t": 40, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 67.0, "watts": 3.80, "rss_mb": 723, "done": 17, "queue": 0}
{"t": 41, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.7, "watts": 4.89, "rss_mb": 707, "done": 18, "queue": 0}
{"t": 42, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 65.2, "watts": 5.13, "rss_mb": 708, "done": 18, "queue": 0}
{"t": 43, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 57.3, "watts": 4.92, "rss_mb": 676, "done": 19, "queue": 0}
{"t": 44, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 68.9, "watts": 5.00, "rss_mb": 725, "done": 19, "queue": 0}
{"t": 45, "temp_c": 55.0, "mhz": 807, "cpu_pct": 61.1, "watts": 5.04, "rss_mb": 584, "done": 19, "queue": 0}
{"t": 46, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.7, "watts": 5.05, "rss_mb": 725, "done": 20, "queue": 0}
{"t": 47, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.4, "watts": 3.88, "rss_mb": 725, "done": 20, "queue": 0}
{"t": 48, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.2, "watts": 5.00, "rss_mb": 725, "done": 21, "queue": 0}
{"t": 49, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.0, "watts": 5.02, "rss_mb": 726, "done": 21, "queue": 0}
{"t": 50, "temp_c": 55.0, "mhz": 1100, "cpu_pct": 59.2, "watts": 4.99, "rss_mb": 726, "done": 22, "queue": 0}
{"t": 51, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 67.8, "watts": 5.13, "rss_mb": 726, "done": 22, "queue": 0}
{"t": 52, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 75.3, "watts": 5.67, "rss_mb": 678, "done": 23, "queue": 0}
{"t": 53, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 70.9, "watts": 4.11, "rss_mb": 710, "done": 23, "queue": 0}
{"t": 54, "temp_c": 54.0, "mhz": 1000, "cpu_pct": 58.8, "watts": 5.06, "rss_mb": 493, "done": 24, "queue": 0}
{"t": 55, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.4, "watts": 4.97, "rss_mb": 710, "done": 24, "queue": 0}
{"t": 56, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.0, "watts": 5.00, "rss_mb": 711, "done": 24, "queue": 0}
{"t": 57, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 59.4, "watts": 4.99, "rss_mb": 727, "done": 25, "queue": 0}
{"t": 58, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 69.2, "watts": 3.86, "rss_mb": 727, "done": 25, "queue": 0}
{"t": 59, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 57.6, "watts": 5.12, "rss_mb": 711, "done": 26, "queue": 0}
{"t": 60, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 67.6, "watts": 5.12, "rss_mb": 711, "done": 26, "queue": 0}
{"t": 61, "temp_c": 53.0, "mhz": 1000, "cpu_pct": 37.3, "watts": 4.36, "rss_mb": 381, "done": 27, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 0, "secs": 60.7, "frames": 27, "frames_per_s": 0.445, "job_ms_p50": 2219, "latency_ms_p50": 2240, "latency_ms_p95": 2339, "latency_ms_max": 2454, "max_queue": 0, "backlog_at_end": 0, "cpu_s_per_frame": 5.193, "peak_rss_mb": 726, "temp_c_mean": 54.9, "temp_c_max": 57.0, "mhz_mean": 1171, "mhz_min": 807, "cpu_pct_mean": 63.8, "watts_mean": 4.83}

View File

@ -0,0 +1,122 @@
{"t": 1, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.5, "watts": 4.85, "rss_mb": 575, "done": 0, "queue": 0}
{"t": 2, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.7, "watts": 5.08, "rss_mb": 534, "done": 0, "queue": 1}
{"t": 3, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 59.8, "watts": 4.94, "rss_mb": 596, "done": 1, "queue": 0}
{"t": 4, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 64.4, "watts": 3.82, "rss_mb": 620, "done": 1, "queue": 1}
{"t": 5, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 60.0, "watts": 4.95, "rss_mb": 508, "done": 2, "queue": 0}
{"t": 6, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.3, "watts": 4.98, "rss_mb": 636, "done": 2, "queue": 1}
{"t": 7, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 56.9, "watts": 5.00, "rss_mb": 306, "done": 3, "queue": 0}
{"t": 8, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.9, "watts": 5.01, "rss_mb": 614, "done": 3, "queue": 1}
{"t": 9, "temp_c": 56.0, "mhz": 1199, "cpu_pct": 64.2, "watts": 5.06, "rss_mb": 579, "done": 3, "queue": 1}
{"t": 10, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 57.9, "watts": 3.64, "rss_mb": 669, "done": 4, "queue": 1}
{"t": 11, "temp_c": 55.0, "mhz": 1100, "cpu_pct": 67.8, "watts": 5.10, "rss_mb": 689, "done": 4, "queue": 1}
{"t": 12, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 57.9, "watts": 4.81, "rss_mb": 658, "done": 5, "queue": 1}
{"t": 13, "temp_c": 56.0, "mhz": 1000, "cpu_pct": 66.8, "watts": 5.21, "rss_mb": 695, "done": 5, "queue": 1}
{"t": 14, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 58.4, "watts": 4.94, "rss_mb": 597, "done": 6, "queue": 1}
{"t": 15, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.8, "watts": 3.96, "rss_mb": 684, "done": 6, "queue": 1}
{"t": 16, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.3, "watts": 4.93, "rss_mb": 560, "done": 6, "queue": 2}
{"t": 17, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.2, "watts": 5.00, "rss_mb": 686, "done": 7, "queue": 1}
{"t": 18, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 61.3, "watts": 5.11, "rss_mb": 687, "done": 7, "queue": 2}
{"t": 19, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 60.7, "watts": 4.79, "rss_mb": 688, "done": 8, "queue": 1}
{"t": 20, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 71.4, "watts": 5.35, "rss_mb": 689, "done": 8, "queue": 2}
{"t": 21, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 79.3, "watts": 4.40, "rss_mb": 690, "done": 9, "queue": 1}
{"t": 22, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.4, "watts": 5.25, "rss_mb": 692, "done": 9, "queue": 2}
{"t": 23, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 57.9, "watts": 4.88, "rss_mb": 660, "done": 10, "queue": 1}
{"t": 24, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 69.7, "watts": 5.01, "rss_mb": 694, "done": 10, "queue": 2}
{"t": 25, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 58.2, "watts": 4.92, "rss_mb": 525, "done": 11, "queue": 1}
{"t": 26, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.8, "watts": 5.15, "rss_mb": 695, "done": 11, "queue": 2}
{"t": 27, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 66.9, "watts": 3.85, "rss_mb": 696, "done": 11, "queue": 2}
{"t": 28, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 61.9, "watts": 4.79, "rss_mb": 700, "done": 12, "queue": 2}
{"t": 29, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.1, "watts": 5.16, "rss_mb": 701, "done": 12, "queue": 2}
{"t": 30, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 56.0, "watts": 4.96, "rss_mb": 702, "done": 13, "queue": 2}
{"t": 31, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.3, "watts": 5.08, "rss_mb": 703, "done": 13, "queue": 2}
{"t": 32, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 63.2, "watts": 4.99, "rss_mb": 702, "done": 14, "queue": 2}
{"t": 33, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 64.2, "watts": 3.80, "rss_mb": 707, "done": 14, "queue": 2}
{"t": 34, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 60.1, "watts": 4.93, "rss_mb": 631, "done": 15, "queue": 2}
{"t": 35, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.8, "watts": 5.18, "rss_mb": 708, "done": 15, "queue": 2}
{"t": 36, "temp_c": 54.0, "mhz": 950, "cpu_pct": 62.8, "watts": 5.06, "rss_mb": 583, "done": 15, "queue": 3}
{"t": 37, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 67.2, "watts": 5.09, "rss_mb": 708, "done": 16, "queue": 2}
{"t": 38, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 62.0, "watts": 3.68, "rss_mb": 709, "done": 16, "queue": 3}
{"t": 39, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 60.6, "watts": 5.03, "rss_mb": 709, "done": 17, "queue": 2}
{"t": 40, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.5, "watts": 5.07, "rss_mb": 725, "done": 17, "queue": 3}
{"t": 41, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 63.4, "watts": 4.99, "rss_mb": 709, "done": 18, "queue": 2}
{"t": 42, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 66.7, "watts": 5.09, "rss_mb": 710, "done": 18, "queue": 3}
{"t": 43, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 57.4, "watts": 4.97, "rss_mb": 678, "done": 19, "queue": 2}
{"t": 44, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 69.1, "watts": 3.78, "rss_mb": 726, "done": 19, "queue": 3}
{"t": 45, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 58.0, "watts": 5.05, "rss_mb": 559, "done": 20, "queue": 2}
{"t": 46, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 64.8, "watts": 5.13, "rss_mb": 726, "done": 20, "queue": 3}
{"t": 47, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 67.9, "watts": 5.03, "rss_mb": 727, "done": 20, "queue": 3}
{"t": 48, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 61.2, "watts": 4.89, "rss_mb": 727, "done": 21, "queue": 3}
{"t": 49, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 66.8, "watts": 5.16, "rss_mb": 727, "done": 21, "queue": 3}
{"t": 50, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 58.1, "watts": 3.67, "rss_mb": 727, "done": 22, "queue": 3}
{"t": 51, "temp_c": 58.0, "mhz": 1200, "cpu_pct": 71.7, "watts": 5.04, "rss_mb": 728, "done": 22, "queue": 3}
{"t": 52, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 75.7, "watts": 5.95, "rss_mb": 674, "done": 23, "queue": 3}
{"t": 53, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 70.6, "watts": 5.31, "rss_mb": 712, "done": 23, "queue": 3}
{"t": 54, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 59.9, "watts": 5.00, "rss_mb": 507, "done": 24, "queue": 3}
{"t": 55, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 65.7, "watts": 5.08, "rss_mb": 714, "done": 24, "queue": 3}
{"t": 56, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 67.2, "watts": 3.92, "rss_mb": 714, "done": 24, "queue": 4}
{"t": 57, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 61.8, "watts": 4.74, "rss_mb": 714, "done": 25, "queue": 3}
{"t": 58, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.8, "watts": 5.08, "rss_mb": 715, "done": 25, "queue": 4}
{"t": 59, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 57.9, "watts": 5.10, "rss_mb": 715, "done": 26, "queue": 3}
{"t": 60, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 69.2, "watts": 5.07, "rss_mb": 715, "done": 26, "queue": 4}
{"t": 61, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 61.2, "watts": 4.91, "rss_mb": 715, "done": 27, "queue": 3}
{"t": 62, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.1, "watts": 3.89, "rss_mb": 716, "done": 27, "queue": 4}
{"t": 63, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 58.5, "watts": 4.99, "rss_mb": 684, "done": 28, "queue": 3}
{"t": 64, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 71.8, "watts": 5.09, "rss_mb": 716, "done": 28, "queue": 4}
{"t": 65, "temp_c": 55.0, "mhz": 800, "cpu_pct": 57.5, "watts": 5.08, "rss_mb": 515, "done": 28, "queue": 4}
{"t": 66, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.3, "watts": 5.03, "rss_mb": 732, "done": 29, "queue": 4}
{"t": 67, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 64.4, "watts": 5.04, "rss_mb": 732, "done": 29, "queue": 4}
{"t": 68, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 56.4, "watts": 3.68, "rss_mb": 716, "done": 30, "queue": 4}
{"t": 69, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.7, "watts": 5.03, "rss_mb": 716, "done": 30, "queue": 4}
{"t": 70, "temp_c": 57.0, "mhz": 1100, "cpu_pct": 61.1, "watts": 4.92, "rss_mb": 716, "done": 31, "queue": 4}
{"t": 71, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.2, "watts": 5.00, "rss_mb": 716, "done": 31, "queue": 4}
{"t": 72, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 58.0, "watts": 4.87, "rss_mb": 684, "done": 32, "queue": 4}
{"t": 73, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 70.5, "watts": 5.20, "rss_mb": 717, "done": 32, "queue": 4}
{"t": 74, "temp_c": 56.0, "mhz": 1199, "cpu_pct": 61.2, "watts": 5.01, "rss_mb": 601, "done": 33, "queue": 4}
{"t": 75, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 63.9, "watts": 3.93, "rss_mb": 717, "done": 33, "queue": 4}
{"t": 76, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.2, "watts": 4.96, "rss_mb": 652, "done": 33, "queue": 5}
{"t": 77, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.0, "watts": 4.99, "rss_mb": 717, "done": 34, "queue": 4}
{"t": 78, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 63.2, "watts": 5.08, "rss_mb": 717, "done": 34, "queue": 5}
{"t": 79, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 58.4, "watts": 4.77, "rss_mb": 717, "done": 35, "queue": 4}
{"t": 80, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.5, "watts": 5.16, "rss_mb": 717, "done": 35, "queue": 5}
{"t": 81, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 63.1, "watts": 3.64, "rss_mb": 717, "done": 36, "queue": 4}
{"t": 82, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 70.9, "watts": 5.25, "rss_mb": 718, "done": 36, "queue": 5}
{"t": 83, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 79.0, "watts": 5.91, "rss_mb": 618, "done": 37, "queue": 4}
{"t": 84, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.2, "watts": 5.21, "rss_mb": 718, "done": 37, "queue": 5}
{"t": 85, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 62.8, "watts": 5.03, "rss_mb": 593, "done": 37, "queue": 5}
{"t": 86, "temp_c": 56.0, "mhz": 1199, "cpu_pct": 67.6, "watts": 5.23, "rss_mb": 718, "done": 38, "queue": 5}
{"t": 87, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 63.2, "watts": 3.70, "rss_mb": 718, "done": 38, "queue": 5}
{"t": 88, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 60.1, "watts": 4.96, "rss_mb": 718, "done": 39, "queue": 5}
{"t": 89, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 68.2, "watts": 5.14, "rss_mb": 718, "done": 39, "queue": 5}
{"t": 90, "temp_c": 56.0, "mhz": 1099, "cpu_pct": 61.4, "watts": 4.95, "rss_mb": 718, "done": 40, "queue": 5}
{"t": 91, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.9, "watts": 5.04, "rss_mb": 718, "done": 40, "queue": 5}
{"t": 92, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 58.8, "watts": 4.94, "rss_mb": 686, "done": 41, "queue": 5}
{"t": 93, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 69.1, "watts": 3.83, "rss_mb": 718, "done": 41, "queue": 5}
{"t": 94, "temp_c": 56.0, "mhz": 1199, "cpu_pct": 60.8, "watts": 5.00, "rss_mb": 635, "done": 42, "queue": 5}
{"t": 95, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 67.2, "watts": 5.20, "rss_mb": 719, "done": 42, "queue": 5}
{"t": 96, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 63.4, "watts": 5.03, "rss_mb": 594, "done": 42, "queue": 6}
{"t": 97, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.2, "watts": 5.10, "rss_mb": 719, "done": 43, "queue": 5}
{"t": 98, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.0, "watts": 5.10, "rss_mb": 719, "done": 43, "queue": 6}
{"t": 99, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 59.3, "watts": 3.71, "rss_mb": 735, "done": 44, "queue": 5}
{"t": 100, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.4, "watts": 5.08, "rss_mb": 735, "done": 44, "queue": 6}
{"t": 101, "temp_c": 55.0, "mhz": 1000, "cpu_pct": 61.4, "watts": 4.88, "rss_mb": 719, "done": 45, "queue": 5}
{"t": 102, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 66.2, "watts": 5.09, "rss_mb": 719, "done": 45, "queue": 6}
{"t": 103, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 59.8, "watts": 5.08, "rss_mb": 700, "done": 46, "queue": 5}
{"t": 104, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 69.1, "watts": 5.23, "rss_mb": 719, "done": 46, "queue": 6}
{"t": 105, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 61.6, "watts": 3.65, "rss_mb": 646, "done": 47, "queue": 5}
{"t": 106, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 66.3, "watts": 5.15, "rss_mb": 719, "done": 47, "queue": 6}
{"t": 107, "temp_c": 55.0, "mhz": 1118, "cpu_pct": 62.1, "watts": 5.07, "rss_mb": 502, "done": 47, "queue": 6}
{"t": 108, "temp_c": 56.0, "mhz": 900, "cpu_pct": 67.2, "watts": 5.01, "rss_mb": 720, "done": 48, "queue": 6}
{"t": 109, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 64.2, "watts": 4.91, "rss_mb": 720, "done": 48, "queue": 6}
{"t": 110, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 61.2, "watts": 4.98, "rss_mb": 720, "done": 49, "queue": 6}
{"t": 111, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 68.6, "watts": 3.92, "rss_mb": 720, "done": 49, "queue": 6}
{"t": 112, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 58.4, "watts": 5.02, "rss_mb": 720, "done": 50, "queue": 6}
{"t": 113, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 73.2, "watts": 5.16, "rss_mb": 720, "done": 50, "queue": 6}
{"t": 114, "temp_c": 56.0, "mhz": 1100, "cpu_pct": 72.0, "watts": 5.80, "rss_mb": 659, "done": 51, "queue": 6}
{"t": 115, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 68.2, "watts": 5.13, "rss_mb": 720, "done": 51, "queue": 6}
{"t": 116, "temp_c": 55.0, "mhz": 1100, "cpu_pct": 60.4, "watts": 3.83, "rss_mb": 503, "done": 52, "queue": 6}
{"t": 117, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 68.2, "watts": 4.97, "rss_mb": 737, "done": 52, "queue": 6}
{"t": 118, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 64.4, "watts": 5.13, "rss_mb": 738, "done": 52, "queue": 7}
{"t": 119, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 59.6, "watts": 4.81, "rss_mb": 722, "done": 53, "queue": 6}
{"t": 120, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 67.4, "watts": 5.09, "rss_mb": 722, "done": 53, "queue": 6}
{"t": 121, "temp_c": 53.0, "mhz": 784, "cpu_pct": 25.3, "watts": 3.88, "rss_mb": 392, "done": 54, "queue": 6}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 120.6, "frames": 54, "frames_per_s": 0.448, "job_ms_p50": 2202, "latency_ms_p50": 8839, "latency_ms_p95": 13952, "latency_ms_max": 14584, "max_queue": 7, "backlog_at_end": 6, "cpu_s_per_frame": 5.184, "peak_rss_mb": 738, "temp_c_mean": 55.8, "temp_c_max": 58.0, "mhz_mean": 1167, "mhz_min": 784, "cpu_pct_mean": 64.1, "watts_mean": 4.85}

View File

@ -0,0 +1,62 @@
{"t": 1, "temp_c": 55.0, "mhz": 1400, "cpu_pct": 59.9, "watts": 5.29, "rss_mb": 578, "done": 0, "queue": 0}
{"t": 2, "temp_c": 56.0, "mhz": 1000, "cpu_pct": 62.6, "watts": 5.37, "rss_mb": 372, "done": 1, "queue": 0}
{"t": 3, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 64.2, "watts": 5.57, "rss_mb": 620, "done": 1, "queue": 0}
{"t": 4, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 59.3, "watts": 3.95, "rss_mb": 606, "done": 2, "queue": 0}
{"t": 5, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 64.8, "watts": 5.45, "rss_mb": 632, "done": 2, "queue": 0}
{"t": 6, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 57.0, "watts": 5.41, "rss_mb": 567, "done": 3, "queue": 0}
{"t": 7, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 63.5, "watts": 5.29, "rss_mb": 571, "done": 3, "queue": 0}
{"t": 8, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 61.6, "watts": 5.42, "rss_mb": 684, "done": 4, "queue": 0}
{"t": 9, "temp_c": 54.0, "mhz": 1000, "cpu_pct": 57.7, "watts": 5.51, "rss_mb": 452, "done": 5, "queue": 0}
{"t": 10, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 68.0, "watts": 5.47, "rss_mb": 693, "done": 5, "queue": 0}
{"t": 11, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 59.5, "watts": 5.41, "rss_mb": 669, "done": 6, "queue": 0}
{"t": 12, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.5, "watts": 4.17, "rss_mb": 681, "done": 6, "queue": 0}
{"t": 13, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 58.5, "watts": 5.45, "rss_mb": 687, "done": 7, "queue": 0}
{"t": 14, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.4, "watts": 5.52, "rss_mb": 688, "done": 7, "queue": 0}
{"t": 15, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 63.2, "watts": 5.57, "rss_mb": 690, "done": 8, "queue": 0}
{"t": 16, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 58.2, "watts": 5.33, "rss_mb": 658, "done": 9, "queue": 0}
{"t": 17, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 75.3, "watts": 5.93, "rss_mb": 696, "done": 9, "queue": 0}
{"t": 18, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 5.80, "rss_mb": 697, "done": 10, "queue": 0}
{"t": 19, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 65.3, "watts": 4.09, "rss_mb": 699, "done": 10, "queue": 0}
{"t": 20, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 63.2, "watts": 5.43, "rss_mb": 700, "done": 11, "queue": 0}
{"t": 21, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 58.1, "watts": 5.68, "rss_mb": 525, "done": 12, "queue": 0}
{"t": 22, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 64.9, "watts": 5.45, "rss_mb": 702, "done": 12, "queue": 0}
{"t": 23, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.8, "watts": 5.56, "rss_mb": 702, "done": 13, "queue": 0}
{"t": 24, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 5.60, "rss_mb": 703, "done": 13, "queue": 0}
{"t": 25, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 60.6, "watts": 5.49, "rss_mb": 704, "done": 14, "queue": 0}
{"t": 26, "temp_c": 55.0, "mhz": 789, "cpu_pct": 57.9, "watts": 5.44, "rss_mb": 580, "done": 14, "queue": 0}
{"t": 27, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.2, "watts": 4.26, "rss_mb": 705, "done": 15, "queue": 0}
{"t": 28, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 60.2, "watts": 5.29, "rss_mb": 701, "done": 16, "queue": 0}
{"t": 29, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 65.9, "watts": 5.55, "rss_mb": 705, "done": 16, "queue": 0}
{"t": 30, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 60.0, "watts": 5.51, "rss_mb": 709, "done": 17, "queue": 0}
{"t": 31, "temp_c": 56.0, "mhz": 1591, "cpu_pct": 64.1, "watts": 5.52, "rss_mb": 722, "done": 17, "queue": 0}
{"t": 32, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 61.7, "watts": 5.53, "rss_mb": 707, "done": 18, "queue": 0}
{"t": 33, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 59.8, "watts": 5.48, "rss_mb": 675, "done": 19, "queue": 0}
{"t": 34, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.8, "watts": 5.60, "rss_mb": 724, "done": 19, "queue": 0}
{"t": 35, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 58.5, "watts": 4.14, "rss_mb": 724, "done": 20, "queue": 0}
{"t": 36, "temp_c": 58.0, "mhz": 1587, "cpu_pct": 67.7, "watts": 5.62, "rss_mb": 724, "done": 20, "queue": 0}
{"t": 37, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 58.8, "watts": 5.43, "rss_mb": 724, "done": 21, "queue": 0}
{"t": 38, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 62.6, "watts": 5.38, "rss_mb": 648, "done": 22, "queue": 0}
{"t": 39, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.9, "watts": 5.69, "rss_mb": 726, "done": 22, "queue": 0}
{"t": 40, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 60.4, "watts": 5.55, "rss_mb": 710, "done": 23, "queue": 0}
{"t": 41, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.2, "watts": 5.56, "rss_mb": 710, "done": 23, "queue": 0}
{"t": 42, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 62.0, "watts": 4.01, "rss_mb": 710, "done": 24, "queue": 0}
{"t": 43, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 59.8, "watts": 5.66, "rss_mb": 585, "done": 25, "queue": 0}
{"t": 44, "temp_c": 56.0, "mhz": 1000, "cpu_pct": 63.8, "watts": 5.49, "rss_mb": 726, "done": 25, "queue": 0}
{"t": 45, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.4, "watts": 5.54, "rss_mb": 710, "done": 26, "queue": 0}
{"t": 46, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 5.50, "rss_mb": 711, "done": 26, "queue": 0}
{"t": 47, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 61.2, "watts": 5.57, "rss_mb": 712, "done": 27, "queue": 0}
{"t": 48, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 72.9, "watts": 5.92, "rss_mb": 712, "done": 27, "queue": 0}
{"t": 49, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.2, "watts": 4.49, "rss_mb": 712, "done": 28, "queue": 0}
{"t": 50, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 59.9, "watts": 5.39, "rss_mb": 673, "done": 29, "queue": 0}
{"t": 51, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 67.8, "watts": 5.64, "rss_mb": 712, "done": 29, "queue": 0}
{"t": 52, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 57.3, "watts": 5.47, "rss_mb": 712, "done": 30, "queue": 0}
{"t": 53, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 67.6, "watts": 5.68, "rss_mb": 713, "done": 30, "queue": 0}
{"t": 54, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 60.6, "watts": 5.40, "rss_mb": 713, "done": 31, "queue": 0}
{"t": 55, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 58.5, "watts": 5.34, "rss_mb": 519, "done": 32, "queue": 0}
{"t": 56, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 65.0, "watts": 4.19, "rss_mb": 714, "done": 32, "queue": 0}
{"t": 57, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 62.2, "watts": 5.39, "rss_mb": 714, "done": 33, "queue": 0}
{"t": 58, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 5.57, "rss_mb": 715, "done": 33, "queue": 0}
{"t": 59, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 61.8, "watts": 5.39, "rss_mb": 731, "done": 34, "queue": 0}
{"t": 60, "temp_c": 56.0, "mhz": 1498, "cpu_pct": 59.9, "watts": 5.56, "rss_mb": 591, "done": 34, "queue": 0}
{"t": 61, "temp_c": 53.0, "mhz": 795, "cpu_pct": 5.3, "watts": 3.18, "rss_mb": 386, "done": 35, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 0, "secs": 60.1, "frames": 35, "frames_per_s": 0.582, "job_ms_p50": 1681, "latency_ms_p50": 1695, "latency_ms_p95": 1794, "latency_ms_max": 1881, "max_queue": 0, "backlog_at_end": 0, "cpu_s_per_frame": 3.972, "peak_rss_mb": 731, "temp_c_mean": 56.9, "temp_c_max": 59.0, "mhz_mean": 1519, "mhz_min": 789, "cpu_pct_mean": 62.0, "watts_mean": 5.30}

View File

@ -0,0 +1,121 @@
{"t": 1, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 59.8, "watts": 5.29, "rss_mb": 579, "done": 0, "queue": 0}
{"t": 2, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 54.7, "watts": 5.09, "rss_mb": 248, "done": 1, "queue": 0}
{"t": 3, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.9, "watts": 4.16, "rss_mb": 616, "done": 1, "queue": 0}
{"t": 4, "temp_c": 53.0, "mhz": 800, "cpu_pct": 45.8, "watts": 5.52, "rss_mb": 288, "done": 2, "queue": 0}
{"t": 5, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 64.2, "watts": 4.92, "rss_mb": 632, "done": 2, "queue": 0}
{"t": 6, "temp_c": 54.0, "mhz": 794, "cpu_pct": 46.4, "watts": 5.12, "rss_mb": 302, "done": 3, "queue": 0}
{"t": 7, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 62.1, "watts": 5.11, "rss_mb": 553, "done": 3, "queue": 0}
{"t": 8, "temp_c": 54.0, "mhz": 796, "cpu_pct": 49.2, "watts": 5.15, "rss_mb": 388, "done": 4, "queue": 0}
{"t": 9, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.8, "watts": 5.32, "rss_mb": 686, "done": 4, "queue": 0}
{"t": 10, "temp_c": 54.0, "mhz": 866, "cpu_pct": 45.0, "watts": 4.87, "rss_mb": 454, "done": 5, "queue": 0}
{"t": 11, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.4, "watts": 4.22, "rss_mb": 694, "done": 5, "queue": 0}
{"t": 12, "temp_c": 54.0, "mhz": 800, "cpu_pct": 39.9, "watts": 5.13, "rss_mb": 461, "done": 6, "queue": 0}
{"t": 13, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.4, "watts": 5.05, "rss_mb": 681, "done": 6, "queue": 0}
{"t": 14, "temp_c": 54.0, "mhz": 835, "cpu_pct": 41.5, "watts": 5.07, "rss_mb": 464, "done": 7, "queue": 0}
{"t": 15, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.8, "watts": 5.23, "rss_mb": 684, "done": 7, "queue": 0}
{"t": 16, "temp_c": 54.0, "mhz": 900, "cpu_pct": 40.3, "watts": 4.72, "rss_mb": 467, "done": 8, "queue": 0}
{"t": 17, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 80.7, "watts": 6.22, "rss_mb": 686, "done": 8, "queue": 0}
{"t": 18, "temp_c": 55.0, "mhz": 942, "cpu_pct": 55.8, "watts": 4.27, "rss_mb": 469, "done": 9, "queue": 0}
{"t": 19, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.0, "watts": 5.21, "rss_mb": 688, "done": 9, "queue": 0}
{"t": 20, "temp_c": 54.0, "mhz": 1119, "cpu_pct": 39.2, "watts": 4.99, "rss_mb": 471, "done": 10, "queue": 0}
{"t": 21, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 68.5, "watts": 5.30, "rss_mb": 690, "done": 10, "queue": 0}
{"t": 22, "temp_c": 55.0, "mhz": 1244, "cpu_pct": 41.4, "watts": 4.95, "rss_mb": 473, "done": 11, "queue": 0}
{"t": 23, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.9, "watts": 5.49, "rss_mb": 694, "done": 11, "queue": 0}
{"t": 24, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 40.9, "watts": 4.52, "rss_mb": 476, "done": 12, "queue": 0}
{"t": 25, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 67.3, "watts": 4.26, "rss_mb": 695, "done": 12, "queue": 0}
{"t": 26, "temp_c": 54.0, "mhz": 1421, "cpu_pct": 41.1, "watts": 5.08, "rss_mb": 478, "done": 13, "queue": 0}
{"t": 27, "temp_c": 58.0, "mhz": 1200, "cpu_pct": 66.7, "watts": 5.16, "rss_mb": 697, "done": 13, "queue": 0}
{"t": 28, "temp_c": 54.0, "mhz": 1026, "cpu_pct": 38.9, "watts": 4.88, "rss_mb": 479, "done": 14, "queue": 0}
{"t": 29, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.0, "watts": 5.39, "rss_mb": 700, "done": 14, "queue": 0}
{"t": 30, "temp_c": 55.0, "mhz": 1200, "cpu_pct": 41.2, "watts": 4.79, "rss_mb": 483, "done": 15, "queue": 0}
{"t": 31, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 66.3, "watts": 5.57, "rss_mb": 702, "done": 15, "queue": 0}
{"t": 32, "temp_c": 55.0, "mhz": 1255, "cpu_pct": 39.6, "watts": 3.70, "rss_mb": 484, "done": 16, "queue": 0}
{"t": 33, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 67.6, "watts": 5.03, "rss_mb": 702, "done": 16, "queue": 0}
{"t": 34, "temp_c": 54.0, "mhz": 1400, "cpu_pct": 40.9, "watts": 5.04, "rss_mb": 485, "done": 17, "queue": 0}
{"t": 35, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.5, "watts": 5.09, "rss_mb": 719, "done": 17, "queue": 0}
{"t": 36, "temp_c": 54.0, "mhz": 1404, "cpu_pct": 41.5, "watts": 4.91, "rss_mb": 486, "done": 18, "queue": 0}
{"t": 37, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 67.3, "watts": 5.39, "rss_mb": 704, "done": 18, "queue": 0}
{"t": 38, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 40.7, "watts": 4.70, "rss_mb": 494, "done": 19, "queue": 0}
{"t": 39, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.6, "watts": 4.22, "rss_mb": 722, "done": 19, "queue": 0}
{"t": 40, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 42.0, "watts": 5.14, "rss_mb": 504, "done": 20, "queue": 0}
{"t": 41, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.7, "watts": 4.99, "rss_mb": 722, "done": 20, "queue": 0}
{"t": 42, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 43.1, "watts": 5.13, "rss_mb": 517, "done": 21, "queue": 0}
{"t": 43, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 66.8, "watts": 5.25, "rss_mb": 722, "done": 21, "queue": 0}
{"t": 44, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 42.6, "watts": 4.71, "rss_mb": 516, "done": 22, "queue": 0}
{"t": 45, "temp_c": 57.0, "mhz": 1200, "cpu_pct": 64.9, "watts": 5.57, "rss_mb": 722, "done": 22, "queue": 0}
{"t": 46, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 41.9, "watts": 4.55, "rss_mb": 529, "done": 23, "queue": 0}
{"t": 47, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 66.5, "watts": 4.18, "rss_mb": 707, "done": 23, "queue": 0}
{"t": 48, "temp_c": 55.0, "mhz": 1300, "cpu_pct": 60.5, "watts": 5.89, "rss_mb": 503, "done": 24, "queue": 0}
{"t": 49, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.0, "watts": 5.10, "rss_mb": 707, "done": 24, "queue": 0}
{"t": 50, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 43.4, "watts": 4.93, "rss_mb": 555, "done": 25, "queue": 0}
{"t": 51, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 63.8, "watts": 5.48, "rss_mb": 724, "done": 25, "queue": 0}
{"t": 52, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 43.4, "watts": 4.58, "rss_mb": 541, "done": 26, "queue": 0}
{"t": 53, "temp_c": 57.0, "mhz": 1599, "cpu_pct": 64.1, "watts": 5.53, "rss_mb": 708, "done": 26, "queue": 0}
{"t": 54, "temp_c": 55.0, "mhz": 1600, "cpu_pct": 44.3, "watts": 3.82, "rss_mb": 564, "done": 27, "queue": 0}
{"t": 55, "temp_c": 58.0, "mhz": 1400, "cpu_pct": 63.9, "watts": 5.03, "rss_mb": 709, "done": 27, "queue": 0}
{"t": 56, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 43.7, "watts": 4.86, "rss_mb": 554, "done": 28, "queue": 0}
{"t": 57, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 64.1, "watts": 5.39, "rss_mb": 709, "done": 28, "queue": 0}
{"t": 58, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 45.5, "watts": 4.79, "rss_mb": 580, "done": 29, "queue": 0}
{"t": 59, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 64.1, "watts": 5.49, "rss_mb": 709, "done": 29, "queue": 0}
{"t": 60, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 45.4, "watts": 4.66, "rss_mb": 567, "done": 30, "queue": 0}
{"t": 61, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 62.6, "watts": 5.49, "rss_mb": 710, "done": 30, "queue": 0}
{"t": 62, "temp_c": 58.0, "mhz": 1599, "cpu_pct": 44.5, "watts": 3.73, "rss_mb": 586, "done": 31, "queue": 0}
{"t": 63, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 63.9, "watts": 5.17, "rss_mb": 710, "done": 31, "queue": 0}
{"t": 64, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 45.2, "watts": 4.82, "rss_mb": 595, "done": 32, "queue": 0}
{"t": 65, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 62.4, "watts": 5.48, "rss_mb": 710, "done": 32, "queue": 0}
{"t": 66, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 44.8, "watts": 4.66, "rss_mb": 590, "done": 33, "queue": 0}
{"t": 67, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 63.9, "watts": 5.62, "rss_mb": 710, "done": 33, "queue": 0}
{"t": 68, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 44.0, "watts": 4.69, "rss_mb": 598, "done": 34, "queue": 0}
{"t": 69, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 63.9, "watts": 4.11, "rss_mb": 727, "done": 34, "queue": 0}
{"t": 70, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 42.4, "watts": 4.89, "rss_mb": 599, "done": 35, "queue": 0}
{"t": 71, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 64.1, "watts": 5.35, "rss_mb": 711, "done": 35, "queue": 0}
{"t": 72, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 46.5, "watts": 4.87, "rss_mb": 605, "done": 36, "queue": 0}
{"t": 73, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.4, "watts": 5.52, "rss_mb": 711, "done": 36, "queue": 0}
{"t": 74, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 40.5, "watts": 4.55, "rss_mb": 604, "done": 37, "queue": 0}
{"t": 75, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.6, "watts": 5.67, "rss_mb": 712, "done": 37, "queue": 0}
{"t": 76, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 42.2, "watts": 3.55, "rss_mb": 608, "done": 38, "queue": 0}
{"t": 77, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.8, "watts": 5.29, "rss_mb": 712, "done": 38, "queue": 0}
{"t": 78, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 43.9, "watts": 4.92, "rss_mb": 609, "done": 39, "queue": 0}
{"t": 79, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 82.8, "watts": 6.23, "rss_mb": 713, "done": 39, "queue": 0}
{"t": 80, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 52.3, "watts": 5.16, "rss_mb": 613, "done": 40, "queue": 0}
{"t": 81, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.6, "watts": 5.65, "rss_mb": 713, "done": 40, "queue": 0}
{"t": 82, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 43.5, "watts": 4.63, "rss_mb": 611, "done": 41, "queue": 0}
{"t": 83, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.3, "watts": 4.16, "rss_mb": 713, "done": 41, "queue": 0}
{"t": 84, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 43.2, "watts": 5.07, "rss_mb": 619, "done": 42, "queue": 0}
{"t": 85, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.9, "watts": 5.34, "rss_mb": 713, "done": 42, "queue": 0}
{"t": 86, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 40.5, "watts": 4.53, "rss_mb": 615, "done": 43, "queue": 0}
{"t": 87, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 65.1, "watts": 5.67, "rss_mb": 714, "done": 43, "queue": 0}
{"t": 88, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 43.4, "watts": 4.60, "rss_mb": 620, "done": 44, "queue": 0}
{"t": 89, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 63.8, "watts": 5.52, "rss_mb": 731, "done": 44, "queue": 0}
{"t": 90, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 46.0, "watts": 4.79, "rss_mb": 611, "done": 45, "queue": 0}
{"t": 91, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 63.2, "watts": 4.04, "rss_mb": 731, "done": 45, "queue": 0}
{"t": 92, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 44.9, "watts": 4.95, "rss_mb": 629, "done": 46, "queue": 0}
{"t": 93, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 66.0, "watts": 5.47, "rss_mb": 731, "done": 46, "queue": 0}
{"t": 94, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 43.2, "watts": 4.55, "rss_mb": 615, "done": 47, "queue": 0}
{"t": 95, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 64.2, "watts": 5.62, "rss_mb": 715, "done": 47, "queue": 0}
{"t": 96, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 43.9, "watts": 4.80, "rss_mb": 631, "done": 48, "queue": 0}
{"t": 97, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 4.12, "rss_mb": 731, "done": 48, "queue": 0}
{"t": 98, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 43.4, "watts": 4.91, "rss_mb": 622, "done": 49, "queue": 0}
{"t": 99, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.5, "watts": 5.37, "rss_mb": 731, "done": 49, "queue": 0}
{"t": 100, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 41.4, "watts": 4.67, "rss_mb": 640, "done": 50, "queue": 0}
{"t": 101, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.4, "watts": 5.59, "rss_mb": 733, "done": 50, "queue": 0}
{"t": 102, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 43.3, "watts": 4.68, "rss_mb": 630, "done": 51, "queue": 0}
{"t": 103, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.7, "watts": 5.52, "rss_mb": 733, "done": 51, "queue": 0}
{"t": 104, "temp_c": 56.0, "mhz": 1600, "cpu_pct": 42.1, "watts": 4.66, "rss_mb": 641, "done": 52, "queue": 0}
{"t": 105, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 66.5, "watts": 4.27, "rss_mb": 733, "done": 52, "queue": 0}
{"t": 106, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 43.0, "watts": 4.77, "rss_mb": 632, "done": 53, "queue": 0}
{"t": 107, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.2, "watts": 5.50, "rss_mb": 717, "done": 53, "queue": 0}
{"t": 108, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 43.4, "watts": 4.68, "rss_mb": 644, "done": 54, "queue": 0}
{"t": 109, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 68.2, "watts": 5.60, "rss_mb": 734, "done": 54, "queue": 0}
{"t": 110, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.5, "watts": 5.85, "rss_mb": 612, "done": 55, "queue": 0}
{"t": 111, "temp_c": 57.0, "mhz": 1600, "cpu_pct": 63.2, "watts": 5.52, "rss_mb": 718, "done": 55, "queue": 0}
{"t": 112, "temp_c": 56.0, "mhz": 1400, "cpu_pct": 44.7, "watts": 3.63, "rss_mb": 654, "done": 56, "queue": 0}
{"t": 113, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 65.9, "watts": 5.42, "rss_mb": 719, "done": 56, "queue": 0}
{"t": 114, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 42.3, "watts": 4.64, "rss_mb": 644, "done": 57, "queue": 0}
{"t": 115, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 64.9, "watts": 5.55, "rss_mb": 720, "done": 57, "queue": 0}
{"t": 116, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 40.7, "watts": 4.68, "rss_mb": 659, "done": 58, "queue": 0}
{"t": 117, "temp_c": 58.0, "mhz": 1600, "cpu_pct": 67.2, "watts": 5.55, "rss_mb": 720, "done": 58, "queue": 0}
{"t": 118, "temp_c": 56.0, "mhz": 1200, "cpu_pct": 42.0, "watts": 3.51, "rss_mb": 653, "done": 59, "queue": 0}
{"t": 119, "temp_c": 59.0, "mhz": 1600, "cpu_pct": 66.4, "watts": 5.35, "rss_mb": 737, "done": 59, "queue": 0}
{"t": 120, "temp_c": 55.0, "mhz": 800, "cpu_pct": 32.7, "watts": 4.70, "rss_mb": 391, "done": 60, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 120.0, "frames": 60, "frames_per_s": 0.500, "job_ms_p50": 1703, "latency_ms_p50": 1718, "latency_ms_p95": 1843, "latency_ms_max": 1872, "max_queue": 1, "backlog_at_end": 0, "cpu_s_per_frame": 3.977, "peak_rss_mb": 736, "temp_c_mean": 56.6, "temp_c_max": 59.0, "mhz_mean": 1476, "mhz_min": 794, "cpu_pct_mean": 55.0, "watts_mean": 4.98}

View File

@ -0,0 +1,62 @@
{"t": 1, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.9, "watts": 4.52, "rss_mb": 527, "done": 0, "queue": 0}
{"t": 2, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.8, "watts": 4.60, "rss_mb": 531, "done": 0, "queue": 0}
{"t": 3, "temp_c": 52.0, "mhz": 800, "cpu_pct": 72.1, "watts": 4.91, "rss_mb": 534, "done": 0, "queue": 0}
{"t": 4, "temp_c": 54.0, "mhz": 800, "cpu_pct": 55.7, "watts": 3.30, "rss_mb": 478, "done": 1, "queue": 0}
{"t": 5, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.0, "watts": 4.86, "rss_mb": 621, "done": 1, "queue": 0}
{"t": 6, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.3, "watts": 4.68, "rss_mb": 626, "done": 1, "queue": 0}
{"t": 7, "temp_c": 53.0, "mhz": 800, "cpu_pct": 53.1, "watts": 4.58, "rss_mb": 296, "done": 2, "queue": 0}
{"t": 8, "temp_c": 52.0, "mhz": 800, "cpu_pct": 64.8, "watts": 3.65, "rss_mb": 614, "done": 2, "queue": 0}
{"t": 9, "temp_c": 53.0, "mhz": 800, "cpu_pct": 72.0, "watts": 4.73, "rss_mb": 635, "done": 2, "queue": 0}
{"t": 10, "temp_c": 53.0, "mhz": 800, "cpu_pct": 65.5, "watts": 4.68, "rss_mb": 636, "done": 2, "queue": 0}
{"t": 11, "temp_c": 53.0, "mhz": 800, "cpu_pct": 61.2, "watts": 4.67, "rss_mb": 601, "done": 3, "queue": 0}
{"t": 12, "temp_c": 54.0, "mhz": 800, "cpu_pct": 62.9, "watts": 4.72, "rss_mb": 618, "done": 3, "queue": 0}
{"t": 13, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.2, "watts": 3.45, "rss_mb": 582, "done": 3, "queue": 0}
{"t": 14, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.4, "watts": 4.61, "rss_mb": 469, "done": 4, "queue": 0}
{"t": 15, "temp_c": 54.0, "mhz": 800, "cpu_pct": 63.3, "watts": 4.65, "rss_mb": 670, "done": 4, "queue": 0}
{"t": 16, "temp_c": 53.0, "mhz": 800, "cpu_pct": 66.9, "watts": 4.91, "rss_mb": 687, "done": 4, "queue": 0}
{"t": 17, "temp_c": 53.0, "mhz": 800, "cpu_pct": 67.3, "watts": 3.62, "rss_mb": 688, "done": 4, "queue": 0}
{"t": 18, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.2, "watts": 4.73, "rss_mb": 693, "done": 5, "queue": 0}
{"t": 19, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.2, "watts": 4.70, "rss_mb": 696, "done": 5, "queue": 0}
{"t": 20, "temp_c": 53.0, "mhz": 800, "cpu_pct": 68.4, "watts": 4.99, "rss_mb": 696, "done": 5, "queue": 0}
{"t": 21, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.4, "watts": 3.28, "rss_mb": 664, "done": 6, "queue": 0}
{"t": 22, "temp_c": 53.0, "mhz": 800, "cpu_pct": 65.3, "watts": 4.97, "rss_mb": 684, "done": 6, "queue": 0}
{"t": 23, "temp_c": 54.0, "mhz": 800, "cpu_pct": 78.5, "watts": 4.95, "rss_mb": 685, "done": 6, "queue": 0}
{"t": 24, "temp_c": 54.0, "mhz": 800, "cpu_pct": 74.2, "watts": 5.27, "rss_mb": 532, "done": 7, "queue": 0}
{"t": 25, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.5, "watts": 3.66, "rss_mb": 688, "done": 7, "queue": 0}
{"t": 26, "temp_c": 53.0, "mhz": 800, "cpu_pct": 68.5, "watts": 5.06, "rss_mb": 689, "done": 7, "queue": 0}
{"t": 27, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.1, "watts": 4.67, "rss_mb": 689, "done": 7, "queue": 0}
{"t": 28, "temp_c": 53.0, "mhz": 800, "cpu_pct": 62.4, "watts": 4.71, "rss_mb": 690, "done": 8, "queue": 0}
{"t": 29, "temp_c": 54.0, "mhz": 800, "cpu_pct": 70.8, "watts": 4.77, "rss_mb": 692, "done": 8, "queue": 0}
{"t": 30, "temp_c": 54.0, "mhz": 800, "cpu_pct": 63.8, "watts": 3.59, "rss_mb": 692, "done": 8, "queue": 0}
{"t": 31, "temp_c": 55.0, "mhz": 800, "cpu_pct": 62.2, "watts": 4.51, "rss_mb": 692, "done": 9, "queue": 0}
{"t": 32, "temp_c": 52.0, "mhz": 800, "cpu_pct": 62.1, "watts": 4.97, "rss_mb": 692, "done": 9, "queue": 0}
{"t": 33, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.0, "watts": 4.81, "rss_mb": 693, "done": 9, "queue": 0}
{"t": 34, "temp_c": 53.0, "mhz": 800, "cpu_pct": 54.8, "watts": 3.41, "rss_mb": 661, "done": 10, "queue": 0}
{"t": 35, "temp_c": 54.0, "mhz": 800, "cpu_pct": 76.5, "watts": 4.96, "rss_mb": 694, "done": 10, "queue": 0}
{"t": 36, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.0, "watts": 4.79, "rss_mb": 694, "done": 10, "queue": 0}
{"t": 37, "temp_c": 53.0, "mhz": 800, "cpu_pct": 55.2, "watts": 4.47, "rss_mb": 517, "done": 11, "queue": 0}
{"t": 38, "temp_c": 54.0, "mhz": 800, "cpu_pct": 70.4, "watts": 3.70, "rss_mb": 696, "done": 11, "queue": 0}
{"t": 39, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.8, "watts": 4.89, "rss_mb": 697, "done": 11, "queue": 0}
{"t": 40, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.2, "watts": 4.70, "rss_mb": 627, "done": 11, "queue": 0}
{"t": 41, "temp_c": 52.0, "mhz": 800, "cpu_pct": 63.2, "watts": 4.66, "rss_mb": 697, "done": 12, "queue": 0}
{"t": 42, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.8, "watts": 4.74, "rss_mb": 698, "done": 12, "queue": 0}
{"t": 43, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.9, "watts": 3.59, "rss_mb": 698, "done": 12, "queue": 0}
{"t": 44, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.1, "watts": 4.65, "rss_mb": 698, "done": 13, "queue": 0}
{"t": 45, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.2, "watts": 4.91, "rss_mb": 699, "done": 13, "queue": 0}
{"t": 46, "temp_c": 54.0, "mhz": 800, "cpu_pct": 72.6, "watts": 4.78, "rss_mb": 699, "done": 13, "queue": 0}
{"t": 47, "temp_c": 54.0, "mhz": 800, "cpu_pct": 55.4, "watts": 3.50, "rss_mb": 667, "done": 14, "queue": 0}
{"t": 48, "temp_c": 54.0, "mhz": 800, "cpu_pct": 74.2, "watts": 4.86, "rss_mb": 700, "done": 14, "queue": 0}
{"t": 49, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.2, "watts": 4.76, "rss_mb": 700, "done": 14, "queue": 0}
{"t": 50, "temp_c": 54.0, "mhz": 800, "cpu_pct": 56.8, "watts": 4.54, "rss_mb": 506, "done": 15, "queue": 0}
{"t": 51, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.2, "watts": 4.79, "rss_mb": 701, "done": 15, "queue": 0}
{"t": 52, "temp_c": 53.0, "mhz": 800, "cpu_pct": 66.6, "watts": 3.70, "rss_mb": 701, "done": 15, "queue": 0}
{"t": 53, "temp_c": 53.0, "mhz": 800, "cpu_pct": 67.8, "watts": 4.63, "rss_mb": 701, "done": 15, "queue": 0}
{"t": 54, "temp_c": 55.0, "mhz": 800, "cpu_pct": 70.1, "watts": 4.84, "rss_mb": 703, "done": 16, "queue": 0}
{"t": 55, "temp_c": 54.0, "mhz": 800, "cpu_pct": 84.9, "watts": 4.39, "rss_mb": 706, "done": 16, "queue": 0}
{"t": 56, "temp_c": 54.0, "mhz": 800, "cpu_pct": 73.2, "watts": 4.94, "rss_mb": 706, "done": 16, "queue": 0}
{"t": 57, "temp_c": 54.0, "mhz": 800, "cpu_pct": 51.4, "watts": 4.60, "rss_mb": 674, "done": 17, "queue": 0}
{"t": 58, "temp_c": 55.0, "mhz": 800, "cpu_pct": 73.6, "watts": 4.82, "rss_mb": 723, "done": 17, "queue": 0}
{"t": 59, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.4, "watts": 4.85, "rss_mb": 723, "done": 17, "queue": 0}
{"t": 60, "temp_c": 52.0, "mhz": 800, "cpu_pct": 58.9, "watts": 3.52, "rss_mb": 454, "done": 18, "queue": 0}
{"t": 61, "temp_c": 51.0, "mhz": 798, "cpu_pct": 7.8, "watts": 3.02, "rss_mb": 377, "done": 18, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 0, "secs": 60.1, "frames": 18, "frames_per_s": 0.299, "job_ms_p50": 3294, "latency_ms_p50": 3312, "latency_ms_p95": 3537, "latency_ms_max": 3567, "max_queue": 0, "backlog_at_end": 0, "cpu_s_per_frame": 7.664, "peak_rss_mb": 723, "temp_c_mean": 53.5, "temp_c_max": 55.0, "mhz_mean": 800, "mhz_min": 798, "cpu_pct_mean": 64.9, "watts_mean": 4.47}

View File

@ -0,0 +1,122 @@
{"t": 1, "temp_c": 54.0, "mhz": 800, "cpu_pct": 62.3, "watts": 4.46, "rss_mb": 525, "done": 0, "queue": 0}
{"t": 2, "temp_c": 53.0, "mhz": 800, "cpu_pct": 65.1, "watts": 4.70, "rss_mb": 531, "done": 0, "queue": 1}
{"t": 3, "temp_c": 53.0, "mhz": 800, "cpu_pct": 72.4, "watts": 4.95, "rss_mb": 534, "done": 0, "queue": 1}
{"t": 4, "temp_c": 54.0, "mhz": 800, "cpu_pct": 54.9, "watts": 3.32, "rss_mb": 465, "done": 1, "queue": 1}
{"t": 5, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.2, "watts": 4.74, "rss_mb": 604, "done": 1, "queue": 1}
{"t": 6, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.7, "watts": 4.75, "rss_mb": 609, "done": 1, "queue": 2}
{"t": 7, "temp_c": 53.0, "mhz": 800, "cpu_pct": 57.5, "watts": 4.57, "rss_mb": 284, "done": 2, "queue": 1}
{"t": 8, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.5, "watts": 4.66, "rss_mb": 602, "done": 2, "queue": 2}
{"t": 9, "temp_c": 53.0, "mhz": 800, "cpu_pct": 73.6, "watts": 3.63, "rss_mb": 623, "done": 2, "queue": 2}
{"t": 10, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.1, "watts": 4.83, "rss_mb": 625, "done": 2, "queue": 3}
{"t": 11, "temp_c": 54.0, "mhz": 800, "cpu_pct": 58.2, "watts": 4.50, "rss_mb": 592, "done": 3, "queue": 2}
{"t": 12, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.6, "watts": 4.87, "rss_mb": 597, "done": 3, "queue": 3}
{"t": 13, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.8, "watts": 4.51, "rss_mb": 562, "done": 3, "queue": 3}
{"t": 14, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.3, "watts": 3.58, "rss_mb": 479, "done": 4, "queue": 3}
{"t": 15, "temp_c": 55.0, "mhz": 800, "cpu_pct": 64.2, "watts": 4.58, "rss_mb": 662, "done": 4, "queue": 3}
{"t": 16, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.1, "watts": 4.94, "rss_mb": 680, "done": 4, "queue": 4}
{"t": 17, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.9, "watts": 4.77, "rss_mb": 681, "done": 4, "queue": 4}
{"t": 18, "temp_c": 54.0, "mhz": 800, "cpu_pct": 62.8, "watts": 3.43, "rss_mb": 683, "done": 5, "queue": 4}
{"t": 19, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.8, "watts": 4.79, "rss_mb": 686, "done": 5, "queue": 4}
{"t": 20, "temp_c": 53.0, "mhz": 800, "cpu_pct": 64.5, "watts": 4.88, "rss_mb": 686, "done": 5, "queue": 5}
{"t": 21, "temp_c": 54.0, "mhz": 800, "cpu_pct": 62.2, "watts": 4.57, "rss_mb": 671, "done": 6, "queue": 4}
{"t": 22, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.2, "watts": 4.78, "rss_mb": 679, "done": 6, "queue": 5}
{"t": 23, "temp_c": 56.0, "mhz": 800, "cpu_pct": 88.9, "watts": 4.14, "rss_mb": 680, "done": 6, "queue": 5}
{"t": 24, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.7, "watts": 5.29, "rss_mb": 463, "done": 7, "queue": 5}
{"t": 25, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.6, "watts": 4.70, "rss_mb": 684, "done": 7, "queue": 5}
{"t": 26, "temp_c": 54.0, "mhz": 800, "cpu_pct": 70.9, "watts": 4.95, "rss_mb": 685, "done": 7, "queue": 6}
{"t": 27, "temp_c": 53.0, "mhz": 800, "cpu_pct": 68.6, "watts": 3.59, "rss_mb": 686, "done": 7, "queue": 6}
{"t": 28, "temp_c": 53.0, "mhz": 800, "cpu_pct": 62.0, "watts": 4.72, "rss_mb": 686, "done": 8, "queue": 6}
{"t": 29, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.1, "watts": 4.81, "rss_mb": 688, "done": 8, "queue": 6}
{"t": 30, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.5, "watts": 4.80, "rss_mb": 688, "done": 8, "queue": 7}
{"t": 31, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.8, "watts": 3.31, "rss_mb": 686, "done": 9, "queue": 6}
{"t": 32, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.2, "watts": 5.06, "rss_mb": 691, "done": 9, "queue": 7}
{"t": 33, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.9, "watts": 4.56, "rss_mb": 692, "done": 9, "queue": 7}
{"t": 34, "temp_c": 53.0, "mhz": 800, "cpu_pct": 55.8, "watts": 4.59, "rss_mb": 660, "done": 10, "queue": 7}
{"t": 35, "temp_c": 54.0, "mhz": 800, "cpu_pct": 75.3, "watts": 5.00, "rss_mb": 694, "done": 10, "queue": 7}
{"t": 36, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.8, "watts": 3.44, "rss_mb": 695, "done": 10, "queue": 8}
{"t": 37, "temp_c": 54.0, "mhz": 800, "cpu_pct": 58.8, "watts": 4.74, "rss_mb": 525, "done": 11, "queue": 7}
{"t": 38, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.7, "watts": 4.60, "rss_mb": 697, "done": 11, "queue": 8}
{"t": 39, "temp_c": 53.0, "mhz": 800, "cpu_pct": 66.3, "watts": 4.95, "rss_mb": 697, "done": 11, "queue": 8}
{"t": 40, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.7, "watts": 3.59, "rss_mb": 697, "done": 11, "queue": 9}
{"t": 41, "temp_c": 54.0, "mhz": 800, "cpu_pct": 61.7, "watts": 4.93, "rss_mb": 697, "done": 12, "queue": 8}
{"t": 42, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.2, "watts": 4.77, "rss_mb": 698, "done": 12, "queue": 9}
{"t": 43, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.1, "watts": 4.75, "rss_mb": 698, "done": 12, "queue": 9}
{"t": 44, "temp_c": 54.0, "mhz": 800, "cpu_pct": 58.8, "watts": 3.32, "rss_mb": 684, "done": 13, "queue": 9}
{"t": 45, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.8, "watts": 5.05, "rss_mb": 699, "done": 13, "queue": 9}
{"t": 46, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.6, "watts": 4.62, "rss_mb": 699, "done": 13, "queue": 10}
{"t": 47, "temp_c": 53.0, "mhz": 801, "cpu_pct": 59.1, "watts": 4.68, "rss_mb": 652, "done": 14, "queue": 9}
{"t": 48, "temp_c": 54.0, "mhz": 800, "cpu_pct": 74.1, "watts": 3.62, "rss_mb": 700, "done": 14, "queue": 10}
{"t": 49, "temp_c": 53.0, "mhz": 800, "cpu_pct": 65.7, "watts": 4.80, "rss_mb": 700, "done": 14, "queue": 10}
{"t": 50, "temp_c": 54.0, "mhz": 800, "cpu_pct": 57.0, "watts": 4.76, "rss_mb": 493, "done": 15, "queue": 10}
{"t": 51, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.3, "watts": 4.57, "rss_mb": 701, "done": 15, "queue": 10}
{"t": 52, "temp_c": 54.0, "mhz": 800, "cpu_pct": 69.4, "watts": 3.77, "rss_mb": 702, "done": 15, "queue": 11}
{"t": 53, "temp_c": 54.0, "mhz": 800, "cpu_pct": 70.0, "watts": 4.80, "rss_mb": 702, "done": 15, "queue": 11}
{"t": 54, "temp_c": 55.0, "mhz": 800, "cpu_pct": 75.5, "watts": 5.33, "rss_mb": 688, "done": 16, "queue": 11}
{"t": 55, "temp_c": 54.0, "mhz": 800, "cpu_pct": 87.9, "watts": 5.57, "rss_mb": 704, "done": 16, "queue": 11}
{"t": 56, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.1, "watts": 3.65, "rss_mb": 705, "done": 16, "queue": 12}
{"t": 57, "temp_c": 55.0, "mhz": 800, "cpu_pct": 62.4, "watts": 4.67, "rss_mb": 565, "done": 17, "queue": 11}
{"t": 58, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.4, "watts": 4.61, "rss_mb": 705, "done": 17, "queue": 12}
{"t": 59, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.7, "watts": 4.98, "rss_mb": 721, "done": 17, "queue": 12}
{"t": 60, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.6, "watts": 3.54, "rss_mb": 721, "done": 17, "queue": 13}
{"t": 61, "temp_c": 53.0, "mhz": 800, "cpu_pct": 61.2, "watts": 4.78, "rss_mb": 705, "done": 18, "queue": 12}
{"t": 62, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.4, "watts": 4.73, "rss_mb": 706, "done": 18, "queue": 13}
{"t": 63, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.2, "watts": 4.85, "rss_mb": 706, "done": 18, "queue": 13}
{"t": 64, "temp_c": 54.0, "mhz": 800, "cpu_pct": 63.3, "watts": 4.63, "rss_mb": 699, "done": 19, "queue": 13}
{"t": 65, "temp_c": 53.0, "mhz": 800, "cpu_pct": 66.6, "watts": 3.70, "rss_mb": 723, "done": 19, "queue": 13}
{"t": 66, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.6, "watts": 4.75, "rss_mb": 723, "done": 19, "queue": 14}
{"t": 67, "temp_c": 54.0, "mhz": 800, "cpu_pct": 55.8, "watts": 4.63, "rss_mb": 666, "done": 20, "queue": 13}
{"t": 68, "temp_c": 55.0, "mhz": 800, "cpu_pct": 74.7, "watts": 4.97, "rss_mb": 723, "done": 20, "queue": 14}
{"t": 69, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.1, "watts": 3.47, "rss_mb": 723, "done": 20, "queue": 14}
{"t": 70, "temp_c": 54.0, "mhz": 800, "cpu_pct": 55.6, "watts": 4.73, "rss_mb": 493, "done": 21, "queue": 14}
{"t": 71, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.5, "watts": 4.70, "rss_mb": 724, "done": 21, "queue": 14}
{"t": 72, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.2, "watts": 4.94, "rss_mb": 724, "done": 21, "queue": 15}
{"t": 73, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.4, "watts": 3.57, "rss_mb": 724, "done": 21, "queue": 15}
{"t": 74, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.7, "watts": 4.61, "rss_mb": 724, "done": 22, "queue": 15}
{"t": 75, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.1, "watts": 4.77, "rss_mb": 724, "done": 22, "queue": 15}
{"t": 76, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.8, "watts": 4.95, "rss_mb": 724, "done": 22, "queue": 16}
{"t": 77, "temp_c": 54.0, "mhz": 800, "cpu_pct": 59.6, "watts": 4.58, "rss_mb": 679, "done": 23, "queue": 15}
{"t": 78, "temp_c": 53.0, "mhz": 800, "cpu_pct": 71.2, "watts": 3.69, "rss_mb": 708, "done": 23, "queue": 16}
{"t": 79, "temp_c": 55.0, "mhz": 800, "cpu_pct": 66.2, "watts": 4.74, "rss_mb": 709, "done": 23, "queue": 16}
{"t": 80, "temp_c": 54.0, "mhz": 800, "cpu_pct": 60.3, "watts": 4.70, "rss_mb": 649, "done": 24, "queue": 16}
{"t": 81, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.8, "watts": 4.91, "rss_mb": 709, "done": 24, "queue": 16}
{"t": 82, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.5, "watts": 3.47, "rss_mb": 709, "done": 24, "queue": 17}
{"t": 83, "temp_c": 54.0, "mhz": 800, "cpu_pct": 57.4, "watts": 4.79, "rss_mb": 496, "done": 25, "queue": 16}
{"t": 84, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.7, "watts": 4.70, "rss_mb": 709, "done": 25, "queue": 17}
{"t": 85, "temp_c": 54.0, "mhz": 800, "cpu_pct": 83.2, "watts": 5.34, "rss_mb": 726, "done": 25, "queue": 17}
{"t": 86, "temp_c": 54.0, "mhz": 800, "cpu_pct": 83.3, "watts": 4.00, "rss_mb": 726, "done": 25, "queue": 18}
{"t": 87, "temp_c": 54.0, "mhz": 800, "cpu_pct": 57.5, "watts": 4.54, "rss_mb": 682, "done": 26, "queue": 17}
{"t": 88, "temp_c": 55.0, "mhz": 800, "cpu_pct": 70.7, "watts": 5.03, "rss_mb": 710, "done": 26, "queue": 18}
{"t": 89, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.8, "watts": 4.69, "rss_mb": 710, "done": 26, "queue": 18}
{"t": 90, "temp_c": 54.0, "mhz": 800, "cpu_pct": 56.5, "watts": 3.47, "rss_mb": 664, "done": 27, "queue": 18}
{"t": 91, "temp_c": 55.0, "mhz": 800, "cpu_pct": 72.8, "watts": 4.82, "rss_mb": 711, "done": 27, "queue": 18}
{"t": 92, "temp_c": 53.0, "mhz": 800, "cpu_pct": 66.3, "watts": 4.85, "rss_mb": 711, "done": 27, "queue": 19}
{"t": 93, "temp_c": 54.0, "mhz": 800, "cpu_pct": 58.0, "watts": 4.62, "rss_mb": 522, "done": 28, "queue": 18}
{"t": 94, "temp_c": 54.0, "mhz": 800, "cpu_pct": 64.1, "watts": 4.70, "rss_mb": 711, "done": 28, "queue": 19}
{"t": 95, "temp_c": 53.0, "mhz": 800, "cpu_pct": 72.4, "watts": 3.76, "rss_mb": 711, "done": 28, "queue": 19}
{"t": 96, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.8, "watts": 4.74, "rss_mb": 711, "done": 28, "queue": 20}
{"t": 97, "temp_c": 53.0, "mhz": 800, "cpu_pct": 60.2, "watts": 4.78, "rss_mb": 711, "done": 29, "queue": 19}
{"t": 98, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.0, "watts": 4.79, "rss_mb": 712, "done": 29, "queue": 20}
{"t": 99, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.0, "watts": 3.54, "rss_mb": 712, "done": 29, "queue": 20}
{"t": 100, "temp_c": 54.0, "mhz": 800, "cpu_pct": 60.4, "watts": 4.52, "rss_mb": 691, "done": 30, "queue": 20}
{"t": 101, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.9, "watts": 5.11, "rss_mb": 712, "done": 30, "queue": 20}
{"t": 102, "temp_c": 54.0, "mhz": 800, "cpu_pct": 67.2, "watts": 4.64, "rss_mb": 713, "done": 30, "queue": 21}
{"t": 103, "temp_c": 53.0, "mhz": 800, "cpu_pct": 56.9, "watts": 3.45, "rss_mb": 662, "done": 31, "queue": 20}
{"t": 104, "temp_c": 54.0, "mhz": 800, "cpu_pct": 72.9, "watts": 4.85, "rss_mb": 713, "done": 31, "queue": 21}
{"t": 105, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.9, "watts": 4.87, "rss_mb": 713, "done": 31, "queue": 21}
{"t": 106, "temp_c": 54.0, "mhz": 800, "cpu_pct": 55.9, "watts": 4.62, "rss_mb": 495, "done": 32, "queue": 21}
{"t": 107, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.8, "watts": 3.75, "rss_mb": 713, "done": 32, "queue": 21}
{"t": 108, "temp_c": 54.0, "mhz": 800, "cpu_pct": 71.8, "watts": 4.72, "rss_mb": 713, "done": 32, "queue": 22}
{"t": 109, "temp_c": 54.0, "mhz": 800, "cpu_pct": 66.7, "watts": 4.71, "rss_mb": 713, "done": 32, "queue": 22}
{"t": 110, "temp_c": 55.0, "mhz": 800, "cpu_pct": 63.0, "watts": 4.88, "rss_mb": 713, "done": 33, "queue": 22}
{"t": 111, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.5, "watts": 4.77, "rss_mb": 713, "done": 33, "queue": 22}
{"t": 112, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.7, "watts": 3.55, "rss_mb": 713, "done": 33, "queue": 23}
{"t": 113, "temp_c": 55.0, "mhz": 800, "cpu_pct": 61.8, "watts": 4.61, "rss_mb": 685, "done": 34, "queue": 22}
{"t": 114, "temp_c": 54.0, "mhz": 800, "cpu_pct": 72.0, "watts": 5.02, "rss_mb": 729, "done": 34, "queue": 23}
{"t": 115, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.8, "watts": 4.65, "rss_mb": 729, "done": 34, "queue": 23}
{"t": 116, "temp_c": 54.0, "mhz": 800, "cpu_pct": 63.8, "watts": 3.50, "rss_mb": 634, "done": 35, "queue": 23}
{"t": 117, "temp_c": 55.0, "mhz": 800, "cpu_pct": 85.3, "watts": 5.43, "rss_mb": 714, "done": 35, "queue": 23}
{"t": 118, "temp_c": 54.0, "mhz": 800, "cpu_pct": 68.2, "watts": 5.08, "rss_mb": 714, "done": 35, "queue": 24}
{"t": 119, "temp_c": 53.0, "mhz": 800, "cpu_pct": 69.2, "watts": 4.74, "rss_mb": 714, "done": 35, "queue": 24}
{"t": 120, "temp_c": 54.0, "mhz": 800, "cpu_pct": 59.9, "watts": 4.59, "rss_mb": 714, "done": 36, "queue": 23}
{"t": 121, "temp_c": 54.0, "mhz": 800, "cpu_pct": 65.3, "watts": 3.50, "rss_mb": 714, "done": 36, "queue": 23}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 122.8, "frames": 37, "frames_per_s": 0.301, "job_ms_p50": 3246, "latency_ms_p50": 27515, "latency_ms_p95": 47924, "latency_ms_max": 50735, "max_queue": 24, "backlog_at_end": 23, "cpu_s_per_frame": 7.634, "peak_rss_mb": 729, "temp_c_mean": 53.9, "temp_c_max": 56.0, "mhz_mean": 800, "mhz_min": 800, "cpu_pct_mean": 66.3, "watts_mean": 4.51}

View File

@ -0,0 +1,122 @@
{"t": 1, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.3, "watts": -1.00, "rss_mb": 505, "done": 0, "queue": 0}
{"t": 2, "temp_c": 50.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 562, "done": 0, "queue": 1}
{"t": 3, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.2, "watts": -1.00, "rss_mb": 541, "done": 0, "queue": 1}
{"t": 4, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.0, "watts": -1.00, "rss_mb": 534, "done": 0, "queue": 2}
{"t": 5, "temp_c": 50.0, "mhz": 800, "cpu_pct": 91.5, "watts": -1.00, "rss_mb": 336, "done": 1, "queue": 1}
{"t": 6, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.7, "watts": -1.00, "rss_mb": 591, "done": 1, "queue": 2}
{"t": 7, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.5, "watts": -1.00, "rss_mb": 612, "done": 1, "queue": 2}
{"t": 8, "temp_c": 51.0, "mhz": 800, "cpu_pct": 95.2, "watts": -1.00, "rss_mb": 615, "done": 1, "queue": 3}
{"t": 9, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.3, "watts": -1.00, "rss_mb": 616, "done": 1, "queue": 3}
{"t": 10, "temp_c": 50.0, "mhz": 800, "cpu_pct": 85.8, "watts": -1.00, "rss_mb": 504, "done": 2, "queue": 3}
{"t": 11, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.0, "watts": -1.00, "rss_mb": 623, "done": 2, "queue": 3}
{"t": 12, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.8, "watts": -1.00, "rss_mb": 642, "done": 2, "queue": 4}
{"t": 13, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.6, "watts": -1.00, "rss_mb": 643, "done": 2, "queue": 4}
{"t": 14, "temp_c": 52.0, "mhz": 800, "cpu_pct": 96.5, "watts": -1.00, "rss_mb": 644, "done": 2, "queue": 5}
{"t": 15, "temp_c": 51.0, "mhz": 800, "cpu_pct": 99.5, "watts": -1.00, "rss_mb": 515, "done": 3, "queue": 4}
{"t": 16, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 562, "done": 3, "queue": 5}
{"t": 17, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.3, "watts": -1.00, "rss_mb": 572, "done": 3, "queue": 5}
{"t": 18, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 591, "done": 3, "queue": 6}
{"t": 19, "temp_c": 51.0, "mhz": 800, "cpu_pct": 90.8, "watts": -1.00, "rss_mb": 499, "done": 3, "queue": 6}
{"t": 20, "temp_c": 51.0, "mhz": 800, "cpu_pct": 93.5, "watts": -1.00, "rss_mb": 675, "done": 4, "queue": 6}
{"t": 21, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 695, "done": 4, "queue": 6}
{"t": 22, "temp_c": 51.0, "mhz": 800, "cpu_pct": 90.0, "watts": -1.00, "rss_mb": 697, "done": 4, "queue": 7}
{"t": 23, "temp_c": 51.0, "mhz": 800, "cpu_pct": 92.6, "watts": -1.00, "rss_mb": 697, "done": 4, "queue": 7}
{"t": 24, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.8, "watts": -1.00, "rss_mb": 649, "done": 5, "queue": 7}
{"t": 25, "temp_c": 51.0, "mhz": 800, "cpu_pct": 95.0, "watts": -1.00, "rss_mb": 700, "done": 5, "queue": 7}
{"t": 26, "temp_c": 51.0, "mhz": 800, "cpu_pct": 93.8, "watts": -1.00, "rss_mb": 702, "done": 5, "queue": 8}
{"t": 27, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.5, "watts": -1.00, "rss_mb": 703, "done": 5, "queue": 8}
{"t": 28, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.3, "watts": -1.00, "rss_mb": 533, "done": 6, "queue": 8}
{"t": 29, "temp_c": 51.0, "mhz": 800, "cpu_pct": 90.5, "watts": -1.00, "rss_mb": 691, "done": 6, "queue": 8}
{"t": 30, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 693, "done": 6, "queue": 9}
{"t": 31, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 694, "done": 6, "queue": 9}
{"t": 32, "temp_c": 51.0, "mhz": 800, "cpu_pct": 90.5, "watts": -1.00, "rss_mb": 694, "done": 6, "queue": 10}
{"t": 33, "temp_c": 51.0, "mhz": 800, "cpu_pct": 91.0, "watts": -1.00, "rss_mb": 682, "done": 7, "queue": 9}
{"t": 34, "temp_c": 52.0, "mhz": 800, "cpu_pct": 95.3, "watts": -1.00, "rss_mb": 695, "done": 7, "queue": 10}
{"t": 35, "temp_c": 51.0, "mhz": 800, "cpu_pct": 89.8, "watts": -1.00, "rss_mb": 697, "done": 7, "queue": 10}
{"t": 36, "temp_c": 51.0, "mhz": 800, "cpu_pct": 93.7, "watts": -1.00, "rss_mb": 697, "done": 7, "queue": 11}
{"t": 37, "temp_c": 52.0, "mhz": 800, "cpu_pct": 88.8, "watts": -1.00, "rss_mb": 665, "done": 8, "queue": 10}
{"t": 38, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 701, "done": 8, "queue": 11}
{"t": 39, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 703, "done": 8, "queue": 11}
{"t": 40, "temp_c": 53.0, "mhz": 800, "cpu_pct": 91.5, "watts": -1.00, "rss_mb": 703, "done": 8, "queue": 12}
{"t": 41, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.3, "watts": -1.00, "rss_mb": 509, "done": 9, "queue": 11}
{"t": 42, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.0, "watts": -1.00, "rss_mb": 703, "done": 9, "queue": 12}
{"t": 43, "temp_c": 51.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 704, "done": 9, "queue": 12}
{"t": 44, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 705, "done": 9, "queue": 13}
{"t": 45, "temp_c": 52.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 705, "done": 9, "queue": 13}
{"t": 46, "temp_c": 52.0, "mhz": 800, "cpu_pct": 99.0, "watts": -1.00, "rss_mb": 658, "done": 10, "queue": 13}
{"t": 47, "temp_c": 52.0, "mhz": 800, "cpu_pct": 95.0, "watts": -1.00, "rss_mb": 707, "done": 10, "queue": 13}
{"t": 48, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.7, "watts": -1.00, "rss_mb": 708, "done": 10, "queue": 14}
{"t": 49, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 708, "done": 10, "queue": 14}
{"t": 50, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 584, "done": 10, "queue": 15}
{"t": 51, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.3, "watts": -1.00, "rss_mb": 708, "done": 11, "queue": 14}
{"t": 52, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.8, "watts": -1.00, "rss_mb": 709, "done": 11, "queue": 15}
{"t": 53, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.5, "watts": -1.00, "rss_mb": 710, "done": 11, "queue": 15}
{"t": 54, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.5, "watts": -1.00, "rss_mb": 710, "done": 11, "queue": 16}
{"t": 55, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.3, "watts": -1.00, "rss_mb": 678, "done": 12, "queue": 15}
{"t": 56, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 717, "done": 12, "queue": 16}
{"t": 57, "temp_c": 52.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 718, "done": 12, "queue": 16}
{"t": 58, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 718, "done": 12, "queue": 17}
{"t": 59, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.8, "watts": -1.00, "rss_mb": 588, "done": 13, "queue": 16}
{"t": 60, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.5, "watts": -1.00, "rss_mb": 724, "done": 13, "queue": 17}
{"t": 61, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.0, "watts": -1.00, "rss_mb": 726, "done": 13, "queue": 17}
{"t": 62, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.2, "watts": -1.00, "rss_mb": 726, "done": 13, "queue": 18}
{"t": 63, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.5, "watts": -1.00, "rss_mb": 726, "done": 13, "queue": 18}
{"t": 64, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 726, "done": 14, "queue": 18}
{"t": 65, "temp_c": 52.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 728, "done": 14, "queue": 18}
{"t": 66, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.0, "watts": -1.00, "rss_mb": 729, "done": 14, "queue": 19}
{"t": 67, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.5, "watts": -1.00, "rss_mb": 729, "done": 14, "queue": 19}
{"t": 68, "temp_c": 52.0, "mhz": 800, "cpu_pct": 88.6, "watts": -1.00, "rss_mb": 697, "done": 15, "queue": 19}
{"t": 69, "temp_c": 54.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 730, "done": 15, "queue": 19}
{"t": 70, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.8, "watts": -1.00, "rss_mb": 730, "done": 15, "queue": 20}
{"t": 71, "temp_c": 53.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 730, "done": 15, "queue": 20}
{"t": 72, "temp_c": 53.0, "mhz": 800, "cpu_pct": 89.3, "watts": -1.00, "rss_mb": 527, "done": 16, "queue": 20}
{"t": 73, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 733, "done": 16, "queue": 20}
{"t": 74, "temp_c": 52.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 734, "done": 16, "queue": 21}
{"t": 75, "temp_c": 53.0, "mhz": 800, "cpu_pct": 95.3, "watts": -1.00, "rss_mb": 735, "done": 16, "queue": 21}
{"t": 76, "temp_c": 52.0, "mhz": 800, "cpu_pct": 92.5, "watts": -1.00, "rss_mb": 735, "done": 16, "queue": 22}
{"t": 77, "temp_c": 53.0, "mhz": 800, "cpu_pct": 97.8, "watts": -1.00, "rss_mb": 703, "done": 17, "queue": 21}
{"t": 78, "temp_c": 53.0, "mhz": 800, "cpu_pct": 99.8, "watts": -1.00, "rss_mb": 735, "done": 17, "queue": 22}
{"t": 79, "temp_c": 53.0, "mhz": 800, "cpu_pct": 95.0, "watts": -1.00, "rss_mb": 735, "done": 17, "queue": 22}
{"t": 80, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 735, "done": 17, "queue": 23}
{"t": 81, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 735, "done": 17, "queue": 23}
{"t": 82, "temp_c": 53.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 751, "done": 18, "queue": 23}
{"t": 83, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.8, "watts": -1.00, "rss_mb": 752, "done": 18, "queue": 23}
{"t": 84, "temp_c": 52.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 752, "done": 18, "queue": 24}
{"t": 85, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.5, "watts": -1.00, "rss_mb": 752, "done": 18, "queue": 24}
{"t": 86, "temp_c": 52.0, "mhz": 800, "cpu_pct": 89.5, "watts": -1.00, "rss_mb": 696, "done": 19, "queue": 24}
{"t": 87, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.0, "watts": -1.00, "rss_mb": 737, "done": 19, "queue": 24}
{"t": 88, "temp_c": 52.0, "mhz": 800, "cpu_pct": 95.2, "watts": -1.00, "rss_mb": 737, "done": 19, "queue": 25}
{"t": 89, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.2, "watts": -1.00, "rss_mb": 737, "done": 19, "queue": 25}
{"t": 90, "temp_c": 53.0, "mhz": 800, "cpu_pct": 89.0, "watts": -1.00, "rss_mb": 519, "done": 20, "queue": 25}
{"t": 91, "temp_c": 53.0, "mhz": 800, "cpu_pct": 93.5, "watts": -1.00, "rss_mb": 737, "done": 20, "queue": 25}
{"t": 92, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 737, "done": 20, "queue": 26}
{"t": 93, "temp_c": 53.0, "mhz": 800, "cpu_pct": 96.3, "watts": -1.00, "rss_mb": 738, "done": 20, "queue": 26}
{"t": 94, "temp_c": 52.0, "mhz": 800, "cpu_pct": 91.8, "watts": -1.00, "rss_mb": 738, "done": 20, "queue": 27}
{"t": 95, "temp_c": 53.0, "mhz": 800, "cpu_pct": 87.5, "watts": -1.00, "rss_mb": 705, "done": 21, "queue": 26}
{"t": 96, "temp_c": 52.0, "mhz": 800, "cpu_pct": 93.3, "watts": -1.00, "rss_mb": 754, "done": 21, "queue": 27}
{"t": 97, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 754, "done": 21, "queue": 27}
{"t": 98, "temp_c": 54.0, "mhz": 800, "cpu_pct": 94.3, "watts": -1.00, "rss_mb": 754, "done": 21, "queue": 28}
{"t": 99, "temp_c": 53.0, "mhz": 800, "cpu_pct": 88.7, "watts": -1.00, "rss_mb": 629, "done": 22, "queue": 27}
{"t": 100, "temp_c": 54.0, "mhz": 800, "cpu_pct": 87.2, "watts": -1.00, "rss_mb": 755, "done": 22, "queue": 28}
{"t": 101, "temp_c": 53.0, "mhz": 800, "cpu_pct": 95.3, "watts": -1.00, "rss_mb": 756, "done": 22, "queue": 28}
{"t": 102, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.0, "watts": -1.00, "rss_mb": 757, "done": 22, "queue": 29}
{"t": 103, "temp_c": 52.0, "mhz": 800, "cpu_pct": 90.7, "watts": -1.00, "rss_mb": 616, "done": 22, "queue": 29}
{"t": 104, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.7, "watts": -1.00, "rss_mb": 757, "done": 23, "queue": 29}
{"t": 105, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.3, "watts": -1.00, "rss_mb": 757, "done": 23, "queue": 29}
{"t": 106, "temp_c": 53.0, "mhz": 800, "cpu_pct": 94.5, "watts": -1.00, "rss_mb": 758, "done": 23, "queue": 30}
{"t": 107, "temp_c": 53.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 758, "done": 23, "queue": 30}
{"t": 108, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.2, "watts": -1.00, "rss_mb": 653, "done": 24, "queue": 30}
{"t": 109, "temp_c": 53.0, "mhz": 800, "cpu_pct": 99.5, "watts": -1.00, "rss_mb": 758, "done": 24, "queue": 30}
{"t": 110, "temp_c": 53.0, "mhz": 800, "cpu_pct": 97.0, "watts": -1.00, "rss_mb": 758, "done": 24, "queue": 31}
{"t": 111, "temp_c": 53.0, "mhz": 800, "cpu_pct": 95.2, "watts": -1.00, "rss_mb": 758, "done": 24, "queue": 31}
{"t": 112, "temp_c": 53.0, "mhz": 800, "cpu_pct": 91.2, "watts": -1.00, "rss_mb": 758, "done": 24, "queue": 32}
{"t": 113, "temp_c": 53.0, "mhz": 800, "cpu_pct": 90.2, "watts": -1.00, "rss_mb": 743, "done": 25, "queue": 31}
{"t": 114, "temp_c": 53.0, "mhz": 800, "cpu_pct": 93.0, "watts": -1.00, "rss_mb": 759, "done": 25, "queue": 32}
{"t": 115, "temp_c": 54.0, "mhz": 800, "cpu_pct": 94.8, "watts": -1.00, "rss_mb": 760, "done": 25, "queue": 32}
{"t": 116, "temp_c": 53.0, "mhz": 800, "cpu_pct": 92.2, "watts": -1.00, "rss_mb": 760, "done": 25, "queue": 33}
{"t": 117, "temp_c": 54.0, "mhz": 800, "cpu_pct": 89.0, "watts": -1.00, "rss_mb": 712, "done": 26, "queue": 32}
{"t": 118, "temp_c": 54.0, "mhz": 800, "cpu_pct": 93.3, "watts": -1.00, "rss_mb": 762, "done": 26, "queue": 33}
{"t": 119, "temp_c": 53.0, "mhz": 800, "cpu_pct": 95.0, "watts": -1.00, "rss_mb": 762, "done": 26, "queue": 33}
{"t": 120, "temp_c": 54.0, "mhz": 800, "cpu_pct": 95.0, "watts": -1.00, "rss_mb": 762, "done": 26, "queue": 33}
{"t": 121, "temp_c": 52.0, "mhz": 742, "cpu_pct": 83.7, "watts": -1.00, "rss_mb": 372, "done": 27, "queue": 33}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 121.3, "frames": 27, "frames_per_s": 0.223, "job_ms_p50": 4410, "latency_ms_p50": 37333, "latency_ms_p95": 66872, "latency_ms_max": 69294, "max_queue": 33, "backlog_at_end": 33, "cpu_s_per_frame": 6.880, "peak_rss_mb": 761, "temp_c_mean": 52.1, "temp_c_max": 54.0, "mhz_mean": 800, "mhz_min": 742, "cpu_pct_mean": 92.7, "watts_mean": -1.00}

View File

@ -0,0 +1,229 @@
16:56:32.187 [INFO ] hardware: /etc/fire_gimbal_control/hardware.ini board lattepanda-3-delta axes yaw, pitch
16:56:32.188 [INFO ] logging to /home/ggs/.local/share/fire_gimbal_control/logs/fgc_20261008-165632.000.log (level debug, 10 x 20 MB)
16:56:32.188 [INFO ] Loaded config: /home/ggs/.config/fire_gimbal_control/config.ini
16:56:32.188 [INFO ] Single-instance lock held: /run/user/1000/fire_gimbal_control.lock
16:56:32.188 [INFO ] wire trace enabled: mqtt
16:56:32.246 [INFO ] camera 'rgb': 3 profile(s) from /home/ggs/.config/fire_gimbal_control/camera_profiles/rgb; active 'standard'
16:56:32.247 [INFO ] IMU frames: reference NWU -> NED; mount x 180 y 0 z 0 (deg, Z-X-Y); readings are in the gimbal frame
16:56:32.258 [INFO ] MQTT (re)connected; subscribing
16:56:32.258 [INFO ] MQTT connected to tcp://127.0.0.1:1883 as 'dev'
16:56:32.258 [INFO ] capture output dir: /home/ggs/projects/fwt_2a/images
16:56:32.258 [INFO ] image saving: 1 worker(s) x 4 thread(s), up to 4 frame(s) waiting
16:56:32.258 [INFO ] scan grid: 15 waypoints
16:56:32.258 [INFO ] keep rule: fixed wait of 1000 ms after standstill
16:56:32.259 [INFO ] CPU guard: pictures slow down at 85 °C or when the CPU throttles, and pause at 92 °C (resume at 75 °C)
16:56:32.259 [INFO ] yaw motor current: run 756 mA (GLOBALSCALER 80, IRUN 31), homing 756 mA (IRUN 31) -- R_sense 0.075 ohm from hardware.ini [Motor.yaw]
16:56:32.259 [INFO ] pitch motor current: run 472 mA (GLOBALSCALER 50, IRUN 31), homing 472 mA (IRUN 31) -- R_sense 0.075 ohm from hardware.ini [Motor.pitch]
16:56:32.259 [INFO ] Serial controller started on /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00 @ 115200
16:56:32.941 [INFO ] MTi config: MTi-28A53G35 fw 2.6.1 scenario 7 (machine_nomagfield) @ 100 Hz, mode 0x0007 settings 0x00000005
16:56:33.002 [INFO ] MTi IMU started on /dev/ttyS0 @ 115200
16:56:34.025 [INFO ] Opened camera 'rgb' DEV_1AB22C049CD1 (1800 U-1240c)
16:56:43.189 [ERROR] camera: applying profile 'standard' from /home/ggs/.config/fire_gimbal_control/camera_profiles/rgb/standard.xml failed (error -39)
16:56:43.190 [INFO ] hardware: declared assembly confirmed (yaw, pitch)
16:56:43.190 [MQTT] PUB GGS/FWT/dev/StatusCode 2
16:56:43.282 [INFO ] Running gimbal init sequence (enable + home)
16:56:43.282 [INFO ] drivetrain re-sent to axis Y: 12 values from hardware.ini
16:56:43.282 [INFO ] drivetrain re-sent to axis P: 12 values from hardware.ini
16:57:26.384 [INFO ] Gimbal homed (both axes READY)
16:57:26.441 [INFO ] firmware dump:
DUMP BEGIN build=8efa63b uptime=84894318 mcusr=0x0 reset=none free_ram=1607
DUMP Y state=3 hsub=8 enabled=1 lim_neg=408820 lim_pos=591181 hold_target=500000 speed=5908 center=500000 soft_margin=5000 gs=80 irun=31 irun_home=31 steps_per_rev=177000 enc_dev=1500 homing_speed=12362 center_speed=25753 sw_mode=2208 cfg_rx=0x1FFE limp=0 hold_corr=4 hold_peak=142693 hold_cumul=2969 has_encoder=1 cs=8 en=7
DUMP Y TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x3000002C TSTEP=0x000FFFFF RAMPMODE=0x00000000 XACTUAL=0x0007A120 VACTUAL=0x00000000 XTARGET=0x0007A120 SW_MODE=0x000008A0 RAMP_STAT=0x00000680 X_ENC=0x0007A117 ENC_STATUS=0x00000000 CHOPCONF=0x00410043 DRV_STATUS=0x801F4000 PWM_SCALE=0x00000040 PWM_AUTO=0x006C003E
DUMP P state=3 hsub=8 enabled=1 lim_neg=213898 lim_pos=786103 hold_target=500000 speed=157403 center=500000 soft_margin=5000 gs=50 irun=31 irun_home=31 steps_per_rev=500000 enc_dev=1500 homing_speed=170978 center_speed=68470 sw_mode=2211 cfg_rx=0x1FFE limp=0 hold_corr=2 hold_peak=245129 hold_cumul=615 has_encoder=1 cs=1 en=0
DUMP P TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x30000008 TSTEP=0x000FFFFF RAMPMODE=0x00000000 XACTUAL=0x0007A120 VACTUAL=0x00000000 XTARGET=0x0007A120 SW_MODE=0x000008A3 RAMP_STAT=0x00001688 X_ENC=0x0007A113 ENC_STATUS=0x00000000 CHOPCONF=0x00410043 DRV_STATUS=0x80084000 PWM_SCALE=0x0000000F PWM_AUTO=0x001E0038
DUMP ENV t=21 h=52 ok=1 fan=0 rpm=0 dht=9 fan_pwm=10 fan_tach=11
DUMP END
16:57:26.500 [INFO ] yaw counts_per_deg 528.254 agrees with the homed travel (182361 counts / 345 deg = 528.583); leaving the calibration alone
16:57:26.500 [INFO ] pitch counts_per_deg -14072.9 agrees with the homed travel (572205 counts / 40.5 deg = -14128.5); leaving the calibration alone
16:57:26.500 [INFO ] homed yaw travel: 112.4..438.684 deg (headings 112.4..78.6844 true), centre 500000 = 275.541 deg
16:57:26.500 [INFO ] homed pitch travel: -27.3943..12.5553 deg, centre 500000 = -7.41947 deg
16:57:26.500 [WARN ] 3 of 15 scan waypoints lie outside the reachable yaw arc (112.4..78.6844 true) and will be skipped
16:57:26.500 [INFO ] yaw speed: 7.99943 deg/s (VMAX 5908) -- 528.254 counts/deg
16:57:26.500 [INFO ] pitch speed: 8.00003 deg/s (VMAX 157403) -- -14072.9 counts/deg
16:57:26.500 [INFO ] Gimbal init finished
16:57:26.509 [INFO ] mode auto
16:57:26.509 [MQTT] PUB GGS/FWT/dev/StatusCode 0
16:57:26.509 [INFO ] Entering control loop (type 'exit' to quit, or Ctrl-C)
16:57:26.509 [INFO ] scan move 1/15: yaw 0 pitch 10 (deg)
16:57:26.511 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"normal","reason":"","guard":true,"cpu_c":51.0,"cpu_mhz":2800,"cpu_busy_pct":null,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":0,"save_ms_mean":null,"save_ms_max":null,"held_s":0,"interval_s":5.0,"time":1791471446511}
16:57:38.211 [INFO ] captured waypoint 1/15
16:57:38.222 [INFO ] scan move 2/15: yaw 45 pitch 10 (deg)
16:57:39.022 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":86, "time":1791471458131 }
16:57:45.060 [WARN ] waypoint 2/15: 6.8 s since the last picture, over the 5.0 s interval (move 5.8 s, exposure 1.0 s)
16:57:45.084 [INFO ] captured waypoint 2/15
16:57:45.095 [WARN ] scan waypoint 3/15 heading 90 deg is outside the reachable yaw arc; skipping it (further unreachable waypoints are skipped silently)
16:57:45.095 [INFO ] scan move 4/15: yaw 135 pitch 10 (deg)
16:57:45.692 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":450, "pit":86, "time":1791471464990 }
16:58:19.746 [WARN ] waypoint 4/15: 34.7 s since the last picture, over the 5.0 s interval (move 33.6 s, exposure 1.0 s)
16:58:19.760 [INFO ] captured waypoint 4/15
16:58:19.770 [INFO ] scan move 5/15: yaw 180 pitch 10 (deg)
16:58:20.384 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1353, "pit":93, "time":1791471499666 }
16:58:26.626 [WARN ] waypoint 5/15: 6.9 s since the last picture, over the 5.0 s interval (move 5.8 s, exposure 1.0 s)
16:58:26.637 [INFO ] captured waypoint 5/15
16:58:26.639 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"normal","reason":"","guard":true,"cpu_c":53.0,"cpu_mhz":1077,"cpu_busy_pct":15,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":681,"save_ms_max":812,"held_s":0,"interval_s":5.0,"time":1791471506639}
16:58:26.650 [INFO ] scan move 6/15: yaw 0 pitch 5 (deg)
16:58:27.239 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":98, "time":1791471506525 }
16:58:50.079 [WARN ] waypoint 6/15: 23.4 s since the last picture, over the 5.0 s interval (move 22.4 s, exposure 1.0 s)
16:58:50.087 [INFO ] captured waypoint 6/15
16:58:50.097 [INFO ] scan move 7/15: yaw 45 pitch 5 (deg)
16:58:50.779 [INFO ] saving: 3 pictures in the last minute, 639 ms each on average (longest 692 ms)
16:58:50.779 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":36, "time":1791471530055 }
16:58:56.963 [WARN ] waypoint 7/15: 6.9 s since the last picture, over the 5.0 s interval (move 5.9 s, exposure 1.0 s)
16:58:56.977 [INFO ] captured waypoint 7/15
16:58:56.987 [INFO ] scan move 9/15: yaw 135 pitch 5 (deg)
16:58:57.604 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":36, "time":1791471536914 }
16:59:26.757 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"normal","reason":"","guard":true,"cpu_c":52.0,"cpu_mhz":778,"cpu_busy_pct":14,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":640,"save_ms_max":692,"held_s":0,"interval_s":5.0,"time":1791471566757}
16:59:31.624 [WARN ] waypoint 9/15: 34.6 s since the last picture, over the 5.0 s interval (move 33.6 s, exposure 1.0 s)
16:59:31.660 [INFO ] captured waypoint 9/15
16:59:31.670 [INFO ] scan move 10/15: yaw 180 pitch 5 (deg)
16:59:32.194 [INFO ] CPU: 52 °C (x86_pkg_temp), 2.8 GHz of 2.9 GHz, 14% busy
16:59:32.194 [INFO ] throttling: no, 0 in the last minute, 0 since start
16:59:32.194 [INFO ] scan pace: normal, one picture every 5.0 s
16:59:32.194 [INFO ] saving: 2 in the last minute, 659 ms each (longest 692 ms)
16:59:32.194 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
16:59:32.571 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1352, "pit":43, "time":1791471571589 }
16:59:36.780 [WARN ] CPU is running slow while busy (judged from its clock, not the kernel's count): 53 °C, running at 0.8 GHz of 2.9 GHz
16:59:36.780 [WARN ] CPU is slowing itself down: one picture every 10.0 s instead of 5.0 until the CPU is below 80 °C and keeping up for 2 min
16:59:36.780 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":53.0,"cpu_mhz":800,"cpu_busy_pct":68,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":743,"save_ms_max":911,"held_s":0,"interval_s":10.0,"time":1791471576780}
16:59:41.678 [INFO ] captured waypoint 10/15
16:59:41.690 [INFO ] scan move 11/15: yaw 0 pitch 15 (deg)
16:59:45.715 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":48, "time":1791471581592 }
17:00:05.030 [WARN ] waypoint 11/15: 23.4 s since the last picture, over the 10.0 s interval (move 22.3 s, exposure 1.0 s)
17:00:05.052 [INFO ] captured waypoint 11/15
17:00:05.064 [INFO ] scan move 12/15: yaw 45 pitch 15 (deg)
17:00:08.937 [INFO ] saving: 3 pictures in the last minute, 2943 ms each on average (longest 4035 ms)
17:00:08.937 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":111, "time":1791471604932 }
17:00:15.076 [INFO ] captured waypoint 12/15
17:00:15.087 [INFO ] scan move 14/15: yaw 135 pitch 15 (deg)
17:00:18.694 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":111, "time":1791471615030 }
17:00:36.922 [WARN ] CPU still slowed: clock low all minute, 53 °C, running at 0.8 GHz of 2.9 GHz
17:00:36.923 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":53.0,"cpu_mhz":800,"cpu_busy_pct":68,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":3846,"save_ms_max":4035,"held_s":0,"interval_s":10.0,"time":1791471636922}
17:00:49.639 [WARN ] waypoint 14/15: 34.6 s since the last picture, over the 10.0 s interval (move 33.5 s, exposure 1.0 s)
17:00:49.658 [INFO ] captured waypoint 14/15
17:00:49.669 [INFO ] scan move 15/15: yaw 180 pitch 15 (deg)
17:00:53.137 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1353, "pit":119, "time":1791471649611 }
17:00:59.678 [INFO ] captured waypoint 15/15
17:00:59.691 [INFO ] scan move 1/15: yaw 0 pitch 10 (deg)
17:01:03.230 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":123, "time":1791471659613 }
17:01:23.125 [INFO ] captured waypoint 1/15
17:01:23.136 [INFO ] scan move 2/15: yaw 45 pitch 10 (deg)
17:01:27.270 [INFO ] saving: 3 pictures in the last minute, 3725 ms each on average (longest 4145 ms)
17:01:27.270 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":86, "time":1791471682953 }
17:01:32.235 [INFO ] CPU: 54 °C (x86_pkg_temp), 0.8 GHz of 2.9 GHz, 69% busy
17:01:32.235 [INFO ] throttling: yes (clock), 0 in the last minute, 0 since start
17:01:32.235 [INFO ] scan pace: slowed - CPU is slowing itself down, one picture every 10.0 s
17:01:32.235 [INFO ] saving: 3 in the last minute, 3725 ms each (longest 4145 ms)
17:01:32.235 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
17:01:33.145 [INFO ] captured waypoint 2/15
17:01:33.157 [INFO ] scan move 4/15: yaw 135 pitch 10 (deg)
17:01:36.745 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":85, "time":1791471693053 }
17:01:37.060 [WARN ] CPU still slowed: clock low all minute, 55 °C, running at 0.8 GHz of 2.9 GHz
17:01:37.061 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":55.0,"cpu_mhz":800,"cpu_busy_pct":91,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":4,"save_ms_mean":3693,"save_ms_max":4145,"held_s":0,"interval_s":10.0,"time":1791471697060}
17:02:07.648 [WARN ] waypoint 4/15: 34.5 s since the last picture, over the 10.0 s interval (move 33.5 s, exposure 1.0 s)
17:02:07.666 [INFO ] captured waypoint 4/15
17:02:07.677 [INFO ] scan move 5/15: yaw 180 pitch 10 (deg)
17:02:11.187 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1353, "pit":93, "time":1791471727633 }
17:02:17.729 [INFO ] captured waypoint 5/15
17:02:17.741 [INFO ] scan move 6/15: yaw 0 pitch 5 (deg)
17:02:21.530 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":98, "time":1791471737641 }
17:02:37.221 [WARN ] CPU still slowed: clock low all minute, 54 °C, running at 0.8 GHz of 2.9 GHz
17:02:37.222 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":54.0,"cpu_mhz":800,"cpu_busy_pct":66,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":2,"save_ms_mean":3660,"save_ms_max":3799,"held_s":0,"interval_s":10.0,"time":1791471757221}
17:02:41.302 [WARN ] waypoint 6/15: 23.6 s since the last picture, over the 10.0 s interval (move 22.6 s, exposure 1.0 s)
17:02:41.322 [INFO ] captured waypoint 6/15
17:02:41.332 [INFO ] scan move 7/15: yaw 45 pitch 5 (deg)
17:02:45.136 [INFO ] saving: 3 pictures in the last minute, 3711 ms each on average (longest 3814 ms)
17:02:45.136 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":34, "time":1791471761260 }
17:02:51.339 [INFO ] captured waypoint 7/15
17:02:51.351 [INFO ] scan move 9/15: yaw 135 pitch 5 (deg)
17:02:54.779 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":36, "time":1791471771262 }
17:03:25.914 [WARN ] waypoint 9/15: 34.6 s since the last picture, over the 10.0 s interval (move 33.6 s, exposure 1.0 s)
17:03:25.932 [INFO ] captured waypoint 9/15
17:03:25.945 [INFO ] scan move 10/15: yaw 180 pitch 5 (deg)
17:03:29.611 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1352, "pit":43, "time":1791471805843 }
17:03:32.234 [INFO ] CPU: 56 °C (x86_pkg_temp), 0.8 GHz of 2.9 GHz, 72% busy
17:03:32.234 [INFO ] throttling: yes (clock), 0 in the last minute, 0 since start
17:03:32.234 [INFO ] scan pace: slowed - CPU is slowing itself down, one picture every 10.0 s
17:03:32.234 [INFO ] saving: 3 in the last minute, 3644 ms each (longest 3814 ms)
17:03:32.234 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
17:03:35.956 [INFO ] captured waypoint 10/15
17:03:35.967 [INFO ] scan move 11/15: yaw 0 pitch 15 (deg)
17:03:37.373 [WARN ] CPU still slowed: clock low all minute, 55 °C, running at 0.8 GHz of 2.9 GHz
17:03:37.374 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":55.0,"cpu_mhz":800,"cpu_busy_pct":88,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":3644,"save_ms_max":3814,"held_s":0,"interval_s":10.0,"time":1791471817373}
17:03:39.531 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":46, "time":1791471815845 }
17:03:59.314 [WARN ] waypoint 11/15: 23.4 s since the last picture, over the 10.0 s interval (move 22.3 s, exposure 1.0 s)
17:03:59.419 [INFO ] captured waypoint 11/15
17:03:59.433 [INFO ] scan move 12/15: yaw 45 pitch 15 (deg)
17:04:03.336 [INFO ] saving: 3 pictures in the last minute, 3723 ms each on average (longest 3917 ms)
17:04:03.336 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":110, "time":1791471839281 }
17:04:09.440 [INFO ] captured waypoint 12/15
17:04:09.460 [INFO ] scan move 14/15: yaw 135 pitch 15 (deg)
17:04:12.912 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":110, "time":1791471849379 }
17:04:37.498 [WARN ] CPU still slowed: clock low all minute, 55 °C, running at 0.8 GHz of 2.9 GHz
17:04:37.498 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":55.0,"cpu_mhz":800,"cpu_busy_pct":68,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":3,"save_ms_mean":3654,"save_ms_max":3917,"held_s":0,"interval_s":10.0,"time":1791471877498}
17:04:43.926 [WARN ] waypoint 14/15: 34.5 s since the last picture, over the 10.0 s interval (move 33.5 s, exposure 1.0 s)
17:04:43.945 [INFO ] captured waypoint 14/15
17:04:43.958 [INFO ] scan move 15/15: yaw 180 pitch 15 (deg)
17:04:47.588 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1353, "pit":118, "time":1791471883866 }
17:04:54.005 [INFO ] captured waypoint 15/15
17:04:54.015 [INFO ] scan move 1/15: yaw 0 pitch 10 (deg)
17:04:57.678 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":122, "time":1791471893874 }
17:05:17.599 [INFO ] captured waypoint 1/15
17:05:17.618 [INFO ] scan move 2/15: yaw 45 pitch 10 (deg)
17:05:21.659 [INFO ] saving: 3 pictures in the last minute, 3790 ms each on average (longest 4059 ms)
17:05:21.659 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":86, "time":1791471917492 }
17:05:27.622 [INFO ] captured waypoint 2/15
17:05:27.636 [INFO ] scan move 4/15: yaw 135 pitch 10 (deg)
17:05:31.188 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":85, "time":1791471927590 }
17:05:32.236 [INFO ] CPU: 55 °C (x86_pkg_temp), 0.8 GHz of 2.9 GHz, 89% busy
17:05:32.236 [INFO ] throttling: yes (clock), 0 in the last minute, 0 since start
17:05:32.236 [INFO ] scan pace: slowed - CPU is slowing itself down, one picture every 10.0 s
17:05:32.237 [INFO ] saving: 4 in the last minute, 3734 ms each (longest 4059 ms)
17:05:32.237 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
17:05:37.647 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":54.0,"cpu_mhz":800,"cpu_busy_pct":14,"throttling":true,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":4,"save_ms_mean":3734,"save_ms_max":4059,"held_s":0,"interval_s":10.0,"time":1791471937647}
17:06:02.200 [WARN ] waypoint 4/15: 34.6 s since the last picture, over the 10.0 s interval (move 33.6 s, exposure 1.0 s)
17:06:02.214 [INFO ] captured waypoint 4/15
17:06:02.225 [INFO ] scan move 5/15: yaw 180 pitch 10 (deg)
17:06:02.858 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1352, "pit":92, "time":1791471962171 }
17:06:12.236 [INFO ] captured waypoint 5/15
17:06:12.247 [INFO ] scan move 6/15: yaw 0 pitch 5 (deg)
17:06:12.864 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":97, "time":1791471972173 }
17:06:31.739 [INFO ] CPU back to full speed: 55 °C, 0.8 GHz
17:06:31.740 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":55.0,"cpu_mhz":800,"cpu_busy_pct":12,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":2,"save_ms_mean":636,"save_ms_max":644,"held_s":0,"interval_s":10.0,"time":1791471991739}
17:06:35.594 [WARN ] waypoint 6/15: 23.4 s since the last picture, over the 10.0 s interval (move 22.3 s, exposure 1.0 s)
17:06:35.608 [INFO ] captured waypoint 6/15
17:06:35.619 [INFO ] scan move 7/15: yaw 45 pitch 5 (deg)
17:06:36.330 [INFO ] saving: 3 pictures in the last minute, 664 ms each on average (longest 722 ms)
17:06:36.330 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":36, "time":1791471995513 }
17:06:45.624 [INFO ] captured waypoint 7/15
17:06:45.634 [INFO ] scan move 9/15: yaw 135 pitch 5 (deg)
17:06:46.258 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":33, "time":1791472005515 }
17:07:02.295 [INFO ] CPU: 55 °C (x86_pkg_temp), 2.8 GHz of 2.9 GHz, 13% busy
17:07:02.295 [INFO ] throttling: no, 0 in the last minute, 0 since start
17:07:02.295 [INFO ] scan pace: slowed - CPU is slowing itself down, one picture every 10.0 s
17:07:02.295 [INFO ] saving: 4 in the last minute, 657 ms each (longest 722 ms)
17:07:02.295 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
17:07:20.203 [WARN ] waypoint 9/15: 34.6 s since the last picture, over the 10.0 s interval (move 33.6 s, exposure 1.0 s)
17:07:20.215 [INFO ] captured waypoint 9/15
17:07:20.226 [INFO ] scan move 10/15: yaw 180 pitch 5 (deg)
17:07:20.864 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1352, "pit":43, "time":1791472040191 }
17:07:30.234 [INFO ] captured waypoint 10/15
17:07:30.244 [INFO ] scan move 11/15: yaw 0 pitch 15 (deg)
17:07:30.864 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":1800, "pit":47, "time":1791472050194 }
17:07:31.859 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"slowed","reason":"CPU is slowing itself down","guard":true,"cpu_c":55.0,"cpu_mhz":800,"cpu_busy_pct":29,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":4,"save_ms_mean":659,"save_ms_max":722,"held_s":0,"interval_s":10.0,"time":1791472051859}
17:07:53.593 [WARN ] waypoint 11/15: 23.4 s since the last picture, over the 10.0 s interval (move 22.3 s, exposure 1.0 s)
17:07:53.604 [INFO ] captured waypoint 11/15
17:07:53.614 [INFO ] scan move 12/15: yaw 45 pitch 15 (deg)
17:07:54.293 [INFO ] saving: 3 pictures in the last minute, 656 ms each on average (longest 689 ms)
17:07:54.293 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":3598, "pit":111, "time":1791472073533 }
17:08:03.621 [INFO ] captured waypoint 12/15
17:08:03.631 [INFO ] scan move 14/15: yaw 135 pitch 15 (deg)
17:08:04.270 [MQTT] PUB GGS/FWT/dev/CamEvent { "fwt":"dev", "cam":"RGB", "hdg":451, "pit":110, "time":1791472083536 }
17:08:31.957 [INFO ] CPU at 54 °C: back to the normal picture pace
17:08:31.957 [MQTT] PUB GGS/FWT/dev/Health {"fwt":"dev","state":"normal","reason":"","guard":true,"cpu_c":54.0,"cpu_mhz":800,"cpu_busy_pct":15,"throttling":false,"throttle_events_last_min":0,"enclosure_c":21.0,"saved_last_min":2,"save_ms_mean":669,"save_ms_max":689,"held_s":0,"interval_s":5.0,"time":1791472111957}
17:08:32.296 [INFO ] CPU: 54 °C (x86_pkg_temp), 0.8 GHz of 2.9 GHz, 15% busy
17:08:32.296 [INFO ] throttling: no, 0 in the last minute, 0 since start
17:08:32.296 [INFO ] scan pace: normal, one picture every 5.0 s
17:08:32.296 [INFO ] saving: 2 in the last minute, 669 ms each (longest 689 ms)
17:08:32.296 [INFO ] limits: slows at 85 °C, pauses at 92 °C for 20 s, resumes at 75 °C after at least 300 s
17:08:37.309 [INFO ] Shutting down

View File

@ -0,0 +1,57 @@
#!/bin/sh
# The program itself on tower 111 - real camera, gimbal, the tower's own config -
# with the CPU throttled: 3 min normal, 6 min at 800 MHz + 2 busy cores, 3 min
# restored. ~13 min. nohup sh ~/thermal-test/guard_run.sh > ~/thermal-test/guard_run.out 2>&1 &
set -u
# What scripts/fgc sets up for the program: the camera SDK's transport layer,
# and the MQTT credentials (read, not sourced).
export GENICAM_GENTL64_PATH=/opt/VimbaX/cti
CRED=/etc/fire_gimbal_control/credentials.env
if [ -r "$CRED" ]; then
FGC_MQTT_USER=$(sed -n 's/^FGC_MQTT_USER=//p' "$CRED")
FGC_MQTT_PW=$(sed -n 's/^FGC_MQTT_PW=//p' "$CRED")
export FGC_MQTT_USER FGC_MQTT_PW
fi
T=~/thermal-test
SIM=$T/software/tools/thermal-sim
BIN=$T/software/build/fire_gimbal_control
LOG=$T/guard_run.log
FIFO=$T/cmd.fifo
restore() {
sudo -n "$SIM/cap-cpu.sh" restore
pkill -f "stress-ng --cpu" 2>/dev/null
}
trap restore EXIT INT TERM
rm -f "$FIFO"; mkfifo "$FIFO"
sleep 100000 > "$FIFO" & # holds the console's stdin open
holder=$!
"$BIN" -c ~/.config/fire_gimbal_control/config.ini --no-tui --trace mqtt \
< "$FIFO" > "$LOG" 2>&1 &
pid=$!
say() { echo "$(date +%T) $*"; }
cmd() { echo "$1" > "$FIFO"; }
say "started pid $pid; normal for 3 min"
sleep 180
cmd thermal
say "cap 800 MHz + 2 busy cores for 6 min"
sudo -n "$SIM/cap-cpu.sh" 800
stress-ng --cpu 2 --timeout 360s --quiet &
sleep 120; cmd thermal
sleep 120; cmd thermal
sleep 120; cmd thermal
wait $! 2>/dev/null
sudo -n "$SIM/cap-cpu.sh" restore
say "restored; 3 min more"
sleep 90; cmd thermal
sleep 90; cmd thermal
sleep 5
say "stopping"
kill -TERM "$pid"
sleep 15
kill "$holder" 2>/dev/null
sleep 3
kill -0 "$pid" 2>/dev/null && { say "still running, killing"; kill -KILL "$pid"; } || say "program exited"
rm -f "$FIFO"

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 55.0, "mhz": 757, "cpu_pct": 3.5, "watts": 3.22, "rss_mb": 43}
{"t": 2, "temp_c": 54.0, "mhz": 797, "cpu_pct": 3.8, "watts": 2.30, "rss_mb": 43}
{"t": 3, "temp_c": 54.0, "mhz": 763, "cpu_pct": 5.0, "watts": 2.96, "rss_mb": 43}
{"t": 4, "temp_c": 54.0, "mhz": 795, "cpu_pct": 4.3, "watts": 2.96, "rss_mb": 43}
{"t": 5, "temp_c": 54.0, "mhz": 794, "cpu_pct": 4.3, "watts": 2.92, "rss_mb": 43}
{"t": 6, "temp_c": 54.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.95, "rss_mb": 43}
{"t": 7, "temp_c": 54.0, "mhz": 768, "cpu_pct": 4.0, "watts": 3.05, "rss_mb": 43}
{"t": 8, "temp_c": 54.0, "mhz": 728, "cpu_pct": 3.8, "watts": 2.98, "rss_mb": 43}
{"t": 9, "temp_c": 54.0, "mhz": 796, "cpu_pct": 4.8, "watts": 2.22, "rss_mb": 43}
{"t": 10, "temp_c": 54.0, "mhz": 777, "cpu_pct": 3.5, "watts": 2.88, "rss_mb": 43}
{"t": 11, "temp_c": 54.0, "mhz": 762, "cpu_pct": 4.5, "watts": 2.94, "rss_mb": 43}
{"t": 12, "temp_c": 53.0, "mhz": 800, "cpu_pct": 3.5, "watts": 2.91, "rss_mb": 43}
{"t": 13, "temp_c": 53.0, "mhz": 798, "cpu_pct": 4.3, "watts": 3.03, "rss_mb": 43}
{"t": 14, "temp_c": 53.0, "mhz": 771, "cpu_pct": 3.0, "watts": 3.00, "rss_mb": 43}
{"t": 15, "temp_c": 54.0, "mhz": 747, "cpu_pct": 5.3, "watts": 3.01, "rss_mb": 43}
{"t": 16, "temp_c": 53.0, "mhz": 775, "cpu_pct": 5.3, "watts": 2.13, "rss_mb": 43}
{"t": 17, "temp_c": 54.0, "mhz": 911, "cpu_pct": 21.3, "watts": 4.54, "rss_mb": 43}
{"t": 18, "temp_c": 53.0, "mhz": 803, "cpu_pct": 3.3, "watts": 2.94, "rss_mb": 43}
{"t": 19, "temp_c": 53.0, "mhz": 775, "cpu_pct": 4.0, "watts": 3.01, "rss_mb": 43}
{"t": 20, "temp_c": 53.0, "mhz": 788, "cpu_pct": 3.3, "watts": 2.94, "rss_mb": 43}
{"t": 21, "temp_c": 53.0, "mhz": 800, "cpu_pct": 6.3, "watts": 3.07, "rss_mb": 43}
{"t": 22, "temp_c": 54.0, "mhz": 773, "cpu_pct": 5.0, "watts": 3.04, "rss_mb": 43}
{"t": 23, "temp_c": 53.0, "mhz": 794, "cpu_pct": 4.0, "watts": 2.14, "rss_mb": 43}
{"t": 24, "temp_c": 53.0, "mhz": 830, "cpu_pct": 4.1, "watts": 2.94, "rss_mb": 43}
{"t": 25, "temp_c": 52.0, "mhz": 783, "cpu_pct": 3.5, "watts": 3.01, "rss_mb": 43}
{"t": 26, "temp_c": 52.0, "mhz": 785, "cpu_pct": 4.0, "watts": 2.91, "rss_mb": 43}
{"t": 27, "temp_c": 52.0, "mhz": 1565, "cpu_pct": 6.3, "watts": 2.99, "rss_mb": 43}
{"t": 28, "temp_c": 52.0, "mhz": 788, "cpu_pct": 3.8, "watts": 2.97, "rss_mb": 43}
{"t": 29, "temp_c": 53.0, "mhz": 767, "cpu_pct": 3.8, "watts": 2.94, "rss_mb": 43}
{"t": 30, "temp_c": 53.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.12, "rss_mb": 43}
{"t": 31, "temp_c": 52.0, "mhz": 787, "cpu_pct": 3.5, "watts": 2.97, "rss_mb": 43}
{"t": 32, "temp_c": 52.0, "mhz": 794, "cpu_pct": 4.5, "watts": 2.95, "rss_mb": 43}
{"t": 33, "temp_c": 53.0, "mhz": 789, "cpu_pct": 5.0, "watts": 2.98, "rss_mb": 43}
{"t": 34, "temp_c": 53.0, "mhz": 793, "cpu_pct": 3.8, "watts": 2.98, "rss_mb": 43}
{"t": 35, "temp_c": 54.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.81, "rss_mb": 43}
{"t": 36, "temp_c": 53.0, "mhz": 1300, "cpu_pct": 4.0, "watts": 2.93, "rss_mb": 43}
{"t": 37, "temp_c": 53.0, "mhz": 788, "cpu_pct": 3.3, "watts": 2.29, "rss_mb": 43}
{"t": 38, "temp_c": 53.0, "mhz": 776, "cpu_pct": 3.8, "watts": 2.92, "rss_mb": 43}
{"t": 39, "temp_c": 53.0, "mhz": 1483, "cpu_pct": 5.6, "watts": 2.94, "rss_mb": 43}
{"t": 40, "temp_c": 53.0, "mhz": 793, "cpu_pct": 4.0, "watts": 3.12, "rss_mb": 43}
{"t": 41, "temp_c": 52.0, "mhz": 802, "cpu_pct": 3.8, "watts": 2.78, "rss_mb": 43}
{"t": 42, "temp_c": 53.0, "mhz": 1379, "cpu_pct": 4.5, "watts": 2.97, "rss_mb": 43}
{"t": 43, "temp_c": 51.0, "mhz": 760, "cpu_pct": 2.8, "watts": 2.89, "rss_mb": 43}
{"t": 44, "temp_c": 51.0, "mhz": 791, "cpu_pct": 4.5, "watts": 2.28, "rss_mb": 43}
{"t": 45, "temp_c": 52.0, "mhz": 795, "cpu_pct": 5.5, "watts": 2.94, "rss_mb": 43}
{"t": 46, "temp_c": 52.0, "mhz": 773, "cpu_pct": 4.0, "watts": 2.89, "rss_mb": 43}
{"t": 47, "temp_c": 52.0, "mhz": 1401, "cpu_pct": 6.8, "watts": 2.91, "rss_mb": 43}
{"t": 48, "temp_c": 53.0, "mhz": 1531, "cpu_pct": 22.9, "watts": 4.61, "rss_mb": 43}
{"t": 49, "temp_c": 52.0, "mhz": 777, "cpu_pct": 3.0, "watts": 2.91, "rss_mb": 43}
{"t": 50, "temp_c": 51.0, "mhz": 797, "cpu_pct": 4.3, "watts": 2.95, "rss_mb": 43}
{"t": 51, "temp_c": 52.0, "mhz": 786, "cpu_pct": 4.8, "watts": 2.28, "rss_mb": 43}
{"t": 52, "temp_c": 51.0, "mhz": 804, "cpu_pct": 3.5, "watts": 2.83, "rss_mb": 43}
{"t": 53, "temp_c": 52.0, "mhz": 928, "cpu_pct": 4.3, "watts": 2.92, "rss_mb": 43}
{"t": 54, "temp_c": 52.0, "mhz": 2590, "cpu_pct": 4.5, "watts": 2.96, "rss_mb": 43}
{"t": 55, "temp_c": 51.0, "mhz": 791, "cpu_pct": 3.3, "watts": 2.98, "rss_mb": 43}
{"t": 56, "temp_c": 52.0, "mhz": 759, "cpu_pct": 4.5, "watts": 2.94, "rss_mb": 43}
{"t": 57, "temp_c": 52.0, "mhz": 801, "cpu_pct": 6.0, "watts": 3.13, "rss_mb": 43}
{"t": 58, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.07, "rss_mb": 43}
{"t": 59, "temp_c": 52.0, "mhz": 797, "cpu_pct": 4.5, "watts": 2.92, "rss_mb": 43}
{"t": 60, "temp_c": 52.0, "mhz": 2423, "cpu_pct": 4.5, "watts": 2.97, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 52.8, "temp_c_max": 55.0, "mhz_mean": 913, "cpu_pct_mean": 4.8, "watts_mean": 2.90}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 53.0, "mhz": 800, "cpu_pct": 5.5, "watts": 3.47, "rss_mb": 43}
{"t": 2, "temp_c": 52.0, "mhz": 787, "cpu_pct": 4.8, "watts": 3.01, "rss_mb": 43}
{"t": 3, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.97, "rss_mb": 43}
{"t": 4, "temp_c": 54.0, "mhz": 1188, "cpu_pct": 4.5, "watts": 2.85, "rss_mb": 43}
{"t": 5, "temp_c": 51.0, "mhz": 878, "cpu_pct": 2.5, "watts": 2.87, "rss_mb": 43}
{"t": 6, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.5, "watts": 2.34, "rss_mb": 43}
{"t": 7, "temp_c": 51.0, "mhz": 791, "cpu_pct": 5.0, "watts": 3.03, "rss_mb": 43}
{"t": 8, "temp_c": 52.0, "mhz": 791, "cpu_pct": 4.3, "watts": 2.95, "rss_mb": 43}
{"t": 9, "temp_c": 52.0, "mhz": 835, "cpu_pct": 3.5, "watts": 2.76, "rss_mb": 43}
{"t": 10, "temp_c": 52.0, "mhz": 2785, "cpu_pct": 5.0, "watts": 2.96, "rss_mb": 43}
{"t": 11, "temp_c": 52.0, "mhz": 772, "cpu_pct": 2.3, "watts": 2.97, "rss_mb": 43}
{"t": 12, "temp_c": 52.0, "mhz": 757, "cpu_pct": 4.5, "watts": 2.93, "rss_mb": 43}
{"t": 13, "temp_c": 52.0, "mhz": 768, "cpu_pct": 5.8, "watts": 2.47, "rss_mb": 43}
{"t": 14, "temp_c": 52.0, "mhz": 797, "cpu_pct": 3.8, "watts": 2.86, "rss_mb": 43}
{"t": 15, "temp_c": 52.0, "mhz": 795, "cpu_pct": 3.0, "watts": 2.83, "rss_mb": 43}
{"t": 16, "temp_c": 52.0, "mhz": 1254, "cpu_pct": 5.5, "watts": 3.05, "rss_mb": 43}
{"t": 17, "temp_c": 51.0, "mhz": 794, "cpu_pct": 2.3, "watts": 2.94, "rss_mb": 43}
{"t": 18, "temp_c": 51.0, "mhz": 796, "cpu_pct": 4.3, "watts": 2.96, "rss_mb": 43}
{"t": 19, "temp_c": 52.0, "mhz": 761, "cpu_pct": 5.5, "watts": 3.04, "rss_mb": 43}
{"t": 20, "temp_c": 51.0, "mhz": 798, "cpu_pct": 3.8, "watts": 2.13, "rss_mb": 43}
{"t": 21, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.3, "watts": 2.92, "rss_mb": 43}
{"t": 22, "temp_c": 53.0, "mhz": 876, "cpu_pct": 4.5, "watts": 3.01, "rss_mb": 43}
{"t": 23, "temp_c": 54.0, "mhz": 900, "cpu_pct": 18.5, "watts": 3.94, "rss_mb": 43}
{"t": 24, "temp_c": 51.0, "mhz": 798, "cpu_pct": 7.3, "watts": 3.55, "rss_mb": 43}
{"t": 25, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.8, "watts": 2.98, "rss_mb": 43}
{"t": 26, "temp_c": 52.0, "mhz": 787, "cpu_pct": 3.5, "watts": 2.98, "rss_mb": 43}
{"t": 27, "temp_c": 51.0, "mhz": 794, "cpu_pct": 4.0, "watts": 2.12, "rss_mb": 43}
{"t": 28, "temp_c": 52.0, "mhz": 1107, "cpu_pct": 6.0, "watts": 3.08, "rss_mb": 43}
{"t": 29, "temp_c": 51.0, "mhz": 773, "cpu_pct": 2.8, "watts": 2.94, "rss_mb": 43}
{"t": 30, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.5, "watts": 2.95, "rss_mb": 43}
{"t": 31, "temp_c": 51.0, "mhz": 798, "cpu_pct": 5.0, "watts": 2.98, "rss_mb": 43}
{"t": 32, "temp_c": 51.0, "mhz": 790, "cpu_pct": 4.0, "watts": 2.93, "rss_mb": 43}
{"t": 33, "temp_c": 52.0, "mhz": 765, "cpu_pct": 4.0, "watts": 2.91, "rss_mb": 43}
{"t": 34, "temp_c": 52.0, "mhz": 800, "cpu_pct": 5.3, "watts": 2.19, "rss_mb": 43}
{"t": 35, "temp_c": 52.0, "mhz": 771, "cpu_pct": 3.0, "watts": 2.94, "rss_mb": 43}
{"t": 36, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.91, "rss_mb": 43}
{"t": 37, "temp_c": 51.0, "mhz": 794, "cpu_pct": 5.0, "watts": 2.92, "rss_mb": 43}
{"t": 38, "temp_c": 51.0, "mhz": 754, "cpu_pct": 4.3, "watts": 2.94, "rss_mb": 43}
{"t": 39, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.3, "watts": 2.90, "rss_mb": 43}
{"t": 40, "temp_c": 52.0, "mhz": 1385, "cpu_pct": 4.8, "watts": 2.95, "rss_mb": 43}
{"t": 41, "temp_c": 51.0, "mhz": 778, "cpu_pct": 2.8, "watts": 2.07, "rss_mb": 43}
{"t": 42, "temp_c": 51.0, "mhz": 752, "cpu_pct": 4.3, "watts": 2.90, "rss_mb": 43}
{"t": 43, "temp_c": 51.0, "mhz": 787, "cpu_pct": 6.3, "watts": 3.10, "rss_mb": 43}
{"t": 44, "temp_c": 51.0, "mhz": 787, "cpu_pct": 4.5, "watts": 2.93, "rss_mb": 43}
{"t": 45, "temp_c": 52.0, "mhz": 765, "cpu_pct": 3.8, "watts": 2.91, "rss_mb": 43}
{"t": 46, "temp_c": 52.0, "mhz": 2499, "cpu_pct": 5.6, "watts": 2.86, "rss_mb": 43}
{"t": 47, "temp_c": 51.0, "mhz": 2820, "cpu_pct": 3.0, "watts": 2.90, "rss_mb": 43}
{"t": 48, "temp_c": 51.0, "mhz": 791, "cpu_pct": 3.8, "watts": 2.30, "rss_mb": 43}
{"t": 49, "temp_c": 51.0, "mhz": 796, "cpu_pct": 5.3, "watts": 2.96, "rss_mb": 43}
{"t": 50, "temp_c": 51.0, "mhz": 786, "cpu_pct": 3.8, "watts": 2.91, "rss_mb": 43}
{"t": 51, "temp_c": 52.0, "mhz": 801, "cpu_pct": 3.3, "watts": 2.75, "rss_mb": 43}
{"t": 52, "temp_c": 52.0, "mhz": 2800, "cpu_pct": 4.5, "watts": 2.99, "rss_mb": 43}
{"t": 53, "temp_c": 51.0, "mhz": 2414, "cpu_pct": 4.0, "watts": 2.91, "rss_mb": 43}
{"t": 54, "temp_c": 56.0, "mhz": 2800, "cpu_pct": 21.1, "watts": 4.36, "rss_mb": 43}
{"t": 55, "temp_c": 53.0, "mhz": 795, "cpu_pct": 6.5, "watts": 2.44, "rss_mb": 43}
{"t": 56, "temp_c": 51.0, "mhz": 794, "cpu_pct": 4.5, "watts": 2.87, "rss_mb": 43}
{"t": 57, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.5, "watts": 2.91, "rss_mb": 43}
{"t": 58, "temp_c": 51.0, "mhz": 800, "cpu_pct": 5.3, "watts": 3.09, "rss_mb": 43}
{"t": 59, "temp_c": 51.0, "mhz": 657, "cpu_pct": 3.8, "watts": 2.95, "rss_mb": 43}
{"t": 60, "temp_c": 51.0, "mhz": 767, "cpu_pct": 4.0, "watts": 2.92, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 51.7, "temp_c_max": 56.0, "mhz_mean": 1010, "cpu_pct_mean": 4.9, "watts_mean": 2.91}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 54.0, "mhz": 891, "cpu_pct": 3.0, "watts": 3.18, "rss_mb": 43}
{"t": 2, "temp_c": 53.0, "mhz": 800, "cpu_pct": 4.3, "watts": 2.30, "rss_mb": 43}
{"t": 3, "temp_c": 52.0, "mhz": 792, "cpu_pct": 6.3, "watts": 3.11, "rss_mb": 43}
{"t": 4, "temp_c": 54.0, "mhz": 800, "cpu_pct": 3.5, "watts": 2.85, "rss_mb": 43}
{"t": 5, "temp_c": 53.0, "mhz": 813, "cpu_pct": 3.8, "watts": 2.86, "rss_mb": 43}
{"t": 6, "temp_c": 54.0, "mhz": 2506, "cpu_pct": 4.5, "watts": 3.04, "rss_mb": 43}
{"t": 7, "temp_c": 53.0, "mhz": 795, "cpu_pct": 2.8, "watts": 2.94, "rss_mb": 43}
{"t": 8, "temp_c": 53.0, "mhz": 784, "cpu_pct": 4.5, "watts": 2.95, "rss_mb": 43}
{"t": 9, "temp_c": 53.0, "mhz": 784, "cpu_pct": 4.5, "watts": 2.33, "rss_mb": 43}
{"t": 10, "temp_c": 52.0, "mhz": 797, "cpu_pct": 3.0, "watts": 2.74, "rss_mb": 43}
{"t": 11, "temp_c": 53.0, "mhz": 790, "cpu_pct": 5.0, "watts": 2.93, "rss_mb": 43}
{"t": 12, "temp_c": 54.0, "mhz": 2840, "cpu_pct": 4.8, "watts": 3.02, "rss_mb": 43}
{"t": 13, "temp_c": 52.0, "mhz": 797, "cpu_pct": 2.8, "watts": 2.96, "rss_mb": 43}
{"t": 14, "temp_c": 53.0, "mhz": 791, "cpu_pct": 4.0, "watts": 2.94, "rss_mb": 43}
{"t": 15, "temp_c": 53.0, "mhz": 718, "cpu_pct": 4.5, "watts": 2.99, "rss_mb": 43}
{"t": 16, "temp_c": 52.0, "mhz": 756, "cpu_pct": 3.3, "watts": 2.08, "rss_mb": 43}
{"t": 17, "temp_c": 53.0, "mhz": 796, "cpu_pct": 4.3, "watts": 2.92, "rss_mb": 43}
{"t": 18, "temp_c": 53.0, "mhz": 1035, "cpu_pct": 4.8, "watts": 2.99, "rss_mb": 43}
{"t": 19, "temp_c": 52.0, "mhz": 800, "cpu_pct": 3.3, "watts": 2.93, "rss_mb": 43}
{"t": 20, "temp_c": 62.0, "mhz": 2800, "cpu_pct": 10.2, "watts": 3.15, "rss_mb": 43}
{"t": 21, "temp_c": 54.0, "mhz": 772, "cpu_pct": 18.3, "watts": 4.66, "rss_mb": 43}
{"t": 22, "temp_c": 53.0, "mhz": 762, "cpu_pct": 3.5, "watts": 2.93, "rss_mb": 43}
{"t": 23, "temp_c": 52.0, "mhz": 781, "cpu_pct": 4.8, "watts": 2.16, "rss_mb": 43}
{"t": 24, "temp_c": 53.0, "mhz": 1121, "cpu_pct": 5.5, "watts": 3.00, "rss_mb": 43}
{"t": 25, "temp_c": 51.0, "mhz": 711, "cpu_pct": 3.0, "watts": 2.91, "rss_mb": 43}
{"t": 26, "temp_c": 52.0, "mhz": 748, "cpu_pct": 3.5, "watts": 2.94, "rss_mb": 43}
{"t": 27, "temp_c": 52.0, "mhz": 773, "cpu_pct": 4.8, "watts": 2.99, "rss_mb": 43}
{"t": 28, "temp_c": 51.0, "mhz": 776, "cpu_pct": 4.5, "watts": 2.93, "rss_mb": 43}
{"t": 29, "temp_c": 52.0, "mhz": 731, "cpu_pct": 4.5, "watts": 2.98, "rss_mb": 43}
{"t": 30, "temp_c": 52.0, "mhz": 785, "cpu_pct": 4.0, "watts": 2.11, "rss_mb": 43}
{"t": 31, "temp_c": 52.0, "mhz": 2803, "cpu_pct": 3.0, "watts": 2.97, "rss_mb": 43}
{"t": 32, "temp_c": 51.0, "mhz": 798, "cpu_pct": 3.0, "watts": 2.97, "rss_mb": 43}
{"t": 33, "temp_c": 52.0, "mhz": 768, "cpu_pct": 4.8, "watts": 2.98, "rss_mb": 43}
{"t": 34, "temp_c": 52.0, "mhz": 776, "cpu_pct": 4.5, "watts": 2.97, "rss_mb": 43}
{"t": 35, "temp_c": 51.0, "mhz": 795, "cpu_pct": 4.3, "watts": 2.93, "rss_mb": 43}
{"t": 36, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.1, "watts": 2.99, "rss_mb": 43}
{"t": 37, "temp_c": 52.0, "mhz": 2808, "cpu_pct": 3.5, "watts": 2.12, "rss_mb": 43}
{"t": 38, "temp_c": 51.0, "mhz": 796, "cpu_pct": 2.8, "watts": 2.95, "rss_mb": 43}
{"t": 39, "temp_c": 51.0, "mhz": 766, "cpu_pct": 6.0, "watts": 3.06, "rss_mb": 43}
{"t": 40, "temp_c": 51.0, "mhz": 776, "cpu_pct": 4.3, "watts": 2.90, "rss_mb": 43}
{"t": 41, "temp_c": 51.0, "mhz": 751, "cpu_pct": 4.3, "watts": 2.97, "rss_mb": 43}
{"t": 42, "temp_c": 51.0, "mhz": 796, "cpu_pct": 4.3, "watts": 2.94, "rss_mb": 43}
{"t": 43, "temp_c": 53.0, "mhz": 2800, "cpu_pct": 4.3, "watts": 2.10, "rss_mb": 43}
{"t": 44, "temp_c": 51.0, "mhz": 793, "cpu_pct": 1.8, "watts": 2.89, "rss_mb": 43}
{"t": 45, "temp_c": 51.0, "mhz": 782, "cpu_pct": 5.8, "watts": 2.97, "rss_mb": 43}
{"t": 46, "temp_c": 51.0, "mhz": 751, "cpu_pct": 4.3, "watts": 2.93, "rss_mb": 43}
{"t": 47, "temp_c": 52.0, "mhz": 750, "cpu_pct": 3.8, "watts": 2.93, "rss_mb": 43}
{"t": 48, "temp_c": 51.0, "mhz": 796, "cpu_pct": 4.8, "watts": 2.99, "rss_mb": 43}
{"t": 49, "temp_c": 52.0, "mhz": 1222, "cpu_pct": 3.3, "watts": 2.82, "rss_mb": 43}
{"t": 50, "temp_c": 51.0, "mhz": 791, "cpu_pct": 2.3, "watts": 2.14, "rss_mb": 43}
{"t": 51, "temp_c": 54.0, "mhz": 820, "cpu_pct": 14.6, "watts": 3.35, "rss_mb": 43}
{"t": 52, "temp_c": 51.0, "mhz": 770, "cpu_pct": 12.2, "watts": 4.17, "rss_mb": 43}
{"t": 53, "temp_c": 51.0, "mhz": 765, "cpu_pct": 4.3, "watts": 2.95, "rss_mb": 43}
{"t": 54, "temp_c": 52.0, "mhz": 795, "cpu_pct": 4.0, "watts": 2.94, "rss_mb": 43}
{"t": 55, "temp_c": 52.0, "mhz": 834, "cpu_pct": 3.3, "watts": 2.81, "rss_mb": 43}
{"t": 56, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.0, "watts": 2.89, "rss_mb": 43}
{"t": 57, "temp_c": 51.0, "mhz": 800, "cpu_pct": 6.8, "watts": 3.13, "rss_mb": 43}
{"t": 58, "temp_c": 51.0, "mhz": 781, "cpu_pct": 3.3, "watts": 2.32, "rss_mb": 43}
{"t": 59, "temp_c": 51.0, "mhz": 790, "cpu_pct": 4.5, "watts": 2.95, "rss_mb": 43}
{"t": 60, "temp_c": 52.0, "mhz": 797, "cpu_pct": 4.0, "watts": 2.73, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 52.2, "temp_c_max": 62.0, "mhz_mean": 999, "cpu_pct_mean": 4.7, "watts_mean": 2.89}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 52.0, "mhz": 779, "cpu_pct": 3.0, "watts": 3.15, "rss_mb": 42}
{"t": 2, "temp_c": 52.0, "mhz": 1084, "cpu_pct": 4.8, "watts": 3.04, "rss_mb": 43}
{"t": 3, "temp_c": 52.0, "mhz": 798, "cpu_pct": 3.5, "watts": 2.33, "rss_mb": 43}
{"t": 4, "temp_c": 51.0, "mhz": 790, "cpu_pct": 3.8, "watts": 2.98, "rss_mb": 43}
{"t": 5, "temp_c": 52.0, "mhz": 772, "cpu_pct": 5.3, "watts": 2.94, "rss_mb": 43}
{"t": 6, "temp_c": 52.0, "mhz": 801, "cpu_pct": 3.8, "watts": 2.87, "rss_mb": 43}
{"t": 7, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.0, "watts": 2.95, "rss_mb": 43}
{"t": 8, "temp_c": 52.0, "mhz": 799, "cpu_pct": 5.8, "watts": 3.13, "rss_mb": 43}
{"t": 9, "temp_c": 51.0, "mhz": 794, "cpu_pct": 4.0, "watts": 2.32, "rss_mb": 43}
{"t": 10, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.5, "watts": 2.91, "rss_mb": 43}
{"t": 11, "temp_c": 51.0, "mhz": 777, "cpu_pct": 5.5, "watts": 2.90, "rss_mb": 43}
{"t": 12, "temp_c": 51.0, "mhz": 862, "cpu_pct": 4.0, "watts": 2.90, "rss_mb": 43}
{"t": 13, "temp_c": 52.0, "mhz": 1300, "cpu_pct": 3.5, "watts": 2.96, "rss_mb": 43}
{"t": 14, "temp_c": 51.0, "mhz": 778, "cpu_pct": 4.8, "watts": 3.06, "rss_mb": 43}
{"t": 15, "temp_c": 51.0, "mhz": 775, "cpu_pct": 3.8, "watts": 2.97, "rss_mb": 43}
{"t": 16, "temp_c": 51.0, "mhz": 758, "cpu_pct": 3.8, "watts": 2.31, "rss_mb": 43}
{"t": 17, "temp_c": 51.0, "mhz": 795, "cpu_pct": 4.0, "watts": 2.86, "rss_mb": 43}
{"t": 18, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.91, "rss_mb": 43}
{"t": 19, "temp_c": 51.0, "mhz": 1273, "cpu_pct": 3.8, "watts": 3.00, "rss_mb": 43}
{"t": 20, "temp_c": 52.0, "mhz": 771, "cpu_pct": 4.5, "watts": 3.02, "rss_mb": 43}
{"t": 21, "temp_c": 51.0, "mhz": 797, "cpu_pct": 4.3, "watts": 2.98, "rss_mb": 43}
{"t": 22, "temp_c": 52.0, "mhz": 787, "cpu_pct": 3.8, "watts": 2.89, "rss_mb": 43}
{"t": 23, "temp_c": 51.0, "mhz": 800, "cpu_pct": 5.1, "watts": 2.19, "rss_mb": 43}
{"t": 24, "temp_c": 51.0, "mhz": 856, "cpu_pct": 5.3, "watts": 2.93, "rss_mb": 43}
{"t": 25, "temp_c": 52.0, "mhz": 799, "cpu_pct": 20.9, "watts": 4.73, "rss_mb": 43}
{"t": 26, "temp_c": 51.0, "mhz": 780, "cpu_pct": 5.6, "watts": 3.11, "rss_mb": 43}
{"t": 27, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.87, "rss_mb": 43}
{"t": 28, "temp_c": 51.0, "mhz": 757, "cpu_pct": 4.0, "watts": 2.95, "rss_mb": 43}
{"t": 29, "temp_c": 51.0, "mhz": 800, "cpu_pct": 5.3, "watts": 2.22, "rss_mb": 43}
{"t": 30, "temp_c": 52.0, "mhz": 2051, "cpu_pct": 4.5, "watts": 2.98, "rss_mb": 43}
{"t": 31, "temp_c": 51.0, "mhz": 796, "cpu_pct": 2.5, "watts": 2.98, "rss_mb": 43}
{"t": 32, "temp_c": 51.0, "mhz": 782, "cpu_pct": 3.8, "watts": 2.94, "rss_mb": 43}
{"t": 33, "temp_c": 51.0, "mhz": 796, "cpu_pct": 3.3, "watts": 2.90, "rss_mb": 43}
{"t": 34, "temp_c": 51.0, "mhz": 790, "cpu_pct": 4.5, "watts": 2.92, "rss_mb": 43}
{"t": 35, "temp_c": 51.0, "mhz": 763, "cpu_pct": 6.3, "watts": 3.06, "rss_mb": 43}
{"t": 36, "temp_c": 52.0, "mhz": 866, "cpu_pct": 4.0, "watts": 2.14, "rss_mb": 43}
{"t": 37, "temp_c": 51.0, "mhz": 797, "cpu_pct": 2.0, "watts": 2.94, "rss_mb": 43}
{"t": 38, "temp_c": 51.0, "mhz": 787, "cpu_pct": 3.8, "watts": 2.98, "rss_mb": 43}
{"t": 39, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.8, "watts": 2.96, "rss_mb": 43}
{"t": 40, "temp_c": 51.0, "mhz": 735, "cpu_pct": 4.5, "watts": 2.93, "rss_mb": 43}
{"t": 41, "temp_c": 51.0, "mhz": 792, "cpu_pct": 6.5, "watts": 3.10, "rss_mb": 43}
{"t": 42, "temp_c": 52.0, "mhz": 1260, "cpu_pct": 3.3, "watts": 2.89, "rss_mb": 43}
{"t": 43, "temp_c": 51.0, "mhz": 800, "cpu_pct": 2.0, "watts": 2.13, "rss_mb": 43}
{"t": 44, "temp_c": 51.0, "mhz": 789, "cpu_pct": 5.0, "watts": 3.00, "rss_mb": 43}
{"t": 45, "temp_c": 51.0, "mhz": 796, "cpu_pct": 4.3, "watts": 2.93, "rss_mb": 43}
{"t": 46, "temp_c": 51.0, "mhz": 783, "cpu_pct": 4.3, "watts": 2.97, "rss_mb": 43}
{"t": 47, "temp_c": 51.0, "mhz": 777, "cpu_pct": 5.0, "watts": 2.98, "rss_mb": 43}
{"t": 48, "temp_c": 52.0, "mhz": 861, "cpu_pct": 3.3, "watts": 2.75, "rss_mb": 43}
{"t": 49, "temp_c": 51.0, "mhz": 779, "cpu_pct": 3.0, "watts": 2.90, "rss_mb": 43}
{"t": 50, "temp_c": 51.0, "mhz": 794, "cpu_pct": 4.3, "watts": 2.28, "rss_mb": 43}
{"t": 51, "temp_c": 51.0, "mhz": 778, "cpu_pct": 4.0, "watts": 2.95, "rss_mb": 43}
{"t": 52, "temp_c": 51.0, "mhz": 774, "cpu_pct": 4.8, "watts": 2.95, "rss_mb": 43}
{"t": 53, "temp_c": 51.0, "mhz": 790, "cpu_pct": 4.5, "watts": 2.84, "rss_mb": 43}
{"t": 54, "temp_c": 52.0, "mhz": 1624, "cpu_pct": 4.5, "watts": 2.92, "rss_mb": 43}
{"t": 55, "temp_c": 51.0, "mhz": 800, "cpu_pct": 5.0, "watts": 3.13, "rss_mb": 43}
{"t": 56, "temp_c": 51.0, "mhz": 764, "cpu_pct": 24.8, "watts": 4.61, "rss_mb": 43}
{"t": 57, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.91, "rss_mb": 43}
{"t": 58, "temp_c": 51.0, "mhz": 797, "cpu_pct": 4.3, "watts": 2.12, "rss_mb": 43}
{"t": 59, "temp_c": 51.0, "mhz": 1127, "cpu_pct": 5.6, "watts": 3.01, "rss_mb": 43}
{"t": 60, "temp_c": 51.0, "mhz": 786, "cpu_pct": 3.3, "watts": 2.94, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 51.2, "temp_c_max": 52.0, "mhz_mean": 862, "cpu_pct_mean": 4.8, "watts_mean": 2.91}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 49.0, "mhz": 800, "cpu_pct": 7.6, "watts": -1.00, "rss_mb": 43}
{"t": 2, "temp_c": 48.0, "mhz": 800, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 3, "temp_c": 49.0, "mhz": 780, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 4, "temp_c": 49.0, "mhz": 2456, "cpu_pct": 4.5, "watts": -1.00, "rss_mb": 43}
{"t": 5, "temp_c": 48.0, "mhz": 794, "cpu_pct": 3.8, "watts": -1.00, "rss_mb": 43}
{"t": 6, "temp_c": 50.0, "mhz": 781, "cpu_pct": 22.0, "watts": -1.00, "rss_mb": 43}
{"t": 7, "temp_c": 48.0, "mhz": 786, "cpu_pct": 3.5, "watts": -1.00, "rss_mb": 43}
{"t": 8, "temp_c": 48.0, "mhz": 794, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 9, "temp_c": 48.0, "mhz": 793, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 10, "temp_c": 49.0, "mhz": 1249, "cpu_pct": 4.0, "watts": -1.00, "rss_mb": 43}
{"t": 11, "temp_c": 48.0, "mhz": 761, "cpu_pct": 1.8, "watts": -1.00, "rss_mb": 43}
{"t": 12, "temp_c": 48.0, "mhz": 798, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 13, "temp_c": 47.0, "mhz": 794, "cpu_pct": 3.5, "watts": -1.00, "rss_mb": 43}
{"t": 14, "temp_c": 48.0, "mhz": 767, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 15, "temp_c": 48.0, "mhz": 690, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 16, "temp_c": 55.0, "mhz": 2800, "cpu_pct": 12.8, "watts": -1.00, "rss_mb": 43}
{"t": 17, "temp_c": 52.0, "mhz": 800, "cpu_pct": 27.6, "watts": -1.00, "rss_mb": 43}
{"t": 18, "temp_c": 48.0, "mhz": 794, "cpu_pct": 9.4, "watts": -1.00, "rss_mb": 43}
{"t": 19, "temp_c": 48.0, "mhz": 769, "cpu_pct": 3.5, "watts": -1.00, "rss_mb": 43}
{"t": 20, "temp_c": 48.0, "mhz": 800, "cpu_pct": 2.3, "watts": -1.00, "rss_mb": 43}
{"t": 21, "temp_c": 48.0, "mhz": 812, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 22, "temp_c": 49.0, "mhz": 873, "cpu_pct": 5.3, "watts": -1.00, "rss_mb": 43}
{"t": 23, "temp_c": 48.0, "mhz": 1691, "cpu_pct": 1.8, "watts": -1.00, "rss_mb": 43}
{"t": 24, "temp_c": 48.0, "mhz": 790, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 25, "temp_c": 47.0, "mhz": 762, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 26, "temp_c": 48.0, "mhz": 800, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 27, "temp_c": 48.0, "mhz": 793, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 28, "temp_c": 48.0, "mhz": 791, "cpu_pct": 3.8, "watts": -1.00, "rss_mb": 43}
{"t": 29, "temp_c": 48.0, "mhz": 1217, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 30, "temp_c": 47.0, "mhz": 800, "cpu_pct": 1.3, "watts": -1.00, "rss_mb": 43}
{"t": 31, "temp_c": 49.0, "mhz": 794, "cpu_pct": 3.5, "watts": -1.00, "rss_mb": 43}
{"t": 32, "temp_c": 48.0, "mhz": 777, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 33, "temp_c": 48.0, "mhz": 800, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 34, "temp_c": 47.0, "mhz": 800, "cpu_pct": 3.8, "watts": -1.00, "rss_mb": 43}
{"t": 35, "temp_c": 48.0, "mhz": 764, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 36, "temp_c": 48.0, "mhz": 2800, "cpu_pct": 1.5, "watts": -1.00, "rss_mb": 43}
{"t": 37, "temp_c": 48.0, "mhz": 801, "cpu_pct": 3.8, "watts": -1.00, "rss_mb": 43}
{"t": 38, "temp_c": 48.0, "mhz": 794, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 39, "temp_c": 48.0, "mhz": 798, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 40, "temp_c": 48.0, "mhz": 790, "cpu_pct": 4.3, "watts": -1.00, "rss_mb": 43}
{"t": 41, "temp_c": 49.0, "mhz": 805, "cpu_pct": 2.3, "watts": -1.00, "rss_mb": 43}
{"t": 42, "temp_c": 49.0, "mhz": 777, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 43, "temp_c": 48.0, "mhz": 793, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 44, "temp_c": 47.0, "mhz": 791, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 45, "temp_c": 48.0, "mhz": 800, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 46, "temp_c": 48.0, "mhz": 800, "cpu_pct": 3.5, "watts": -1.00, "rss_mb": 43}
{"t": 47, "temp_c": 50.0, "mhz": 800, "cpu_pct": 17.0, "watts": -1.00, "rss_mb": 43}
{"t": 48, "temp_c": 48.0, "mhz": 779, "cpu_pct": 10.8, "watts": -1.00, "rss_mb": 43}
{"t": 49, "temp_c": 48.0, "mhz": 800, "cpu_pct": 3.3, "watts": -1.00, "rss_mb": 43}
{"t": 50, "temp_c": 48.0, "mhz": 754, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 51, "temp_c": 48.0, "mhz": 741, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 52, "temp_c": 48.0, "mhz": 800, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 53, "temp_c": 48.0, "mhz": 800, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 54, "temp_c": 49.0, "mhz": 798, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"t": 55, "temp_c": 50.0, "mhz": 1573, "cpu_pct": 5.8, "watts": -1.00, "rss_mb": 43}
{"t": 56, "temp_c": 48.0, "mhz": 2828, "cpu_pct": 2.0, "watts": -1.00, "rss_mb": 43}
{"t": 57, "temp_c": 48.0, "mhz": 759, "cpu_pct": 3.0, "watts": -1.00, "rss_mb": 43}
{"t": 58, "temp_c": 49.0, "mhz": 790, "cpu_pct": 3.8, "watts": -1.00, "rss_mb": 43}
{"t": 59, "temp_c": 48.0, "mhz": 789, "cpu_pct": 2.8, "watts": -1.00, "rss_mb": 43}
{"t": 60, "temp_c": 48.0, "mhz": 796, "cpu_pct": 2.5, "watts": -1.00, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 48.4, "temp_c_max": 55.0, "mhz_mean": 960, "cpu_pct_mean": 4.5, "watts_mean": -1.00}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 55.0, "mhz": 885, "cpu_pct": 4.5, "watts": 3.53, "rss_mb": 43}
{"t": 2, "temp_c": 54.0, "mhz": 1289, "cpu_pct": 3.8, "watts": 2.96, "rss_mb": 43}
{"t": 3, "temp_c": 53.0, "mhz": 800, "cpu_pct": 4.3, "watts": 3.07, "rss_mb": 43}
{"t": 4, "temp_c": 53.0, "mhz": 762, "cpu_pct": 3.5, "watts": 2.95, "rss_mb": 43}
{"t": 5, "temp_c": 54.0, "mhz": 1200, "cpu_pct": 5.1, "watts": 2.96, "rss_mb": 43}
{"t": 6, "temp_c": 54.0, "mhz": 707, "cpu_pct": 6.0, "watts": 3.12, "rss_mb": 43}
{"t": 7, "temp_c": 54.0, "mhz": 985, "cpu_pct": 4.0, "watts": 2.13, "rss_mb": 43}
{"t": 8, "temp_c": 54.0, "mhz": 801, "cpu_pct": 4.0, "watts": 3.00, "rss_mb": 43}
{"t": 9, "temp_c": 54.0, "mhz": 756, "cpu_pct": 4.0, "watts": 3.02, "rss_mb": 43}
{"t": 10, "temp_c": 53.0, "mhz": 798, "cpu_pct": 4.3, "watts": 2.98, "rss_mb": 43}
{"t": 11, "temp_c": 54.0, "mhz": 763, "cpu_pct": 4.8, "watts": 2.96, "rss_mb": 43}
{"t": 12, "temp_c": 53.0, "mhz": 800, "cpu_pct": 4.3, "watts": 3.04, "rss_mb": 43}
{"t": 13, "temp_c": 54.0, "mhz": 1561, "cpu_pct": 3.5, "watts": 2.91, "rss_mb": 43}
{"t": 14, "temp_c": 53.0, "mhz": 784, "cpu_pct": 3.3, "watts": 2.14, "rss_mb": 43}
{"t": 15, "temp_c": 52.0, "mhz": 793, "cpu_pct": 3.5, "watts": 3.03, "rss_mb": 43}
{"t": 16, "temp_c": 53.0, "mhz": 790, "cpu_pct": 5.0, "watts": 2.98, "rss_mb": 43}
{"t": 17, "temp_c": 54.0, "mhz": 788, "cpu_pct": 5.3, "watts": 2.91, "rss_mb": 43}
{"t": 18, "temp_c": 54.0, "mhz": 797, "cpu_pct": 5.0, "watts": 3.01, "rss_mb": 43}
{"t": 19, "temp_c": 54.0, "mhz": 1884, "cpu_pct": 3.5, "watts": 2.77, "rss_mb": 43}
{"t": 20, "temp_c": 53.0, "mhz": 860, "cpu_pct": 4.0, "watts": 3.00, "rss_mb": 43}
{"t": 21, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.3, "watts": 2.28, "rss_mb": 43}
{"t": 22, "temp_c": 52.0, "mhz": 774, "cpu_pct": 3.5, "watts": 2.94, "rss_mb": 43}
{"t": 23, "temp_c": 53.0, "mhz": 935, "cpu_pct": 5.5, "watts": 2.94, "rss_mb": 43}
{"t": 24, "temp_c": 53.0, "mhz": 779, "cpu_pct": 4.5, "watts": 2.93, "rss_mb": 43}
{"t": 25, "temp_c": 54.0, "mhz": 1192, "cpu_pct": 4.5, "watts": 2.91, "rss_mb": 43}
{"t": 26, "temp_c": 54.0, "mhz": 792, "cpu_pct": 4.0, "watts": 2.93, "rss_mb": 43}
{"t": 27, "temp_c": 52.0, "mhz": 798, "cpu_pct": 3.8, "watts": 2.97, "rss_mb": 43}
{"t": 28, "temp_c": 54.0, "mhz": 800, "cpu_pct": 12.9, "watts": 2.48, "rss_mb": 43}
{"t": 29, "temp_c": 52.0, "mhz": 798, "cpu_pct": 15.4, "watts": 4.12, "rss_mb": 43}
{"t": 30, "temp_c": 53.0, "mhz": 982, "cpu_pct": 4.5, "watts": 2.91, "rss_mb": 43}
{"t": 31, "temp_c": 52.0, "mhz": 792, "cpu_pct": 2.3, "watts": 2.92, "rss_mb": 43}
{"t": 32, "temp_c": 52.0, "mhz": 800, "cpu_pct": 5.3, "watts": 2.92, "rss_mb": 43}
{"t": 33, "temp_c": 52.0, "mhz": 754, "cpu_pct": 4.5, "watts": 2.97, "rss_mb": 43}
{"t": 34, "temp_c": 52.0, "mhz": 763, "cpu_pct": 4.8, "watts": 2.92, "rss_mb": 43}
{"t": 35, "temp_c": 53.0, "mhz": 758, "cpu_pct": 4.8, "watts": 2.09, "rss_mb": 43}
{"t": 36, "temp_c": 53.0, "mhz": 2677, "cpu_pct": 4.8, "watts": 3.08, "rss_mb": 43}
{"t": 37, "temp_c": 52.0, "mhz": 799, "cpu_pct": 2.5, "watts": 2.92, "rss_mb": 43}
{"t": 38, "temp_c": 52.0, "mhz": 795, "cpu_pct": 4.0, "watts": 2.89, "rss_mb": 43}
{"t": 39, "temp_c": 52.0, "mhz": 794, "cpu_pct": 5.0, "watts": 3.02, "rss_mb": 43}
{"t": 40, "temp_c": 52.0, "mhz": 778, "cpu_pct": 4.3, "watts": 2.95, "rss_mb": 43}
{"t": 41, "temp_c": 52.0, "mhz": 800, "cpu_pct": 5.0, "watts": 3.04, "rss_mb": 43}
{"t": 42, "temp_c": 54.0, "mhz": 1473, "cpu_pct": 4.5, "watts": 2.17, "rss_mb": 43}
{"t": 43, "temp_c": 52.0, "mhz": 789, "cpu_pct": 3.0, "watts": 2.92, "rss_mb": 43}
{"t": 44, "temp_c": 52.0, "mhz": 796, "cpu_pct": 4.0, "watts": 2.88, "rss_mb": 43}
{"t": 45, "temp_c": 52.0, "mhz": 762, "cpu_pct": 4.5, "watts": 2.99, "rss_mb": 43}
{"t": 46, "temp_c": 51.0, "mhz": 698, "cpu_pct": 4.6, "watts": 2.93, "rss_mb": 43}
{"t": 47, "temp_c": 53.0, "mhz": 792, "cpu_pct": 4.8, "watts": 3.01, "rss_mb": 43}
{"t": 48, "temp_c": 54.0, "mhz": 2799, "cpu_pct": 4.0, "watts": 2.92, "rss_mb": 43}
{"t": 49, "temp_c": 51.0, "mhz": 785, "cpu_pct": 3.0, "watts": 2.14, "rss_mb": 43}
{"t": 50, "temp_c": 52.0, "mhz": 801, "cpu_pct": 5.0, "watts": 2.91, "rss_mb": 43}
{"t": 51, "temp_c": 51.0, "mhz": 796, "cpu_pct": 3.8, "watts": 3.00, "rss_mb": 43}
{"t": 52, "temp_c": 51.0, "mhz": 800, "cpu_pct": 4.5, "watts": 2.95, "rss_mb": 43}
{"t": 53, "temp_c": 52.0, "mhz": 777, "cpu_pct": 4.3, "watts": 2.93, "rss_mb": 43}
{"t": 54, "temp_c": 52.0, "mhz": 2678, "cpu_pct": 3.8, "watts": 2.75, "rss_mb": 43}
{"t": 55, "temp_c": 51.0, "mhz": 800, "cpu_pct": 2.8, "watts": 2.25, "rss_mb": 43}
{"t": 56, "temp_c": 52.0, "mhz": 801, "cpu_pct": 5.5, "watts": 2.91, "rss_mb": 43}
{"t": 57, "temp_c": 51.0, "mhz": 764, "cpu_pct": 5.0, "watts": 3.11, "rss_mb": 43}
{"t": 58, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.3, "watts": 2.95, "rss_mb": 43}
{"t": 59, "temp_c": 59.0, "mhz": 2800, "cpu_pct": 21.9, "watts": 3.74, "rss_mb": 43}
{"t": 60, "temp_c": 51.0, "mhz": 782, "cpu_pct": 6.3, "watts": 3.67, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 52.8, "temp_c_max": 59.0, "mhz_mean": 991, "cpu_pct_mean": 4.9, "watts_mean": 2.91}

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 53.0, "mhz": 776, "cpu_pct": 4.5, "watts": 3.53, "rss_mb": 42}
{"t": 2, "temp_c": 54.0, "mhz": 794, "cpu_pct": 5.5, "watts": 2.97, "rss_mb": 43}
{"t": 3, "temp_c": 55.0, "mhz": 2773, "cpu_pct": 4.0, "watts": 2.82, "rss_mb": 43}
{"t": 4, "temp_c": 61.0, "mhz": 2800, "cpu_pct": 7.8, "watts": 3.25, "rss_mb": 43}
{"t": 5, "temp_c": 52.0, "mhz": 800, "cpu_pct": 6.6, "watts": 3.21, "rss_mb": 43}
{"t": 6, "temp_c": 52.0, "mhz": 791, "cpu_pct": 3.8, "watts": 2.18, "rss_mb": 43}
{"t": 7, "temp_c": 53.0, "mhz": 790, "cpu_pct": 3.5, "watts": 2.86, "rss_mb": 43}
{"t": 8, "temp_c": 53.0, "mhz": 878, "cpu_pct": 4.8, "watts": 3.03, "rss_mb": 43}
{"t": 9, "temp_c": 53.0, "mhz": 780, "cpu_pct": 3.5, "watts": 2.97, "rss_mb": 43}
{"t": 10, "temp_c": 53.0, "mhz": 774, "cpu_pct": 4.5, "watts": 2.97, "rss_mb": 43}
{"t": 11, "temp_c": 53.0, "mhz": 772, "cpu_pct": 4.8, "watts": 2.97, "rss_mb": 43}
{"t": 12, "temp_c": 52.0, "mhz": 614, "cpu_pct": 4.0, "watts": 2.93, "rss_mb": 43}
{"t": 13, "temp_c": 53.0, "mhz": 798, "cpu_pct": 4.0, "watts": 2.16, "rss_mb": 43}
{"t": 14, "temp_c": 54.0, "mhz": 2847, "cpu_pct": 5.1, "watts": 2.94, "rss_mb": 43}
{"t": 15, "temp_c": 52.0, "mhz": 784, "cpu_pct": 3.0, "watts": 2.94, "rss_mb": 43}
{"t": 16, "temp_c": 52.0, "mhz": 787, "cpu_pct": 3.5, "watts": 2.93, "rss_mb": 43}
{"t": 17, "temp_c": 52.0, "mhz": 786, "cpu_pct": 5.6, "watts": 3.09, "rss_mb": 43}
{"t": 18, "temp_c": 53.0, "mhz": 767, "cpu_pct": 4.5, "watts": 2.92, "rss_mb": 43}
{"t": 19, "temp_c": 52.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.14, "rss_mb": 43}
{"t": 20, "temp_c": 53.0, "mhz": 2853, "cpu_pct": 3.8, "watts": 3.02, "rss_mb": 43}
{"t": 21, "temp_c": 51.0, "mhz": 792, "cpu_pct": 3.0, "watts": 2.92, "rss_mb": 43}
{"t": 22, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.93, "rss_mb": 43}
{"t": 23, "temp_c": 51.0, "mhz": 784, "cpu_pct": 5.5, "watts": 3.00, "rss_mb": 43}
{"t": 24, "temp_c": 52.0, "mhz": 793, "cpu_pct": 4.0, "watts": 2.96, "rss_mb": 43}
{"t": 25, "temp_c": 52.0, "mhz": 758, "cpu_pct": 4.0, "watts": 2.94, "rss_mb": 43}
{"t": 26, "temp_c": 54.0, "mhz": 2843, "cpu_pct": 4.8, "watts": 2.20, "rss_mb": 43}
{"t": 27, "temp_c": 51.0, "mhz": 796, "cpu_pct": 2.5, "watts": 2.92, "rss_mb": 43}
{"t": 28, "temp_c": 52.0, "mhz": 830, "cpu_pct": 4.5, "watts": 2.89, "rss_mb": 43}
{"t": 29, "temp_c": 54.0, "mhz": 737, "cpu_pct": 25.5, "watts": 4.67, "rss_mb": 43}
{"t": 30, "temp_c": 52.0, "mhz": 787, "cpu_pct": 4.3, "watts": 3.12, "rss_mb": 43}
{"t": 31, "temp_c": 52.0, "mhz": 891, "cpu_pct": 3.5, "watts": 2.85, "rss_mb": 43}
{"t": 32, "temp_c": 52.0, "mhz": 900, "cpu_pct": 4.1, "watts": 2.89, "rss_mb": 43}
{"t": 33, "temp_c": 51.0, "mhz": 799, "cpu_pct": 3.8, "watts": 2.27, "rss_mb": 43}
{"t": 34, "temp_c": 51.0, "mhz": 792, "cpu_pct": 3.8, "watts": 2.96, "rss_mb": 43}
{"t": 35, "temp_c": 52.0, "mhz": 791, "cpu_pct": 6.6, "watts": 3.07, "rss_mb": 43}
{"t": 36, "temp_c": 52.0, "mhz": 790, "cpu_pct": 4.5, "watts": 2.83, "rss_mb": 43}
{"t": 37, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.80, "rss_mb": 43}
{"t": 38, "temp_c": 51.0, "mhz": 797, "cpu_pct": 3.3, "watts": 2.98, "rss_mb": 43}
{"t": 39, "temp_c": 52.0, "mhz": 767, "cpu_pct": 4.8, "watts": 2.97, "rss_mb": 43}
{"t": 40, "temp_c": 52.0, "mhz": 800, "cpu_pct": 4.0, "watts": 2.30, "rss_mb": 43}
{"t": 41, "temp_c": 51.0, "mhz": 784, "cpu_pct": 5.3, "watts": 2.92, "rss_mb": 43}
{"t": 42, "temp_c": 52.0, "mhz": 703, "cpu_pct": 3.8, "watts": 2.83, "rss_mb": 43}
{"t": 43, "temp_c": 52.0, "mhz": 754, "cpu_pct": 3.8, "watts": 2.90, "rss_mb": 43}
{"t": 44, "temp_c": 51.0, "mhz": 775, "cpu_pct": 3.5, "watts": 3.01, "rss_mb": 43}
{"t": 45, "temp_c": 51.0, "mhz": 791, "cpu_pct": 4.5, "watts": 2.94, "rss_mb": 43}
{"t": 46, "temp_c": 51.0, "mhz": 770, "cpu_pct": 3.8, "watts": 2.95, "rss_mb": 43}
{"t": 47, "temp_c": 51.0, "mhz": 800, "cpu_pct": 5.5, "watts": 2.17, "rss_mb": 43}
{"t": 48, "temp_c": 51.0, "mhz": 797, "cpu_pct": 4.0, "watts": 2.91, "rss_mb": 43}
{"t": 49, "temp_c": 52.0, "mhz": 1821, "cpu_pct": 4.3, "watts": 2.90, "rss_mb": 43}
{"t": 50, "temp_c": 51.0, "mhz": 750, "cpu_pct": 3.8, "watts": 2.97, "rss_mb": 43}
{"t": 51, "temp_c": 52.0, "mhz": 786, "cpu_pct": 3.8, "watts": 2.90, "rss_mb": 43}
{"t": 52, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.3, "watts": 2.92, "rss_mb": 43}
{"t": 53, "temp_c": 51.0, "mhz": 718, "cpu_pct": 6.3, "watts": 3.06, "rss_mb": 43}
{"t": 54, "temp_c": 52.0, "mhz": 758, "cpu_pct": 4.3, "watts": 2.13, "rss_mb": 43}
{"t": 55, "temp_c": 61.0, "mhz": 2800, "cpu_pct": 20.6, "watts": 3.91, "rss_mb": 43}
{"t": 56, "temp_c": 51.0, "mhz": 792, "cpu_pct": 5.0, "watts": 3.58, "rss_mb": 43}
{"t": 57, "temp_c": 51.0, "mhz": 757, "cpu_pct": 3.8, "watts": 2.91, "rss_mb": 43}
{"t": 58, "temp_c": 51.0, "mhz": 800, "cpu_pct": 3.8, "watts": 2.90, "rss_mb": 43}
{"t": 59, "temp_c": 52.0, "mhz": 794, "cpu_pct": 5.3, "watts": 2.97, "rss_mb": 43}
{"t": 60, "temp_c": 52.0, "mhz": 1399, "cpu_pct": 6.8, "watts": 3.10, "rss_mb": 43}
{"summary": "idle", "secs": 60, "temp_c_mean": 52.3, "temp_c_max": 61.0, "mhz_mean": 1016, "cpu_pct_mean": 5.0, "watts_mean": 2.92}

View File

@ -0,0 +1,121 @@
{"t": 1, "temp_c": 78.0, "mhz": 2700, "cpu_pct": 91.1, "watts": 9.43, "rss_mb": 543, "done": 0, "queue": 0}
{"t": 2, "temp_c": 68.0, "mhz": 2800, "cpu_pct": 70.2, "watts": 8.25, "rss_mb": 259, "done": 1, "queue": 0}
{"t": 3, "temp_c": 79.0, "mhz": 2507, "cpu_pct": 91.5, "watts": 7.15, "rss_mb": 622, "done": 1, "queue": 0}
{"t": 4, "temp_c": 72.0, "mhz": 2800, "cpu_pct": 69.2, "watts": 8.38, "rss_mb": 292, "done": 2, "queue": 0}
{"t": 5, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 90.7, "watts": 9.03, "rss_mb": 643, "done": 2, "queue": 0}
{"t": 6, "temp_c": 68.0, "mhz": 2800, "cpu_pct": 66.5, "watts": 8.15, "rss_mb": 312, "done": 3, "queue": 0}
{"t": 7, "temp_c": 76.0, "mhz": 2700, "cpu_pct": 91.5, "watts": 9.05, "rss_mb": 579, "done": 3, "queue": 0}
{"t": 8, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 68.1, "watts": 8.22, "rss_mb": 401, "done": 4, "queue": 0}
{"t": 9, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 91.2, "watts": 9.22, "rss_mb": 699, "done": 4, "queue": 0}
{"t": 10, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 67.8, "watts": 7.97, "rss_mb": 466, "done": 5, "queue": 0}
{"t": 11, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 92.0, "watts": 6.97, "rss_mb": 707, "done": 5, "queue": 0}
{"t": 12, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 66.3, "watts": 8.36, "rss_mb": 474, "done": 6, "queue": 0}
{"t": 13, "temp_c": 77.0, "mhz": 2200, "cpu_pct": 92.0, "watts": 8.96, "rss_mb": 698, "done": 6, "queue": 0}
{"t": 14, "temp_c": 73.0, "mhz": 2799, "cpu_pct": 64.8, "watts": 8.08, "rss_mb": 480, "done": 7, "queue": 0}
{"t": 15, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 92.5, "watts": 9.09, "rss_mb": 701, "done": 7, "queue": 0}
{"t": 16, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 67.7, "watts": 8.08, "rss_mb": 484, "done": 8, "queue": 0}
{"t": 17, "temp_c": 79.0, "mhz": 2436, "cpu_pct": 92.5, "watts": 9.03, "rss_mb": 705, "done": 8, "queue": 0}
{"t": 18, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 66.4, "watts": 7.98, "rss_mb": 488, "done": 9, "queue": 0}
{"t": 19, "temp_c": 79.0, "mhz": 2500, "cpu_pct": 92.3, "watts": 9.24, "rss_mb": 712, "done": 9, "queue": 0}
{"t": 20, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 68.6, "watts": 6.06, "rss_mb": 518, "done": 10, "queue": 0}
{"t": 21, "temp_c": 77.0, "mhz": 2600, "cpu_pct": 92.0, "watts": 9.08, "rss_mb": 714, "done": 10, "queue": 0}
{"t": 22, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 67.2, "watts": 8.11, "rss_mb": 521, "done": 11, "queue": 0}
{"t": 23, "temp_c": 77.0, "mhz": 2756, "cpu_pct": 93.0, "watts": 8.98, "rss_mb": 715, "done": 11, "queue": 0}
{"t": 24, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 67.2, "watts": 8.06, "rss_mb": 530, "done": 12, "queue": 0}
{"t": 25, "temp_c": 77.0, "mhz": 2700, "cpu_pct": 91.5, "watts": 9.08, "rss_mb": 724, "done": 12, "queue": 0}
{"t": 26, "temp_c": 81.0, "mhz": 2800, "cpu_pct": 67.2, "watts": 7.91, "rss_mb": 541, "done": 13, "queue": 0}
{"t": 27, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 95.2, "watts": 6.97, "rss_mb": 725, "done": 13, "queue": 0}
{"t": 28, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 74.8, "watts": 8.50, "rss_mb": 544, "done": 14, "queue": 0}
{"t": 29, "temp_c": 76.0, "mhz": 2701, "cpu_pct": 92.5, "watts": 8.90, "rss_mb": 726, "done": 14, "queue": 0}
{"t": 30, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 68.0, "watts": 8.05, "rss_mb": 563, "done": 15, "queue": 0}
{"t": 31, "temp_c": 78.0, "mhz": 2700, "cpu_pct": 92.1, "watts": 8.87, "rss_mb": 727, "done": 15, "queue": 0}
{"t": 32, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 66.1, "watts": 7.93, "rss_mb": 602, "done": 16, "queue": 0}
{"t": 33, "temp_c": 79.0, "mhz": 2400, "cpu_pct": 92.2, "watts": 9.04, "rss_mb": 727, "done": 16, "queue": 0}
{"t": 34, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 66.7, "watts": 7.80, "rss_mb": 574, "done": 17, "queue": 0}
{"t": 35, "temp_c": 77.0, "mhz": 2586, "cpu_pct": 91.5, "watts": 9.18, "rss_mb": 745, "done": 17, "queue": 0}
{"t": 36, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 69.3, "watts": 6.03, "rss_mb": 603, "done": 18, "queue": 0}
{"t": 37, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 90.8, "watts": 8.86, "rss_mb": 731, "done": 18, "queue": 0}
{"t": 38, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 67.2, "watts": 8.03, "rss_mb": 584, "done": 19, "queue": 0}
{"t": 39, "temp_c": 77.0, "mhz": 2550, "cpu_pct": 91.8, "watts": 8.73, "rss_mb": 747, "done": 19, "queue": 0}
{"t": 40, "temp_c": 82.0, "mhz": 2799, "cpu_pct": 67.2, "watts": 7.98, "rss_mb": 614, "done": 20, "queue": 0}
{"t": 41, "temp_c": 77.0, "mhz": 2600, "cpu_pct": 91.2, "watts": 8.95, "rss_mb": 748, "done": 20, "queue": 0}
{"t": 42, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 67.2, "watts": 7.87, "rss_mb": 618, "done": 21, "queue": 0}
{"t": 43, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 91.0, "watts": 8.96, "rss_mb": 753, "done": 21, "queue": 0}
{"t": 44, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 67.6, "watts": 7.96, "rss_mb": 586, "done": 22, "queue": 0}
{"t": 45, "temp_c": 76.0, "mhz": 2464, "cpu_pct": 91.5, "watts": 6.90, "rss_mb": 759, "done": 22, "queue": 0}
{"t": 46, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 68.6, "watts": 8.08, "rss_mb": 605, "done": 23, "queue": 0}
{"t": 47, "temp_c": 78.0, "mhz": 2700, "cpu_pct": 91.0, "watts": 8.67, "rss_mb": 743, "done": 23, "queue": 0}
{"t": 48, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 67.8, "watts": 8.02, "rss_mb": 628, "done": 24, "queue": 0}
{"t": 49, "temp_c": 80.0, "mhz": 2787, "cpu_pct": 91.3, "watts": 8.87, "rss_mb": 743, "done": 24, "queue": 0}
{"t": 50, "temp_c": 88.0, "mhz": 2800, "cpu_pct": 67.9, "watts": 7.83, "rss_mb": 598, "done": 25, "queue": 0}
{"t": 51, "temp_c": 78.0, "mhz": 2642, "cpu_pct": 92.0, "watts": 8.89, "rss_mb": 762, "done": 25, "queue": 0}
{"t": 52, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 69.5, "watts": 7.97, "rss_mb": 637, "done": 26, "queue": 0}
{"t": 53, "temp_c": 77.0, "mhz": 2716, "cpu_pct": 91.5, "watts": 6.79, "rss_mb": 746, "done": 26, "queue": 0}
{"t": 54, "temp_c": 81.0, "mhz": 2800, "cpu_pct": 68.5, "watts": 8.08, "rss_mb": 649, "done": 27, "queue": 0}
{"t": 55, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 93.0, "watts": 8.69, "rss_mb": 751, "done": 27, "queue": 0}
{"t": 56, "temp_c": 87.0, "mhz": 2800, "cpu_pct": 68.5, "watts": 8.04, "rss_mb": 632, "done": 28, "queue": 0}
{"t": 57, "temp_c": 77.0, "mhz": 2701, "cpu_pct": 91.5, "watts": 8.92, "rss_mb": 751, "done": 28, "queue": 0}
{"t": 58, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 69.1, "watts": 7.85, "rss_mb": 654, "done": 29, "queue": 0}
{"t": 59, "temp_c": 77.0, "mhz": 2470, "cpu_pct": 96.2, "watts": 9.05, "rss_mb": 751, "done": 29, "queue": 0}
{"t": 60, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 70.9, "watts": 8.10, "rss_mb": 634, "done": 30, "queue": 0}
{"t": 61, "temp_c": 78.0, "mhz": 2701, "cpu_pct": 91.0, "watts": 6.80, "rss_mb": 752, "done": 30, "queue": 0}
{"t": 62, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 68.4, "watts": 8.13, "rss_mb": 660, "done": 31, "queue": 0}
{"t": 63, "temp_c": 77.0, "mhz": 2500, "cpu_pct": 91.5, "watts": 8.87, "rss_mb": 754, "done": 31, "queue": 0}
{"t": 64, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 67.5, "watts": 7.89, "rss_mb": 661, "done": 32, "queue": 0}
{"t": 65, "temp_c": 78.0, "mhz": 2585, "cpu_pct": 92.2, "watts": 8.98, "rss_mb": 755, "done": 32, "queue": 0}
{"t": 66, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 67.4, "watts": 8.05, "rss_mb": 672, "done": 33, "queue": 0}
{"t": 67, "temp_c": 77.0, "mhz": 2600, "cpu_pct": 91.7, "watts": 9.02, "rss_mb": 757, "done": 33, "queue": 0}
{"t": 68, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 66.9, "watts": 5.91, "rss_mb": 682, "done": 34, "queue": 0}
{"t": 69, "temp_c": 79.0, "mhz": 2778, "cpu_pct": 92.0, "watts": 8.82, "rss_mb": 773, "done": 34, "queue": 0}
{"t": 70, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 68.5, "watts": 7.94, "rss_mb": 641, "done": 35, "queue": 0}
{"t": 71, "temp_c": 78.0, "mhz": 2544, "cpu_pct": 91.0, "watts": 8.87, "rss_mb": 759, "done": 35, "queue": 0}
{"t": 72, "temp_c": 78.0, "mhz": 2300, "cpu_pct": 68.2, "watts": 7.88, "rss_mb": 693, "done": 36, "queue": 0}
{"t": 73, "temp_c": 79.0, "mhz": 2304, "cpu_pct": 92.1, "watts": 8.81, "rss_mb": 759, "done": 36, "queue": 0}
{"t": 74, "temp_c": 79.0, "mhz": 2801, "cpu_pct": 69.2, "watts": 8.11, "rss_mb": 671, "done": 37, "queue": 0}
{"t": 75, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 92.7, "watts": 8.81, "rss_mb": 761, "done": 37, "queue": 0}
{"t": 76, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 69.2, "watts": 6.00, "rss_mb": 683, "done": 38, "queue": 0}
{"t": 77, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 91.7, "watts": 8.64, "rss_mb": 761, "done": 38, "queue": 0}
{"t": 78, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 68.4, "watts": 7.99, "rss_mb": 679, "done": 39, "queue": 0}
{"t": 79, "temp_c": 78.0, "mhz": 2473, "cpu_pct": 91.5, "watts": 8.81, "rss_mb": 761, "done": 39, "queue": 0}
{"t": 80, "temp_c": 79.0, "mhz": 2799, "cpu_pct": 67.4, "watts": 7.79, "rss_mb": 684, "done": 40, "queue": 0}
{"t": 81, "temp_c": 77.0, "mhz": 2500, "cpu_pct": 93.2, "watts": 8.92, "rss_mb": 761, "done": 40, "queue": 0}
{"t": 82, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 69.0, "watts": 8.03, "rss_mb": 687, "done": 41, "queue": 0}
{"t": 83, "temp_c": 80.0, "mhz": 2400, "cpu_pct": 92.2, "watts": 8.87, "rss_mb": 761, "done": 41, "queue": 0}
{"t": 84, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 67.5, "watts": 5.93, "rss_mb": 692, "done": 42, "queue": 0}
{"t": 85, "temp_c": 76.0, "mhz": 2700, "cpu_pct": 93.0, "watts": 8.62, "rss_mb": 761, "done": 42, "queue": 0}
{"t": 86, "temp_c": 82.0, "mhz": 2300, "cpu_pct": 67.9, "watts": 7.85, "rss_mb": 695, "done": 43, "queue": 0}
{"t": 87, "temp_c": 79.0, "mhz": 2500, "cpu_pct": 91.3, "watts": 8.62, "rss_mb": 761, "done": 43, "queue": 0}
{"t": 88, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 68.9, "watts": 7.88, "rss_mb": 729, "done": 44, "queue": 0}
{"t": 89, "temp_c": 78.0, "mhz": 2400, "cpu_pct": 93.0, "watts": 8.75, "rss_mb": 779, "done": 44, "queue": 0}
{"t": 90, "temp_c": 80.0, "mhz": 2600, "cpu_pct": 82.7, "watts": 8.46, "rss_mb": 716, "done": 45, "queue": 0}
{"t": 91, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 92.8, "watts": 8.71, "rss_mb": 779, "done": 45, "queue": 0}
{"t": 92, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 67.8, "watts": 5.88, "rss_mb": 709, "done": 46, "queue": 0}
{"t": 93, "temp_c": 78.0, "mhz": 2478, "cpu_pct": 92.2, "watts": 8.65, "rss_mb": 779, "done": 46, "queue": 0}
{"t": 94, "temp_c": 81.0, "mhz": 2800, "cpu_pct": 69.4, "watts": 7.89, "rss_mb": 730, "done": 47, "queue": 0}
{"t": 95, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 92.5, "watts": 8.80, "rss_mb": 762, "done": 47, "queue": 0}
{"t": 96, "temp_c": 86.0, "mhz": 2700, "cpu_pct": 67.1, "watts": 7.85, "rss_mb": 730, "done": 48, "queue": 0}
{"t": 97, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 93.2, "watts": 8.69, "rss_mb": 779, "done": 48, "queue": 0}
{"t": 98, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 66.8, "watts": 7.91, "rss_mb": 730, "done": 49, "queue": 0}
{"t": 99, "temp_c": 77.0, "mhz": 2400, "cpu_pct": 92.5, "watts": 8.69, "rss_mb": 779, "done": 49, "queue": 0}
{"t": 100, "temp_c": 81.0, "mhz": 2800, "cpu_pct": 68.2, "watts": 8.11, "rss_mb": 723, "done": 50, "queue": 0}
{"t": 101, "temp_c": 79.0, "mhz": 2319, "cpu_pct": 92.2, "watts": 6.39, "rss_mb": 779, "done": 50, "queue": 0}
{"t": 102, "temp_c": 82.0, "mhz": 2799, "cpu_pct": 68.9, "watts": 7.91, "rss_mb": 719, "done": 51, "queue": 0}
{"t": 103, "temp_c": 78.0, "mhz": 2582, "cpu_pct": 92.3, "watts": 8.73, "rss_mb": 779, "done": 51, "queue": 0}
{"t": 104, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 68.5, "watts": 7.98, "rss_mb": 731, "done": 52, "queue": 0}
{"t": 105, "temp_c": 79.0, "mhz": 2400, "cpu_pct": 92.3, "watts": 8.83, "rss_mb": 779, "done": 52, "queue": 0}
{"t": 106, "temp_c": 80.0, "mhz": 2600, "cpu_pct": 66.5, "watts": 7.91, "rss_mb": 731, "done": 53, "queue": 0}
{"t": 107, "temp_c": 78.0, "mhz": 2700, "cpu_pct": 92.0, "watts": 8.43, "rss_mb": 763, "done": 53, "queue": 0}
{"t": 108, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 67.9, "watts": 5.85, "rss_mb": 731, "done": 54, "queue": 0}
{"t": 109, "temp_c": 78.0, "mhz": 2500, "cpu_pct": 92.2, "watts": 8.63, "rss_mb": 779, "done": 54, "queue": 0}
{"t": 110, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 68.8, "watts": 7.87, "rss_mb": 731, "done": 55, "queue": 0}
{"t": 111, "temp_c": 78.0, "mhz": 2700, "cpu_pct": 92.2, "watts": 8.77, "rss_mb": 763, "done": 55, "queue": 0}
{"t": 112, "temp_c": 86.0, "mhz": 2700, "cpu_pct": 68.9, "watts": 7.82, "rss_mb": 731, "done": 56, "queue": 0}
{"t": 113, "temp_c": 78.0, "mhz": 2600, "cpu_pct": 92.5, "watts": 8.60, "rss_mb": 763, "done": 56, "queue": 0}
{"t": 114, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 67.3, "watts": 8.08, "rss_mb": 731, "done": 57, "queue": 0}
{"t": 115, "temp_c": 77.0, "mhz": 2325, "cpu_pct": 92.5, "watts": 8.57, "rss_mb": 763, "done": 57, "queue": 0}
{"t": 116, "temp_c": 80.0, "mhz": 2500, "cpu_pct": 68.6, "watts": 7.92, "rss_mb": 731, "done": 58, "queue": 0}
{"t": 117, "temp_c": 77.0, "mhz": 2500, "cpu_pct": 93.0, "watts": 6.32, "rss_mb": 763, "done": 58, "queue": 0}
{"t": 118, "temp_c": 85.0, "mhz": 2600, "cpu_pct": 71.0, "watts": 7.98, "rss_mb": 731, "done": 59, "queue": 0}
{"t": 119, "temp_c": 79.0, "mhz": 2400, "cpu_pct": 93.8, "watts": 8.70, "rss_mb": 781, "done": 59, "queue": 0}
{"t": 120, "temp_c": 73.0, "mhz": 2800, "cpu_pct": 64.8, "watts": 7.78, "rss_mb": 385, "done": 60, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 120.0, "frames": 60, "frames_per_s": 0.500, "job_ms_p50": 1440, "latency_ms_p50": 1447, "latency_ms_p95": 1505, "latency_ms_max": 1563, "max_queue": 1, "backlog_at_end": 0, "cpu_s_per_frame": 2.302, "peak_rss_mb": 781, "temp_c_mean": 79.3, "temp_c_max": 88.0, "mhz_mean": 2663, "mhz_min": 2200, "cpu_pct_mean": 80.2, "watts_mean": 8.18}

View File

@ -0,0 +1,53 @@
16:07:41 cool-down 60 s
16:08:41 none: flat out 60 s
16:08:41 throttle counts: package 0 cores 0 0 0 0 temp 60000
16:09:42 none: paced 2000 ms 120 s
16:09:42 throttle counts: package 0 cores 0 0 0 0 temp 63000
16:11:43 throttle counts: package 0 cores 0 0 0 0 temp 59000
16:11:43 cool-down 60 s
every core capped at 1600 MHz ('/home/ggs/thermal-test/software/tools/thermal-sim/cap-cpu.sh restore' to undo)
16:12:43 cap1600: flat out 60 s
16:12:43 throttle counts: package 0 cores 0 0 0 0 temp 55000
16:13:45 cap1600: paced 2000 ms 120 s
16:13:45 throttle counts: package 0 cores 0 0 0 0 temp 54000
16:15:46 throttle counts: package 0 cores 0 0 0 0 temp 55000
CPU clock limits restored
16:15:46 cool-down 60 s
every core capped at 1200 MHz ('/home/ggs/thermal-test/software/tools/thermal-sim/cap-cpu.sh restore' to undo)
16:16:46 cap1200: flat out 60 s
16:16:46 throttle counts: package 0 cores 0 0 0 0 temp 51000
16:17:49 cap1200: paced 2000 ms 120 s
16:17:49 throttle counts: package 0 cores 0 0 0 0 temp 54000
16:19:51 throttle counts: package 0 cores 0 0 0 0 temp 54000
CPU clock limits restored
16:19:51 cool-down 60 s
every core capped at 800 MHz ('/home/ggs/thermal-test/software/tools/thermal-sim/cap-cpu.sh restore' to undo)
16:20:51 cap800: flat out 60 s
16:20:51 throttle counts: package 0 cores 0 0 0 0 temp 52000
16:21:54 cap800: paced 2000 ms 120 s
16:21:54 throttle counts: package 0 cores 0 0 0 0 temp 52000
16:23:59 throttle counts: package 0 cores 0 0 0 0 temp 53000
CPU clock limits restored
16:23:59 cool-down 60 s
package power capped at 6 W (was 10 W; '/home/ggs/thermal-test/software/tools/thermal-sim/cap-power.sh restore' to undo)
16:24:59 power6w: flat out 60 s
16:24:59 throttle counts: package 0 cores 0 0 0 0 temp 51000
16:26:01 power6w: paced 2000 ms 120 s
16:26:01 throttle counts: package 0 cores 0 0 0 0 temp 56000
16:28:02 throttle counts: package 0 cores 0 0 0 0 temp 58000
power limit restored to 10 W
16:28:02 cool-down 60 s
16:29:02 load2: 2 busy cores, paced 2000 ms 120 s
16:29:02 throttle counts: package 0 cores 0 0 0 0 temp 52000
nothing to restore (no cap set since boot)
nothing to restore (no cap set since boot)
16:51:52 throttle counts: package 0 cores 0 0 0 0 temp 54000
16:51:52 cool-down 60 s
every core capped at 800 MHz ('/home/ggs/thermal-test/software/tools/thermal-sim/cap-cpu.sh restore' to undo)
16:52:52 cap800+load2: paced 2000 ms 120 s
16:52:52 throttle counts: package 0 cores 0 0 0 0 temp 49000
16:54:57 throttle counts: package 0 cores 0 0 0 0 temp 52000
CPU clock limits restored
16:54:57 done
nothing to restore (no cap set since boot)
nothing to restore (no cap set since boot)

View File

@ -0,0 +1,78 @@
#!/bin/sh
# Throttling matrix on tower 111 (3 Delta, N5105): the 12-bit save pipeline under
# clock caps, a power cap and competing load. Tower program stopped. ~25 min.
# nohup sh ~/thermal-test/matrix.sh > ~/thermal-test/matrix.log 2>&1 &
set -u
cd ~/fgc-bench
R=~/thermal-test/results-tower111
O=/tmp/thermal-bench-out
B=./raw12_bench
SIM=~/thermal-test/software/tools/thermal-sim
mkdir -p "$R"
restore() {
sudo -n "$SIM/cap-cpu.sh" restore
sudo -n "$SIM/cap-power.sh" restore
pkill -f "stress-ng --cpu" 2>/dev/null
rm -f /dev/shm/fgc_rapl_run
rm -rf "$O"
}
trap restore EXIT INT TERM
touch /dev/shm/fgc_rapl_run
sudo -n sh -c 'while [ -e /dev/shm/fgc_rapl_run ]; do
cat /sys/class/powercap/intel-rapl:0/energy_uj > /dev/shm/fgc_rapl.tmp &&
chmod 644 /dev/shm/fgc_rapl.tmp && mv /dev/shm/fgc_rapl.tmp /dev/shm/fgc_rapl
sleep 0.25; done; rm -f /dev/shm/fgc_rapl' &
export FGC_RAPL=/dev/shm/fgc_rapl
sleep 1
step() { echo "$(date +%T) $*"; }
counts() {
echo "$(date +%T) throttle counts: package $(cat /sys/devices/system/cpu/cpu0/thermal_throttle/package_throttle_count)" \
"cores $(cat /sys/devices/system/cpu/cpu*/thermal_throttle/core_throttle_count | tr '\n' ' ')" \
"temp $(cat /sys/class/thermal/thermal_zone2/temp)"
}
# label: flat-out 60 s, then one frame per 2 s for 120 s
run() {
step "$1: flat out 60 s"; counts
$B pipeline synth $O c12_ea 1 4 60 0 3 1.0 > "$R/$1_flat.jsonl"
step "$1: paced 2000 ms 120 s"; counts
$B pipeline synth $O c12_ea 1 4 120 2000 3 1.0 > "$R/$1_paced.jsonl"
counts
}
cool() { step "cool-down $1 s"; $B idle synth $O "$1" > "$R/idle_$2.jsonl"; }
cool 60 start
run none
cool 60 a
for mhz in 1600 1200 800; do
sudo -n "$SIM/cap-cpu.sh" "$mhz"
run "cap${mhz}"
sudo -n "$SIM/cap-cpu.sh" restore
cool 60 "after${mhz}"
done
sudo -n "$SIM/cap-power.sh" 6
run power6w
sudo -n "$SIM/cap-power.sh" restore
cool 60 afterpower
step "load2: 2 busy cores, paced 2000 ms 120 s"; counts
stress-ng --cpu 2 --timeout 125s --quiet &
sp=$! # only this: a bare `wait` also waits for the power-reading loop, forever
sleep 2
$B pipeline synth $O c12_ea 1 4 120 2000 3 1.0 > "$R/load2_paced.jsonl"
wait "$sp"
counts
cool 60 afterload
sudo -n "$SIM/cap-cpu.sh" 800
step "cap800+load2: paced 2000 ms 120 s"; counts
stress-ng --cpu 2 --timeout 125s --quiet &
sp=$! # only this: a bare `wait` also waits for the power-reading loop, forever
sleep 2
$B pipeline synth $O c12_ea 1 4 120 2000 3 1.0 > "$R/cap800load2_paced.jsonl"
wait "$sp"
counts
sudo -n "$SIM/cap-cpu.sh" restore
step done

View File

@ -0,0 +1,61 @@
{"t": 1, "temp_c": 76.0, "mhz": 2848, "cpu_pct": 62.6, "watts": 8.44, "rss_mb": 535, "done": 0, "queue": 0}
{"t": 2, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 61.8, "watts": 8.75, "rss_mb": 617, "done": 1, "queue": 0}
{"t": 3, "temp_c": 75.0, "mhz": 2800, "cpu_pct": 57.6, "watts": 8.43, "rss_mb": 639, "done": 2, "queue": 0}
{"t": 4, "temp_c": 76.0, "mhz": 2821, "cpu_pct": 59.0, "watts": 8.51, "rss_mb": 581, "done": 3, "queue": 0}
{"t": 5, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 59.7, "watts": 8.71, "rss_mb": 686, "done": 4, "queue": 0}
{"t": 6, "temp_c": 82.0, "mhz": 2800, "cpu_pct": 68.8, "watts": 8.89, "rss_mb": 689, "done": 5, "queue": 0}
{"t": 7, "temp_c": 76.0, "mhz": 2874, "cpu_pct": 57.8, "watts": 6.11, "rss_mb": 681, "done": 6, "queue": 0}
{"t": 8, "temp_c": 70.0, "mhz": 2853, "cpu_pct": 60.6, "watts": 8.69, "rss_mb": 684, "done": 7, "queue": 0}
{"t": 9, "temp_c": 74.0, "mhz": 2831, "cpu_pct": 60.2, "watts": 8.79, "rss_mb": 688, "done": 8, "queue": 0}
{"t": 10, "temp_c": 72.0, "mhz": 2355, "cpu_pct": 60.1, "watts": 8.78, "rss_mb": 695, "done": 9, "queue": 0}
{"t": 11, "temp_c": 71.0, "mhz": 2801, "cpu_pct": 60.8, "watts": 8.76, "rss_mb": 697, "done": 10, "queue": 0}
{"t": 12, "temp_c": 75.0, "mhz": 2851, "cpu_pct": 60.5, "watts": 8.69, "rss_mb": 699, "done": 11, "queue": 0}
{"t": 13, "temp_c": 79.0, "mhz": 2500, "cpu_pct": 70.9, "watts": 9.14, "rss_mb": 701, "done": 12, "queue": 0}
{"t": 14, "temp_c": 66.0, "mhz": 1848, "cpu_pct": 59.6, "watts": 8.53, "rss_mb": 702, "done": 13, "queue": 0}
{"t": 15, "temp_c": 73.0, "mhz": 2800, "cpu_pct": 61.5, "watts": 8.77, "rss_mb": 704, "done": 14, "queue": 0}
{"t": 16, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 61.6, "watts": 8.77, "rss_mb": 706, "done": 15, "queue": 0}
{"t": 17, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 60.1, "watts": 8.63, "rss_mb": 706, "done": 16, "queue": 0}
{"t": 18, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 61.0, "watts": 6.58, "rss_mb": 723, "done": 17, "queue": 0}
{"t": 19, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 62.0, "watts": 8.73, "rss_mb": 708, "done": 18, "queue": 0}
{"t": 20, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 59.8, "watts": 8.67, "rss_mb": 725, "done": 19, "queue": 0}
{"t": 21, "temp_c": 68.0, "mhz": 2346, "cpu_pct": 60.4, "watts": 8.69, "rss_mb": 727, "done": 20, "queue": 0}
{"t": 22, "temp_c": 72.0, "mhz": 2856, "cpu_pct": 59.9, "watts": 8.57, "rss_mb": 728, "done": 21, "queue": 0}
{"t": 23, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 58.5, "watts": 8.44, "rss_mb": 729, "done": 22, "queue": 0}
{"t": 24, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 61.8, "watts": 8.87, "rss_mb": 713, "done": 23, "queue": 0}
{"t": 25, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 60.5, "watts": 8.65, "rss_mb": 713, "done": 24, "queue": 0}
{"t": 26, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 60.8, "watts": 8.83, "rss_mb": 730, "done": 25, "queue": 0}
{"t": 27, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 60.6, "watts": 8.71, "rss_mb": 715, "done": 26, "queue": 0}
{"t": 28, "temp_c": 86.0, "mhz": 2800, "cpu_pct": 60.7, "watts": 8.74, "rss_mb": 715, "done": 27, "queue": 0}
{"t": 29, "temp_c": 82.0, "mhz": 2800, "cpu_pct": 59.1, "watts": 8.59, "rss_mb": 715, "done": 28, "queue": 0}
{"t": 30, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 62.7, "watts": 6.71, "rss_mb": 716, "done": 29, "queue": 0}
{"t": 31, "temp_c": 75.0, "mhz": 2850, "cpu_pct": 63.4, "watts": 9.06, "rss_mb": 716, "done": 30, "queue": 0}
{"t": 32, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 60.5, "watts": 8.63, "rss_mb": 716, "done": 31, "queue": 0}
{"t": 33, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 60.2, "watts": 8.64, "rss_mb": 716, "done": 32, "queue": 0}
{"t": 34, "temp_c": 84.0, "mhz": 2800, "cpu_pct": 60.2, "watts": 8.60, "rss_mb": 716, "done": 33, "queue": 0}
{"t": 35, "temp_c": 82.0, "mhz": 2854, "cpu_pct": 63.1, "watts": 8.86, "rss_mb": 732, "done": 34, "queue": 0}
{"t": 36, "temp_c": 79.0, "mhz": 2800, "cpu_pct": 60.2, "watts": 8.73, "rss_mb": 717, "done": 35, "queue": 0}
{"t": 37, "temp_c": 73.0, "mhz": 2804, "cpu_pct": 61.4, "watts": 8.82, "rss_mb": 717, "done": 36, "queue": 0}
{"t": 38, "temp_c": 75.0, "mhz": 2359, "cpu_pct": 60.7, "watts": 8.81, "rss_mb": 717, "done": 37, "queue": 0}
{"t": 39, "temp_c": 75.0, "mhz": 2876, "cpu_pct": 60.7, "watts": 8.87, "rss_mb": 717, "done": 38, "queue": 0}
{"t": 40, "temp_c": 76.0, "mhz": 2874, "cpu_pct": 61.1, "watts": 8.77, "rss_mb": 717, "done": 39, "queue": 0}
{"t": 41, "temp_c": 80.0, "mhz": 1841, "cpu_pct": 60.8, "watts": 6.49, "rss_mb": 717, "done": 40, "queue": 0}
{"t": 42, "temp_c": 78.0, "mhz": 2876, "cpu_pct": 60.4, "watts": 8.64, "rss_mb": 717, "done": 41, "queue": 0}
{"t": 43, "temp_c": 75.0, "mhz": 2850, "cpu_pct": 61.2, "watts": 8.78, "rss_mb": 718, "done": 42, "queue": 0}
{"t": 44, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 72.2, "watts": 8.98, "rss_mb": 718, "done": 43, "queue": 0}
{"t": 45, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 60.1, "watts": 8.55, "rss_mb": 734, "done": 44, "queue": 0}
{"t": 46, "temp_c": 72.0, "mhz": 2862, "cpu_pct": 61.7, "watts": 8.82, "rss_mb": 735, "done": 45, "queue": 0}
{"t": 47, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 56.9, "watts": 8.28, "rss_mb": 735, "done": 46, "queue": 0}
{"t": 48, "temp_c": 84.0, "mhz": 2818, "cpu_pct": 60.3, "watts": 8.64, "rss_mb": 719, "done": 47, "queue": 0}
{"t": 49, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 60.8, "watts": 8.68, "rss_mb": 717, "done": 48, "queue": 0}
{"t": 50, "temp_c": 85.0, "mhz": 2833, "cpu_pct": 60.9, "watts": 8.86, "rss_mb": 736, "done": 49, "queue": 0}
{"t": 51, "temp_c": 85.0, "mhz": 2800, "cpu_pct": 61.1, "watts": 8.66, "rss_mb": 723, "done": 50, "queue": 0}
{"t": 52, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 60.1, "watts": 8.77, "rss_mb": 703, "done": 51, "queue": 0}
{"t": 53, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 60.1, "watts": 6.81, "rss_mb": 691, "done": 52, "queue": 0}
{"t": 54, "temp_c": 78.0, "mhz": 2823, "cpu_pct": 61.7, "watts": 8.68, "rss_mb": 688, "done": 53, "queue": 0}
{"t": 55, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 56.5, "watts": 8.64, "rss_mb": 589, "done": 54, "queue": 0}
{"t": 56, "temp_c": 80.0, "mhz": 2830, "cpu_pct": 59.9, "watts": 8.16, "rss_mb": 610, "done": 55, "queue": 0}
{"t": 57, "temp_c": 78.0, "mhz": 2872, "cpu_pct": 60.2, "watts": 8.80, "rss_mb": 608, "done": 56, "queue": 0}
{"t": 58, "temp_c": 75.0, "mhz": 2850, "cpu_pct": 60.8, "watts": 8.67, "rss_mb": 614, "done": 57, "queue": 0}
{"t": 59, "temp_c": 83.0, "mhz": 2800, "cpu_pct": 60.4, "watts": 8.71, "rss_mb": 592, "done": 58, "queue": 0}
{"t": 60, "temp_c": 61.0, "mhz": 757, "cpu_pct": 57.6, "watts": 8.60, "rss_mb": 392, "done": 59, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 0, "secs": 60.0, "frames": 59, "frames_per_s": 0.983, "job_ms_p50": 994, "latency_ms_p50": 1009, "latency_ms_p95": 1094, "latency_ms_max": 1126, "max_queue": 0, "backlog_at_end": 0, "cpu_s_per_frame": 2.331, "peak_rss_mb": 736, "temp_c_mean": 78.0, "temp_c_max": 86.0, "mhz_mean": 2724, "mhz_min": 757, "cpu_pct_mean": 60.9, "watts_mean": 8.52}

View File

@ -0,0 +1,121 @@
{"t": 1, "temp_c": 80.0, "mhz": 2824, "cpu_pct": 62.4, "watts": 8.60, "rss_mb": 535, "done": 0, "queue": 0}
{"t": 2, "temp_c": 57.0, "mhz": 2805, "cpu_pct": 7.0, "watts": 3.96, "rss_mb": 247, "done": 1, "queue": 0}
{"t": 3, "temp_c": 66.0, "mhz": 2398, "cpu_pct": 61.0, "watts": 8.51, "rss_mb": 376, "done": 1, "queue": 0}
{"t": 4, "temp_c": 56.0, "mhz": 800, "cpu_pct": 6.1, "watts": 3.49, "rss_mb": 283, "done": 2, "queue": 0}
{"t": 5, "temp_c": 71.0, "mhz": 2801, "cpu_pct": 61.5, "watts": 8.65, "rss_mb": 397, "done": 2, "queue": 0}
{"t": 6, "temp_c": 56.0, "mhz": 800, "cpu_pct": 4.8, "watts": 2.27, "rss_mb": 304, "done": 3, "queue": 0}
{"t": 7, "temp_c": 77.0, "mhz": 2828, "cpu_pct": 61.6, "watts": 7.53, "rss_mb": 580, "done": 3, "queue": 0}
{"t": 8, "temp_c": 56.0, "mhz": 2700, "cpu_pct": 5.8, "watts": 4.39, "rss_mb": 395, "done": 4, "queue": 0}
{"t": 9, "temp_c": 63.0, "mhz": 1427, "cpu_pct": 60.5, "watts": 7.96, "rss_mb": 543, "done": 4, "queue": 0}
{"t": 10, "temp_c": 56.0, "mhz": 1135, "cpu_pct": 4.3, "watts": 3.73, "rss_mb": 450, "done": 5, "queue": 0}
{"t": 11, "temp_c": 63.0, "mhz": 961, "cpu_pct": 61.0, "watts": 8.18, "rss_mb": 517, "done": 5, "queue": 0}
{"t": 12, "temp_c": 55.0, "mhz": 782, "cpu_pct": 5.3, "watts": 3.37, "rss_mb": 462, "done": 6, "queue": 0}
{"t": 13, "temp_c": 65.0, "mhz": 2225, "cpu_pct": 61.7, "watts": 8.44, "rss_mb": 560, "done": 6, "queue": 0}
{"t": 14, "temp_c": 57.0, "mhz": 800, "cpu_pct": 21.2, "watts": 4.37, "rss_mb": 467, "done": 7, "queue": 0}
{"t": 15, "temp_c": 65.0, "mhz": 2900, "cpu_pct": 62.1, "watts": 6.43, "rss_mb": 562, "done": 7, "queue": 0}
{"t": 16, "temp_c": 56.0, "mhz": 2350, "cpu_pct": 6.0, "watts": 4.45, "rss_mb": 470, "done": 8, "queue": 0}
{"t": 17, "temp_c": 63.0, "mhz": 1287, "cpu_pct": 60.7, "watts": 7.77, "rss_mb": 473, "done": 9, "queue": 0}
{"t": 18, "temp_c": 56.0, "mhz": 800, "cpu_pct": 4.3, "watts": 3.61, "rss_mb": 473, "done": 9, "queue": 0}
{"t": 19, "temp_c": 64.0, "mhz": 1260, "cpu_pct": 60.6, "watts": 8.03, "rss_mb": 478, "done": 9, "queue": 0}
{"t": 20, "temp_c": 55.0, "mhz": 820, "cpu_pct": 3.5, "watts": 3.44, "rss_mb": 478, "done": 10, "queue": 0}
{"t": 21, "temp_c": 63.0, "mhz": 964, "cpu_pct": 60.9, "watts": 8.29, "rss_mb": 561, "done": 10, "queue": 0}
{"t": 22, "temp_c": 58.0, "mhz": 2025, "cpu_pct": 7.3, "watts": 3.33, "rss_mb": 479, "done": 11, "queue": 0}
{"t": 23, "temp_c": 63.0, "mhz": 1983, "cpu_pct": 60.6, "watts": 8.44, "rss_mb": 480, "done": 12, "queue": 0}
{"t": 24, "temp_c": 57.0, "mhz": 2801, "cpu_pct": 5.0, "watts": 3.12, "rss_mb": 480, "done": 12, "queue": 0}
{"t": 25, "temp_c": 63.0, "mhz": 1899, "cpu_pct": 60.3, "watts": 6.72, "rss_mb": 481, "done": 13, "queue": 0}
{"t": 26, "temp_c": 57.0, "mhz": 2800, "cpu_pct": 3.8, "watts": 3.97, "rss_mb": 490, "done": 13, "queue": 0}
{"t": 27, "temp_c": 61.0, "mhz": 988, "cpu_pct": 59.7, "watts": 7.83, "rss_mb": 484, "done": 14, "queue": 0}
{"t": 28, "temp_c": 55.0, "mhz": 1757, "cpu_pct": 5.1, "watts": 3.63, "rss_mb": 484, "done": 14, "queue": 0}
{"t": 29, "temp_c": 63.0, "mhz": 1505, "cpu_pct": 60.5, "watts": 8.21, "rss_mb": 485, "done": 15, "queue": 0}
{"t": 30, "temp_c": 56.0, "mhz": 1597, "cpu_pct": 4.8, "watts": 3.31, "rss_mb": 485, "done": 15, "queue": 0}
{"t": 31, "temp_c": 62.0, "mhz": 895, "cpu_pct": 60.2, "watts": 8.52, "rss_mb": 541, "done": 15, "queue": 0}
{"t": 32, "temp_c": 68.0, "mhz": 2800, "cpu_pct": 5.3, "watts": 2.99, "rss_mb": 509, "done": 16, "queue": 0}
{"t": 33, "temp_c": 60.0, "mhz": 1018, "cpu_pct": 58.5, "watts": 6.76, "rss_mb": 487, "done": 17, "queue": 0}
{"t": 34, "temp_c": 65.0, "mhz": 2800, "cpu_pct": 6.1, "watts": 4.11, "rss_mb": 519, "done": 17, "queue": 0}
{"t": 35, "temp_c": 61.0, "mhz": 1461, "cpu_pct": 59.6, "watts": 7.82, "rss_mb": 489, "done": 18, "queue": 0}
{"t": 36, "temp_c": 58.0, "mhz": 2800, "cpu_pct": 5.5, "watts": 3.75, "rss_mb": 503, "done": 18, "queue": 0}
{"t": 37, "temp_c": 61.0, "mhz": 1150, "cpu_pct": 59.9, "watts": 8.21, "rss_mb": 490, "done": 19, "queue": 0}
{"t": 38, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 6.3, "watts": 3.14, "rss_mb": 541, "done": 19, "queue": 0}
{"t": 39, "temp_c": 61.0, "mhz": 770, "cpu_pct": 57.7, "watts": 8.57, "rss_mb": 490, "done": 20, "queue": 0}
{"t": 40, "temp_c": 62.0, "mhz": 2800, "cpu_pct": 7.3, "watts": 3.22, "rss_mb": 515, "done": 20, "queue": 0}
{"t": 41, "temp_c": 61.0, "mhz": 911, "cpu_pct": 59.5, "watts": 6.74, "rss_mb": 490, "done": 21, "queue": 0}
{"t": 42, "temp_c": 64.0, "mhz": 2800, "cpu_pct": 5.8, "watts": 4.02, "rss_mb": 523, "done": 21, "queue": 0}
{"t": 43, "temp_c": 62.0, "mhz": 1504, "cpu_pct": 60.1, "watts": 7.78, "rss_mb": 490, "done": 22, "queue": 0}
{"t": 44, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 9.6, "watts": 3.88, "rss_mb": 568, "done": 22, "queue": 0}
{"t": 45, "temp_c": 75.0, "mhz": 2851, "cpu_pct": 73.9, "watts": 8.89, "rss_mb": 725, "done": 22, "queue": 0}
{"t": 46, "temp_c": 68.0, "mhz": 2800, "cpu_pct": 8.5, "watts": 4.03, "rss_mb": 542, "done": 23, "queue": 0}
{"t": 47, "temp_c": 61.0, "mhz": 897, "cpu_pct": 58.5, "watts": 8.44, "rss_mb": 492, "done": 24, "queue": 0}
{"t": 48, "temp_c": 70.0, "mhz": 2800, "cpu_pct": 6.8, "watts": 3.07, "rss_mb": 554, "done": 24, "queue": 0}
{"t": 49, "temp_c": 61.0, "mhz": 801, "cpu_pct": 58.5, "watts": 8.55, "rss_mb": 492, "done": 25, "queue": 0}
{"t": 50, "temp_c": 71.0, "mhz": 2823, "cpu_pct": 8.0, "watts": 2.18, "rss_mb": 592, "done": 25, "queue": 0}
{"t": 51, "temp_c": 61.0, "mhz": 859, "cpu_pct": 56.5, "watts": 7.96, "rss_mb": 493, "done": 26, "queue": 0}
{"t": 52, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 7.0, "watts": 3.76, "rss_mb": 568, "done": 26, "queue": 0}
{"t": 53, "temp_c": 60.0, "mhz": 822, "cpu_pct": 57.4, "watts": 8.01, "rss_mb": 587, "done": 26, "queue": 0}
{"t": 54, "temp_c": 72.0, "mhz": 2800, "cpu_pct": 8.3, "watts": 3.49, "rss_mb": 582, "done": 27, "queue": 0}
{"t": 55, "temp_c": 60.0, "mhz": 797, "cpu_pct": 58.4, "watts": 8.50, "rss_mb": 587, "done": 27, "queue": 0}
{"t": 56, "temp_c": 72.0, "mhz": 2846, "cpu_pct": 8.6, "watts": 3.12, "rss_mb": 597, "done": 28, "queue": 0}
{"t": 57, "temp_c": 60.0, "mhz": 2797, "cpu_pct": 57.9, "watts": 8.48, "rss_mb": 495, "done": 29, "queue": 0}
{"t": 58, "temp_c": 66.0, "mhz": 2801, "cpu_pct": 8.3, "watts": 2.35, "rss_mb": 606, "done": 29, "queue": 0}
{"t": 59, "temp_c": 59.0, "mhz": 926, "cpu_pct": 56.4, "watts": 7.81, "rss_mb": 495, "done": 30, "queue": 0}
{"t": 60, "temp_c": 73.0, "mhz": 2825, "cpu_pct": 8.3, "watts": 3.74, "rss_mb": 591, "done": 30, "queue": 0}
{"t": 61, "temp_c": 60.0, "mhz": 791, "cpu_pct": 57.4, "watts": 8.06, "rss_mb": 496, "done": 31, "queue": 0}
{"t": 62, "temp_c": 70.0, "mhz": 2875, "cpu_pct": 7.8, "watts": 3.28, "rss_mb": 596, "done": 31, "queue": 0}
{"t": 63, "temp_c": 61.0, "mhz": 1015, "cpu_pct": 57.2, "watts": 8.50, "rss_mb": 496, "done": 32, "queue": 0}
{"t": 64, "temp_c": 70.0, "mhz": 2850, "cpu_pct": 8.0, "watts": 3.16, "rss_mb": 604, "done": 32, "queue": 0}
{"t": 65, "temp_c": 60.0, "mhz": 763, "cpu_pct": 57.2, "watts": 8.50, "rss_mb": 496, "done": 33, "queue": 0}
{"t": 66, "temp_c": 69.0, "mhz": 2832, "cpu_pct": 8.1, "watts": 3.24, "rss_mb": 598, "done": 33, "queue": 0}
{"t": 67, "temp_c": 60.0, "mhz": 781, "cpu_pct": 57.0, "watts": 6.83, "rss_mb": 496, "done": 34, "queue": 0}
{"t": 68, "temp_c": 70.0, "mhz": 2868, "cpu_pct": 8.3, "watts": 3.68, "rss_mb": 603, "done": 34, "queue": 0}
{"t": 69, "temp_c": 60.0, "mhz": 801, "cpu_pct": 56.8, "watts": 8.06, "rss_mb": 496, "done": 35, "queue": 0}
{"t": 70, "temp_c": 71.0, "mhz": 2846, "cpu_pct": 9.8, "watts": 3.73, "rss_mb": 596, "done": 35, "queue": 0}
{"t": 71, "temp_c": 59.0, "mhz": 870, "cpu_pct": 56.6, "watts": 8.29, "rss_mb": 496, "done": 36, "queue": 0}
{"t": 72, "temp_c": 66.0, "mhz": 2833, "cpu_pct": 7.8, "watts": 3.19, "rss_mb": 608, "done": 36, "queue": 0}
{"t": 73, "temp_c": 60.0, "mhz": 819, "cpu_pct": 57.3, "watts": 8.50, "rss_mb": 497, "done": 37, "queue": 0}
{"t": 74, "temp_c": 69.0, "mhz": 2868, "cpu_pct": 8.6, "watts": 3.09, "rss_mb": 610, "done": 37, "queue": 0}
{"t": 75, "temp_c": 66.0, "mhz": 2819, "cpu_pct": 64.2, "watts": 9.07, "rss_mb": 497, "done": 38, "queue": 0}
{"t": 76, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 15.7, "watts": 3.92, "rss_mb": 609, "done": 38, "queue": 0}
{"t": 77, "temp_c": 59.0, "mhz": 744, "cpu_pct": 56.8, "watts": 6.68, "rss_mb": 497, "done": 39, "queue": 0}
{"t": 78, "temp_c": 66.0, "mhz": 2852, "cpu_pct": 8.8, "watts": 3.74, "rss_mb": 614, "done": 39, "queue": 0}
{"t": 79, "temp_c": 60.0, "mhz": 827, "cpu_pct": 57.5, "watts": 8.15, "rss_mb": 497, "done": 40, "queue": 0}
{"t": 80, "temp_c": 64.0, "mhz": 2850, "cpu_pct": 8.1, "watts": 3.40, "rss_mb": 619, "done": 40, "queue": 0}
{"t": 81, "temp_c": 59.0, "mhz": 813, "cpu_pct": 56.8, "watts": 8.41, "rss_mb": 497, "done": 41, "queue": 0}
{"t": 82, "temp_c": 69.0, "mhz": 2800, "cpu_pct": 9.1, "watts": 3.12, "rss_mb": 634, "done": 41, "queue": 0}
{"t": 83, "temp_c": 59.0, "mhz": 758, "cpu_pct": 55.6, "watts": 8.52, "rss_mb": 497, "done": 42, "queue": 0}
{"t": 84, "temp_c": 63.0, "mhz": 2847, "cpu_pct": 8.5, "watts": 3.40, "rss_mb": 642, "done": 42, "queue": 0}
{"t": 85, "temp_c": 60.0, "mhz": 778, "cpu_pct": 57.0, "watts": 6.78, "rss_mb": 497, "done": 43, "queue": 0}
{"t": 86, "temp_c": 63.0, "mhz": 2841, "cpu_pct": 9.0, "watts": 3.58, "rss_mb": 627, "done": 43, "queue": 0}
{"t": 87, "temp_c": 59.0, "mhz": 859, "cpu_pct": 57.6, "watts": 7.97, "rss_mb": 498, "done": 44, "queue": 0}
{"t": 88, "temp_c": 70.0, "mhz": 2849, "cpu_pct": 10.4, "watts": 3.53, "rss_mb": 620, "done": 44, "queue": 0}
{"t": 89, "temp_c": 60.0, "mhz": 927, "cpu_pct": 56.4, "watts": 8.37, "rss_mb": 499, "done": 45, "queue": 0}
{"t": 90, "temp_c": 62.0, "mhz": 2375, "cpu_pct": 8.8, "watts": 3.21, "rss_mb": 637, "done": 45, "queue": 0}
{"t": 91, "temp_c": 60.0, "mhz": 795, "cpu_pct": 57.9, "watts": 8.64, "rss_mb": 499, "done": 46, "queue": 0}
{"t": 92, "temp_c": 63.0, "mhz": 2375, "cpu_pct": 8.6, "watts": 3.36, "rss_mb": 633, "done": 46, "queue": 0}
{"t": 93, "temp_c": 60.0, "mhz": 800, "cpu_pct": 57.9, "watts": 6.65, "rss_mb": 500, "done": 47, "queue": 0}
{"t": 94, "temp_c": 64.0, "mhz": 2864, "cpu_pct": 9.1, "watts": 3.88, "rss_mb": 616, "done": 47, "queue": 0}
{"t": 95, "temp_c": 59.0, "mhz": 952, "cpu_pct": 56.4, "watts": 7.98, "rss_mb": 500, "done": 48, "queue": 0}
{"t": 96, "temp_c": 65.0, "mhz": 1850, "cpu_pct": 8.6, "watts": 3.57, "rss_mb": 641, "done": 48, "queue": 0}
{"t": 97, "temp_c": 59.0, "mhz": 836, "cpu_pct": 56.9, "watts": 8.34, "rss_mb": 501, "done": 49, "queue": 0}
{"t": 98, "temp_c": 63.0, "mhz": 1850, "cpu_pct": 9.6, "watts": 3.17, "rss_mb": 652, "done": 49, "queue": 0}
{"t": 99, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.4, "watts": 8.57, "rss_mb": 501, "done": 50, "queue": 0}
{"t": 100, "temp_c": 65.0, "mhz": 2824, "cpu_pct": 10.1, "watts": 3.21, "rss_mb": 653, "done": 50, "queue": 0}
{"t": 101, "temp_c": 59.0, "mhz": 800, "cpu_pct": 55.9, "watts": 8.28, "rss_mb": 501, "done": 51, "queue": 0}
{"t": 102, "temp_c": 61.0, "mhz": 2366, "cpu_pct": 9.1, "watts": 2.33, "rss_mb": 649, "done": 51, "queue": 0}
{"t": 103, "temp_c": 59.0, "mhz": 780, "cpu_pct": 56.8, "watts": 7.96, "rss_mb": 501, "done": 52, "queue": 0}
{"t": 104, "temp_c": 66.0, "mhz": 2367, "cpu_pct": 9.3, "watts": 3.59, "rss_mb": 653, "done": 52, "queue": 0}
{"t": 105, "temp_c": 59.0, "mhz": 768, "cpu_pct": 56.4, "watts": 8.32, "rss_mb": 501, "done": 53, "queue": 0}
{"t": 106, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 28.1, "watts": 4.55, "rss_mb": 650, "done": 53, "queue": 0}
{"t": 107, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.3, "watts": 8.80, "rss_mb": 501, "done": 54, "queue": 0}
{"t": 108, "temp_c": 65.0, "mhz": 2301, "cpu_pct": 8.8, "watts": 3.16, "rss_mb": 657, "done": 54, "queue": 0}
{"t": 109, "temp_c": 60.0, "mhz": 780, "cpu_pct": 56.5, "watts": 8.40, "rss_mb": 502, "done": 55, "queue": 0}
{"t": 110, "temp_c": 62.0, "mhz": 2833, "cpu_pct": 8.6, "watts": 3.42, "rss_mb": 661, "done": 55, "queue": 0}
{"t": 111, "temp_c": 59.0, "mhz": 793, "cpu_pct": 56.0, "watts": 6.64, "rss_mb": 502, "done": 56, "queue": 0}
{"t": 112, "temp_c": 64.0, "mhz": 2324, "cpu_pct": 10.4, "watts": 3.61, "rss_mb": 652, "done": 56, "queue": 0}
{"t": 113, "temp_c": 59.0, "mhz": 776, "cpu_pct": 55.6, "watts": 8.26, "rss_mb": 502, "done": 57, "queue": 0}
{"t": 114, "temp_c": 63.0, "mhz": 2343, "cpu_pct": 8.8, "watts": 3.27, "rss_mb": 670, "done": 57, "queue": 0}
{"t": 115, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.6, "watts": 8.56, "rss_mb": 502, "done": 58, "queue": 0}
{"t": 116, "temp_c": 65.0, "mhz": 2859, "cpu_pct": 9.3, "watts": 3.13, "rss_mb": 669, "done": 58, "queue": 0}
{"t": 117, "temp_c": 59.0, "mhz": 875, "cpu_pct": 57.0, "watts": 8.51, "rss_mb": 502, "done": 59, "queue": 0}
{"t": 118, "temp_c": 62.0, "mhz": 2879, "cpu_pct": 10.1, "watts": 3.42, "rss_mb": 663, "done": 59, "queue": 0}
{"t": 119, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.2, "watts": 6.63, "rss_mb": 502, "done": 60, "queue": 0}
{"t": 120, "temp_c": 55.0, "mhz": 800, "cpu_pct": 4.5, "watts": 3.68, "rss_mb": 390, "done": 60, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 120.0, "frames": 60, "frames_per_s": 0.500, "job_ms_p50": 1008, "latency_ms_p50": 1021, "latency_ms_p95": 1094, "latency_ms_max": 1140, "max_queue": 1, "backlog_at_end": 0, "cpu_s_per_frame": 2.343, "peak_rss_mb": 735, "temp_c_mean": 62.9, "temp_c_max": 80.0, "mhz_mean": 1816, "mhz_min": 744, "cpu_pct_mean": 33.4, "watts_mean": 5.75}

View File

@ -0,0 +1,62 @@
{"t": 1, "temp_c": 70.0, "mhz": 2803, "cpu_pct": 62.3, "watts": 8.25, "rss_mb": 536, "done": 0, "queue": 0}
{"t": 2, "temp_c": 72.0, "mhz": 2835, "cpu_pct": 60.7, "watts": 8.51, "rss_mb": 620, "done": 1, "queue": 0}
{"t": 3, "temp_c": 65.0, "mhz": 2376, "cpu_pct": 59.8, "watts": 8.64, "rss_mb": 633, "done": 2, "queue": 0}
{"t": 4, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 59.4, "watts": 8.45, "rss_mb": 575, "done": 3, "queue": 0}
{"t": 5, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 59.8, "watts": 8.59, "rss_mb": 681, "done": 4, "queue": 0}
{"t": 6, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 60.7, "watts": 8.72, "rss_mb": 689, "done": 5, "queue": 0}
{"t": 7, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 60.0, "watts": 8.68, "rss_mb": 681, "done": 6, "queue": 0}
{"t": 8, "temp_c": 69.0, "mhz": 2369, "cpu_pct": 60.2, "watts": 8.58, "rss_mb": 689, "done": 7, "queue": 0}
{"t": 9, "temp_c": 72.0, "mhz": 2874, "cpu_pct": 60.3, "watts": 8.77, "rss_mb": 690, "done": 8, "queue": 0}
{"t": 10, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 61.7, "watts": 8.66, "rss_mb": 692, "done": 9, "queue": 0}
{"t": 11, "temp_c": 73.0, "mhz": 2798, "cpu_pct": 61.4, "watts": 6.51, "rss_mb": 697, "done": 10, "queue": 0}
{"t": 12, "temp_c": 77.0, "mhz": 2848, "cpu_pct": 61.1, "watts": 8.87, "rss_mb": 698, "done": 11, "queue": 0}
{"t": 13, "temp_c": 80.0, "mhz": 2800, "cpu_pct": 61.1, "watts": 8.74, "rss_mb": 700, "done": 12, "queue": 0}
{"t": 14, "temp_c": 69.0, "mhz": 2400, "cpu_pct": 59.6, "watts": 8.67, "rss_mb": 702, "done": 13, "queue": 0}
{"t": 15, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 60.5, "watts": 8.54, "rss_mb": 703, "done": 14, "queue": 0}
{"t": 16, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 61.2, "watts": 8.76, "rss_mb": 704, "done": 15, "queue": 0}
{"t": 17, "temp_c": 66.0, "mhz": 1325, "cpu_pct": 57.4, "watts": 8.54, "rss_mb": 704, "done": 16, "queue": 0}
{"t": 18, "temp_c": 70.0, "mhz": 2807, "cpu_pct": 60.7, "watts": 8.80, "rss_mb": 721, "done": 17, "queue": 0}
{"t": 19, "temp_c": 71.0, "mhz": 2867, "cpu_pct": 60.7, "watts": 8.75, "rss_mb": 705, "done": 18, "queue": 0}
{"t": 20, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 60.2, "watts": 8.59, "rss_mb": 722, "done": 19, "queue": 0}
{"t": 21, "temp_c": 71.0, "mhz": 2867, "cpu_pct": 61.0, "watts": 8.77, "rss_mb": 724, "done": 20, "queue": 0}
{"t": 22, "temp_c": 71.0, "mhz": 2837, "cpu_pct": 60.5, "watts": 8.74, "rss_mb": 725, "done": 21, "queue": 0}
{"t": 23, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 61.7, "watts": 6.56, "rss_mb": 725, "done": 22, "queue": 0}
{"t": 24, "temp_c": 76.0, "mhz": 2800, "cpu_pct": 60.9, "watts": 8.86, "rss_mb": 710, "done": 23, "queue": 0}
{"t": 25, "temp_c": 75.0, "mhz": 2800, "cpu_pct": 61.0, "watts": 8.67, "rss_mb": 710, "done": 24, "queue": 0}
{"t": 26, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 70.9, "watts": 9.28, "rss_mb": 728, "done": 25, "queue": 0}
{"t": 27, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 60.5, "watts": 8.68, "rss_mb": 712, "done": 26, "queue": 0}
{"t": 28, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 60.3, "watts": 8.72, "rss_mb": 714, "done": 27, "queue": 0}
{"t": 29, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 61.5, "watts": 8.90, "rss_mb": 714, "done": 28, "queue": 0}
{"t": 30, "temp_c": 77.0, "mhz": 2800, "cpu_pct": 60.4, "watts": 8.83, "rss_mb": 714, "done": 29, "queue": 0}
{"t": 31, "temp_c": 80.0, "mhz": 2812, "cpu_pct": 59.9, "watts": 8.63, "rss_mb": 715, "done": 30, "queue": 0}
{"t": 32, "temp_c": 78.0, "mhz": 2800, "cpu_pct": 61.8, "watts": 8.76, "rss_mb": 715, "done": 31, "queue": 0}
{"t": 33, "temp_c": 73.0, "mhz": 2825, "cpu_pct": 58.3, "watts": 8.44, "rss_mb": 715, "done": 32, "queue": 0}
{"t": 34, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 60.4, "watts": 8.55, "rss_mb": 716, "done": 33, "queue": 0}
{"t": 35, "temp_c": 71.0, "mhz": 2894, "cpu_pct": 61.5, "watts": 6.49, "rss_mb": 732, "done": 34, "queue": 0}
{"t": 36, "temp_c": 81.0, "mhz": 2874, "cpu_pct": 60.2, "watts": 8.64, "rss_mb": 716, "done": 35, "queue": 0}
{"t": 37, "temp_c": 82.0, "mhz": 2868, "cpu_pct": 60.9, "watts": 8.85, "rss_mb": 717, "done": 36, "queue": 0}
{"t": 38, "temp_c": 81.0, "mhz": 2800, "cpu_pct": 61.0, "watts": 8.88, "rss_mb": 717, "done": 37, "queue": 0}
{"t": 39, "temp_c": 60.0, "mhz": 1700, "cpu_pct": 61.2, "watts": 7.48, "rss_mb": 717, "done": 38, "queue": 0}
{"t": 40, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 58.8, "watts": 6.06, "rss_mb": 685, "done": 39, "queue": 0}
{"t": 41, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 66.5, "watts": 5.80, "rss_mb": 717, "done": 39, "queue": 0}
{"t": 42, "temp_c": 59.0, "mhz": 1500, "cpu_pct": 60.5, "watts": 5.78, "rss_mb": 718, "done": 40, "queue": 0}
{"t": 43, "temp_c": 58.0, "mhz": 1800, "cpu_pct": 60.2, "watts": 5.61, "rss_mb": 686, "done": 41, "queue": 0}
{"t": 44, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 67.2, "watts": 5.78, "rss_mb": 718, "done": 41, "queue": 0}
{"t": 45, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 60.8, "watts": 4.23, "rss_mb": 718, "done": 42, "queue": 0}
{"t": 46, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 59.6, "watts": 5.78, "rss_mb": 686, "done": 43, "queue": 0}
{"t": 47, "temp_c": 59.0, "mhz": 1900, "cpu_pct": 67.1, "watts": 6.01, "rss_mb": 718, "done": 43, "queue": 0}
{"t": 48, "temp_c": 58.0, "mhz": 1800, "cpu_pct": 59.9, "watts": 6.00, "rss_mb": 734, "done": 44, "queue": 0}
{"t": 49, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 60.3, "watts": 5.83, "rss_mb": 688, "done": 45, "queue": 0}
{"t": 50, "temp_c": 59.0, "mhz": 1900, "cpu_pct": 65.8, "watts": 5.98, "rss_mb": 735, "done": 45, "queue": 0}
{"t": 51, "temp_c": 58.0, "mhz": 1624, "cpu_pct": 57.7, "watts": 5.98, "rss_mb": 736, "done": 46, "queue": 0}
{"t": 52, "temp_c": 59.0, "mhz": 1785, "cpu_pct": 62.8, "watts": 6.06, "rss_mb": 720, "done": 47, "queue": 0}
{"t": 53, "temp_c": 58.0, "mhz": 1900, "cpu_pct": 64.3, "watts": 4.53, "rss_mb": 720, "done": 47, "queue": 0}
{"t": 54, "temp_c": 59.0, "mhz": 1900, "cpu_pct": 60.4, "watts": 6.05, "rss_mb": 736, "done": 48, "queue": 0}
{"t": 55, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 61.7, "watts": 6.02, "rss_mb": 736, "done": 49, "queue": 0}
{"t": 56, "temp_c": 60.0, "mhz": 1800, "cpu_pct": 69.3, "watts": 6.12, "rss_mb": 736, "done": 49, "queue": 0}
{"t": 57, "temp_c": 58.0, "mhz": 1025, "cpu_pct": 76.2, "watts": 6.44, "rss_mb": 737, "done": 50, "queue": 0}
{"t": 58, "temp_c": 58.0, "mhz": 1900, "cpu_pct": 59.0, "watts": 5.69, "rss_mb": 689, "done": 51, "queue": 0}
{"t": 59, "temp_c": 59.0, "mhz": 1800, "cpu_pct": 68.7, "watts": 6.05, "rss_mb": 737, "done": 51, "queue": 0}
{"t": 60, "temp_c": 58.0, "mhz": 1900, "cpu_pct": 58.4, "watts": 6.07, "rss_mb": 737, "done": 52, "queue": 0}
{"t": 61, "temp_c": 55.0, "mhz": 797, "cpu_pct": 48.0, "watts": 4.43, "rss_mb": 391, "done": 53, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 0, "secs": 60.8, "frames": 53, "frames_per_s": 0.871, "job_ms_p50": 1006, "latency_ms_p50": 1019, "latency_ms_p95": 1519, "latency_ms_max": 1660, "max_queue": 0, "backlog_at_end": 0, "cpu_s_per_frame": 2.651, "peak_rss_mb": 737, "temp_c_mean": 68.6, "temp_c_max": 82.0, "mhz_mean": 2359, "mhz_min": 797, "cpu_pct_mean": 61.4, "watts_mean": 7.50}

View File

@ -0,0 +1,121 @@
{"t": 1, "temp_c": 60.0, "mhz": 2099, "cpu_pct": 61.9, "watts": 6.70, "rss_mb": 533, "done": 0, "queue": 0}
{"t": 2, "temp_c": 54.0, "mhz": 771, "cpu_pct": 23.0, "watts": 4.74, "rss_mb": 247, "done": 1, "queue": 0}
{"t": 3, "temp_c": 61.0, "mhz": 2300, "cpu_pct": 62.3, "watts": 7.22, "rss_mb": 613, "done": 1, "queue": 0}
{"t": 4, "temp_c": 54.0, "mhz": 800, "cpu_pct": 11.6, "watts": 3.98, "rss_mb": 282, "done": 2, "queue": 0}
{"t": 5, "temp_c": 67.0, "mhz": 2500, "cpu_pct": 62.6, "watts": 8.10, "rss_mb": 628, "done": 2, "queue": 0}
{"t": 6, "temp_c": 55.0, "mhz": 2847, "cpu_pct": 6.8, "watts": 3.31, "rss_mb": 297, "done": 3, "queue": 0}
{"t": 7, "temp_c": 71.0, "mhz": 2700, "cpu_pct": 60.6, "watts": 6.62, "rss_mb": 570, "done": 3, "queue": 0}
{"t": 8, "temp_c": 54.0, "mhz": 775, "cpu_pct": 5.3, "watts": 4.47, "rss_mb": 385, "done": 4, "queue": 0}
{"t": 9, "temp_c": 64.0, "mhz": 2017, "cpu_pct": 61.8, "watts": 7.52, "rss_mb": 534, "done": 4, "queue": 0}
{"t": 10, "temp_c": 54.0, "mhz": 795, "cpu_pct": 4.8, "watts": 4.12, "rss_mb": 442, "done": 5, "queue": 0}
{"t": 11, "temp_c": 63.0, "mhz": 1780, "cpu_pct": 61.2, "watts": 7.88, "rss_mb": 544, "done": 5, "queue": 0}
{"t": 12, "temp_c": 55.0, "mhz": 2800, "cpu_pct": 5.8, "watts": 3.56, "rss_mb": 452, "done": 6, "queue": 0}
{"t": 13, "temp_c": 61.0, "mhz": 1853, "cpu_pct": 59.2, "watts": 8.49, "rss_mb": 462, "done": 7, "queue": 0}
{"t": 14, "temp_c": 55.0, "mhz": 911, "cpu_pct": 4.5, "watts": 3.11, "rss_mb": 462, "done": 7, "queue": 0}
{"t": 15, "temp_c": 61.0, "mhz": 1000, "cpu_pct": 61.2, "watts": 8.52, "rss_mb": 465, "done": 8, "queue": 0}
{"t": 16, "temp_c": 55.0, "mhz": 799, "cpu_pct": 4.8, "watts": 2.21, "rss_mb": 465, "done": 8, "queue": 0}
{"t": 17, "temp_c": 61.0, "mhz": 1664, "cpu_pct": 60.5, "watts": 7.80, "rss_mb": 469, "done": 9, "queue": 0}
{"t": 18, "temp_c": 54.0, "mhz": 2847, "cpu_pct": 6.3, "watts": 3.86, "rss_mb": 469, "done": 9, "queue": 0}
{"t": 19, "temp_c": 61.0, "mhz": 1379, "cpu_pct": 59.9, "watts": 7.94, "rss_mb": 473, "done": 10, "queue": 0}
{"t": 20, "temp_c": 54.0, "mhz": 892, "cpu_pct": 4.5, "watts": 3.44, "rss_mb": 473, "done": 10, "queue": 0}
{"t": 21, "temp_c": 61.0, "mhz": 1106, "cpu_pct": 61.1, "watts": 8.26, "rss_mb": 518, "done": 10, "queue": 0}
{"t": 22, "temp_c": 55.0, "mhz": 940, "cpu_pct": 5.5, "watts": 3.27, "rss_mb": 474, "done": 11, "queue": 0}
{"t": 23, "temp_c": 61.0, "mhz": 1003, "cpu_pct": 60.2, "watts": 8.42, "rss_mb": 476, "done": 12, "queue": 0}
{"t": 24, "temp_c": 59.0, "mhz": 2800, "cpu_pct": 5.2, "watts": 2.19, "rss_mb": 488, "done": 12, "queue": 0}
{"t": 25, "temp_c": 74.0, "mhz": 2800, "cpu_pct": 64.7, "watts": 7.51, "rss_mb": 479, "done": 13, "queue": 0}
{"t": 26, "temp_c": 55.0, "mhz": 1949, "cpu_pct": 16.4, "watts": 5.30, "rss_mb": 479, "done": 13, "queue": 0}
{"t": 27, "temp_c": 61.0, "mhz": 1027, "cpu_pct": 60.4, "watts": 7.74, "rss_mb": 481, "done": 14, "queue": 0}
{"t": 28, "temp_c": 54.0, "mhz": 1430, "cpu_pct": 4.8, "watts": 3.59, "rss_mb": 481, "done": 14, "queue": 0}
{"t": 29, "temp_c": 61.0, "mhz": 1523, "cpu_pct": 59.8, "watts": 8.16, "rss_mb": 483, "done": 15, "queue": 0}
{"t": 30, "temp_c": 55.0, "mhz": 1811, "cpu_pct": 4.5, "watts": 3.40, "rss_mb": 483, "done": 15, "queue": 0}
{"t": 31, "temp_c": 60.0, "mhz": 1240, "cpu_pct": 59.9, "watts": 8.45, "rss_mb": 484, "done": 16, "queue": 0}
{"t": 32, "temp_c": 55.0, "mhz": 2798, "cpu_pct": 5.0, "watts": 3.06, "rss_mb": 484, "done": 16, "queue": 0}
{"t": 33, "temp_c": 60.0, "mhz": 1326, "cpu_pct": 60.3, "watts": 6.75, "rss_mb": 486, "done": 17, "queue": 0}
{"t": 34, "temp_c": 57.0, "mhz": 2800, "cpu_pct": 5.3, "watts": 3.94, "rss_mb": 499, "done": 17, "queue": 0}
{"t": 35, "temp_c": 61.0, "mhz": 951, "cpu_pct": 60.0, "watts": 7.83, "rss_mb": 489, "done": 18, "queue": 0}
{"t": 36, "temp_c": 67.0, "mhz": 2800, "cpu_pct": 8.3, "watts": 3.86, "rss_mb": 542, "done": 18, "queue": 0}
{"t": 37, "temp_c": 59.0, "mhz": 977, "cpu_pct": 58.4, "watts": 8.21, "rss_mb": 490, "done": 19, "queue": 0}
{"t": 38, "temp_c": 64.0, "mhz": 2800, "cpu_pct": 6.3, "watts": 3.20, "rss_mb": 525, "done": 19, "queue": 0}
{"t": 39, "temp_c": 60.0, "mhz": 1375, "cpu_pct": 58.6, "watts": 8.42, "rss_mb": 491, "done": 20, "queue": 0}
{"t": 40, "temp_c": 66.0, "mhz": 2800, "cpu_pct": 6.8, "watts": 3.10, "rss_mb": 535, "done": 20, "queue": 0}
{"t": 41, "temp_c": 60.0, "mhz": 800, "cpu_pct": 58.8, "watts": 8.57, "rss_mb": 491, "done": 21, "queue": 0}
{"t": 42, "temp_c": 71.0, "mhz": 2800, "cpu_pct": 7.3, "watts": 2.24, "rss_mb": 568, "done": 21, "queue": 0}
{"t": 43, "temp_c": 59.0, "mhz": 752, "cpu_pct": 56.8, "watts": 7.77, "rss_mb": 492, "done": 22, "queue": 0}
{"t": 44, "temp_c": 68.0, "mhz": 2800, "cpu_pct": 6.8, "watts": 3.67, "rss_mb": 553, "done": 22, "queue": 0}
{"t": 45, "temp_c": 60.0, "mhz": 913, "cpu_pct": 58.3, "watts": 8.04, "rss_mb": 493, "done": 23, "queue": 0}
{"t": 46, "temp_c": 70.0, "mhz": 2800, "cpu_pct": 7.3, "watts": 3.44, "rss_mb": 569, "done": 23, "queue": 0}
{"t": 47, "temp_c": 59.0, "mhz": 807, "cpu_pct": 57.7, "watts": 8.44, "rss_mb": 493, "done": 24, "queue": 0}
{"t": 48, "temp_c": 73.0, "mhz": 2800, "cpu_pct": 8.6, "watts": 2.98, "rss_mb": 573, "done": 24, "queue": 0}
{"t": 49, "temp_c": 60.0, "mhz": 868, "cpu_pct": 56.8, "watts": 8.58, "rss_mb": 494, "done": 25, "queue": 0}
{"t": 50, "temp_c": 72.0, "mhz": 2800, "cpu_pct": 7.8, "watts": 2.32, "rss_mb": 579, "done": 25, "queue": 0}
{"t": 51, "temp_c": 59.0, "mhz": 791, "cpu_pct": 57.8, "watts": 7.74, "rss_mb": 494, "done": 26, "queue": 0}
{"t": 52, "temp_c": 73.0, "mhz": 2800, "cpu_pct": 7.8, "watts": 3.75, "rss_mb": 584, "done": 26, "queue": 0}
{"t": 53, "temp_c": 60.0, "mhz": 1035, "cpu_pct": 58.4, "watts": 8.00, "rss_mb": 494, "done": 27, "queue": 0}
{"t": 54, "temp_c": 68.0, "mhz": 2850, "cpu_pct": 10.1, "watts": 3.53, "rss_mb": 599, "done": 27, "queue": 0}
{"t": 55, "temp_c": 60.0, "mhz": 794, "cpu_pct": 56.3, "watts": 8.46, "rss_mb": 495, "done": 28, "queue": 0}
{"t": 56, "temp_c": 57.0, "mhz": 1400, "cpu_pct": 18.4, "watts": 4.00, "rss_mb": 503, "done": 28, "queue": 0}
{"t": 57, "temp_c": 64.0, "mhz": 2526, "cpu_pct": 65.7, "watts": 8.73, "rss_mb": 589, "done": 28, "queue": 0}
{"t": 58, "temp_c": 70.0, "mhz": 2835, "cpu_pct": 8.8, "watts": 3.55, "rss_mb": 597, "done": 29, "queue": 0}
{"t": 59, "temp_c": 59.0, "mhz": 822, "cpu_pct": 56.9, "watts": 6.63, "rss_mb": 496, "done": 30, "queue": 0}
{"t": 60, "temp_c": 73.0, "mhz": 2843, "cpu_pct": 8.9, "watts": 3.74, "rss_mb": 593, "done": 30, "queue": 0}
{"t": 61, "temp_c": 59.0, "mhz": 868, "cpu_pct": 55.6, "watts": 8.02, "rss_mb": 496, "done": 31, "queue": 0}
{"t": 62, "temp_c": 68.0, "mhz": 2834, "cpu_pct": 8.6, "watts": 3.48, "rss_mb": 606, "done": 31, "queue": 0}
{"t": 63, "temp_c": 59.0, "mhz": 765, "cpu_pct": 56.4, "watts": 8.40, "rss_mb": 497, "done": 32, "queue": 0}
{"t": 64, "temp_c": 64.0, "mhz": 2849, "cpu_pct": 8.8, "watts": 3.13, "rss_mb": 615, "done": 32, "queue": 0}
{"t": 65, "temp_c": 59.0, "mhz": 744, "cpu_pct": 56.8, "watts": 8.58, "rss_mb": 497, "done": 33, "queue": 0}
{"t": 66, "temp_c": 70.0, "mhz": 2817, "cpu_pct": 9.5, "watts": 3.22, "rss_mb": 603, "done": 33, "queue": 0}
{"t": 67, "temp_c": 59.0, "mhz": 798, "cpu_pct": 56.8, "watts": 6.71, "rss_mb": 497, "done": 34, "queue": 0}
{"t": 68, "temp_c": 64.0, "mhz": 2838, "cpu_pct": 9.1, "watts": 3.80, "rss_mb": 616, "done": 34, "queue": 0}
{"t": 69, "temp_c": 59.0, "mhz": 777, "cpu_pct": 56.8, "watts": 8.05, "rss_mb": 497, "done": 35, "queue": 0}
{"t": 70, "temp_c": 63.0, "mhz": 2851, "cpu_pct": 8.8, "watts": 3.52, "rss_mb": 619, "done": 35, "queue": 0}
{"t": 71, "temp_c": 59.0, "mhz": 801, "cpu_pct": 56.8, "watts": 8.37, "rss_mb": 498, "done": 36, "queue": 0}
{"t": 72, "temp_c": 67.0, "mhz": 2848, "cpu_pct": 10.3, "watts": 3.19, "rss_mb": 629, "done": 36, "queue": 0}
{"t": 73, "temp_c": 59.0, "mhz": 775, "cpu_pct": 55.3, "watts": 8.54, "rss_mb": 498, "done": 37, "queue": 0}
{"t": 74, "temp_c": 64.0, "mhz": 2849, "cpu_pct": 8.4, "watts": 3.39, "rss_mb": 626, "done": 37, "queue": 0}
{"t": 75, "temp_c": 59.0, "mhz": 815, "cpu_pct": 56.0, "watts": 6.70, "rss_mb": 498, "done": 38, "queue": 0}
{"t": 76, "temp_c": 61.0, "mhz": 2900, "cpu_pct": 8.6, "watts": 3.59, "rss_mb": 631, "done": 38, "queue": 0}
{"t": 77, "temp_c": 59.0, "mhz": 868, "cpu_pct": 56.2, "watts": 8.16, "rss_mb": 499, "done": 39, "queue": 0}
{"t": 78, "temp_c": 65.0, "mhz": 2844, "cpu_pct": 10.1, "watts": 3.41, "rss_mb": 619, "done": 39, "queue": 0}
{"t": 79, "temp_c": 59.0, "mhz": 779, "cpu_pct": 56.0, "watts": 8.34, "rss_mb": 499, "done": 40, "queue": 0}
{"t": 80, "temp_c": 61.0, "mhz": 1843, "cpu_pct": 8.6, "watts": 3.26, "rss_mb": 657, "done": 40, "queue": 0}
{"t": 81, "temp_c": 58.0, "mhz": 762, "cpu_pct": 56.0, "watts": 8.56, "rss_mb": 499, "done": 41, "queue": 0}
{"t": 82, "temp_c": 62.0, "mhz": 2859, "cpu_pct": 9.0, "watts": 3.43, "rss_mb": 659, "done": 41, "queue": 0}
{"t": 83, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.5, "watts": 8.15, "rss_mb": 499, "done": 42, "queue": 0}
{"t": 84, "temp_c": 63.0, "mhz": 2370, "cpu_pct": 9.1, "watts": 2.25, "rss_mb": 648, "done": 42, "queue": 0}
{"t": 85, "temp_c": 59.0, "mhz": 800, "cpu_pct": 55.5, "watts": 8.12, "rss_mb": 499, "done": 43, "queue": 0}
{"t": 86, "temp_c": 63.0, "mhz": 2358, "cpu_pct": 8.6, "watts": 3.38, "rss_mb": 648, "done": 43, "queue": 0}
{"t": 87, "temp_c": 74.0, "mhz": 2737, "cpu_pct": 75.9, "watts": 8.89, "rss_mb": 648, "done": 43, "queue": 0}
{"t": 88, "temp_c": 63.0, "mhz": 1850, "cpu_pct": 10.4, "watts": 3.95, "rss_mb": 650, "done": 44, "queue": 0}
{"t": 89, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.0, "watts": 8.58, "rss_mb": 501, "done": 45, "queue": 0}
{"t": 90, "temp_c": 68.0, "mhz": 2823, "cpu_pct": 10.1, "watts": 3.37, "rss_mb": 663, "done": 45, "queue": 0}
{"t": 91, "temp_c": 58.0, "mhz": 800, "cpu_pct": 55.9, "watts": 8.16, "rss_mb": 501, "done": 46, "queue": 0}
{"t": 92, "temp_c": 61.0, "mhz": 1849, "cpu_pct": 8.8, "watts": 2.33, "rss_mb": 653, "done": 46, "queue": 0}
{"t": 93, "temp_c": 59.0, "mhz": 779, "cpu_pct": 57.1, "watts": 8.05, "rss_mb": 501, "done": 47, "queue": 0}
{"t": 94, "temp_c": 62.0, "mhz": 2889, "cpu_pct": 8.9, "watts": 3.48, "rss_mb": 657, "done": 47, "queue": 0}
{"t": 95, "temp_c": 59.0, "mhz": 789, "cpu_pct": 56.5, "watts": 8.39, "rss_mb": 502, "done": 48, "queue": 0}
{"t": 96, "temp_c": 65.0, "mhz": 2830, "cpu_pct": 11.9, "watts": 3.27, "rss_mb": 652, "done": 48, "queue": 0}
{"t": 97, "temp_c": 59.0, "mhz": 768, "cpu_pct": 55.6, "watts": 8.59, "rss_mb": 502, "done": 49, "queue": 0}
{"t": 98, "temp_c": 65.0, "mhz": 2834, "cpu_pct": 9.1, "watts": 3.43, "rss_mb": 662, "done": 49, "queue": 0}
{"t": 99, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.5, "watts": 8.15, "rss_mb": 502, "done": 50, "queue": 0}
{"t": 100, "temp_c": 61.0, "mhz": 1849, "cpu_pct": 8.8, "watts": 2.16, "rss_mb": 666, "done": 50, "queue": 0}
{"t": 101, "temp_c": 59.0, "mhz": 763, "cpu_pct": 56.3, "watts": 8.20, "rss_mb": 502, "done": 51, "queue": 0}
{"t": 102, "temp_c": 62.0, "mhz": 2859, "cpu_pct": 10.3, "watts": 3.42, "rss_mb": 655, "done": 51, "queue": 0}
{"t": 103, "temp_c": 59.0, "mhz": 796, "cpu_pct": 55.3, "watts": 8.46, "rss_mb": 502, "done": 52, "queue": 0}
{"t": 104, "temp_c": 61.0, "mhz": 2349, "cpu_pct": 9.3, "watts": 3.08, "rss_mb": 688, "done": 52, "queue": 0}
{"t": 105, "temp_c": 58.0, "mhz": 796, "cpu_pct": 55.5, "watts": 8.55, "rss_mb": 503, "done": 53, "queue": 0}
{"t": 106, "temp_c": 64.0, "mhz": 2360, "cpu_pct": 9.3, "watts": 3.44, "rss_mb": 677, "done": 53, "queue": 0}
{"t": 107, "temp_c": 59.0, "mhz": 800, "cpu_pct": 56.7, "watts": 8.07, "rss_mb": 503, "done": 54, "queue": 0}
{"t": 108, "temp_c": 62.0, "mhz": 2875, "cpu_pct": 10.9, "watts": 2.33, "rss_mb": 661, "done": 54, "queue": 0}
{"t": 109, "temp_c": 59.0, "mhz": 800, "cpu_pct": 55.5, "watts": 8.23, "rss_mb": 503, "done": 55, "queue": 0}
{"t": 110, "temp_c": 61.0, "mhz": 2884, "cpu_pct": 9.0, "watts": 3.26, "rss_mb": 682, "done": 55, "queue": 0}
{"t": 111, "temp_c": 59.0, "mhz": 749, "cpu_pct": 56.0, "watts": 8.41, "rss_mb": 503, "done": 56, "queue": 0}
{"t": 112, "temp_c": 62.0, "mhz": 2784, "cpu_pct": 9.8, "watts": 3.13, "rss_mb": 689, "done": 56, "queue": 0}
{"t": 113, "temp_c": 58.0, "mhz": 798, "cpu_pct": 54.4, "watts": 8.50, "rss_mb": 504, "done": 57, "queue": 0}
{"t": 114, "temp_c": 61.0, "mhz": 2871, "cpu_pct": 10.3, "watts": 3.38, "rss_mb": 689, "done": 57, "queue": 0}
{"t": 115, "temp_c": 59.0, "mhz": 2825, "cpu_pct": 55.1, "watts": 7.96, "rss_mb": 504, "done": 58, "queue": 0}
{"t": 116, "temp_c": 60.0, "mhz": 2878, "cpu_pct": 8.3, "watts": 2.31, "rss_mb": 689, "done": 58, "queue": 0}
{"t": 117, "temp_c": 59.0, "mhz": 800, "cpu_pct": 57.4, "watts": 8.26, "rss_mb": 506, "done": 59, "queue": 0}
{"t": 118, "temp_c": 63.0, "mhz": 2726, "cpu_pct": 27.6, "watts": 4.88, "rss_mb": 692, "done": 59, "queue": 0}
{"t": 119, "temp_c": 59.0, "mhz": 800, "cpu_pct": 55.3, "watts": 8.37, "rss_mb": 506, "done": 60, "queue": 0}
{"t": 120, "temp_c": 55.0, "mhz": 903, "cpu_pct": 5.6, "watts": 3.07, "rss_mb": 394, "done": 60, "queue": 0}
{"summary": "pipeline", "kind": "c12_ea", "workers": 1, "threads": 4, "effort": 3, "distance": 1.00, "period_ms": 2000, "secs": 120.0, "frames": 60, "frames_per_s": 0.500, "job_ms_p50": 1000, "latency_ms_p50": 1013, "latency_ms_p95": 1109, "latency_ms_max": 1395, "max_queue": 1, "backlog_at_end": 0, "cpu_s_per_frame": 2.353, "peak_rss_mb": 739, "temp_c_mean": 61.2, "temp_c_max": 74.0, "mhz_mean": 1765, "mhz_min": 744, "cpu_pct_mean": 33.6, "watts_mean": 5.72}

View File

@ -0,0 +1,34 @@
#!/bin/sh
# CPU the program spends while the camera streams and nothing is saved: the
# tower's installed build against the branch (buffer reuse). Manual mode, one
# snapshot to start the stream, then 60 s measured. ~5 min.
set -u
export GENICAM_GENTL64_PATH=/opt/VimbaX/cti # as scripts/fgc does
T=~/thermal-test
CFG=$T/manual-config.ini
# The tower's config, but starting in manual (still homing: a snapshot needs it).
sed 's/^mode = auto/mode = manual/' ~/.config/fire_gimbal_control/config.ini > "$CFG"
ln -sf ~/.config/fire_gimbal_control/scan.csv "$T/scan.csv"
ln -sfn ~/.config/fire_gimbal_control/camera_profiles "$T/camera_profiles"
HZ=$(getconf CLK_TCK)
measure() { # label binary
FIFO=$T/cmd2.fifo
rm -f "$FIFO"; mkfifo "$FIFO"
sleep 100000 > "$FIFO" &
holder=$!
"$2" -c "$CFG" --no-tui --no-mqtt --no-log-file < "$FIFO" > "$T/stream_$1.log" 2>&1 &
pid=$!
sleep 60 # start + homing
echo snapshot > "$FIFO"
sleep 15 # the snapshot saved; the stream keeps running
a=$(awk '{print $14 + $15}' /proc/$pid/stat)
sleep 60
b=$(awk '{print $14 + $15}' /proc/$pid/stat)
echo "$1: $(echo "scale=1; 100 * ($b - $a) / $HZ / 60" | bc) % of one core while streaming"
kill -TERM "$pid"; sleep 10; kill "$holder" 2>/dev/null; sleep 3
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid"
rm -f "$FIFO"
}
measure installed ~/projects/fwt_2a/software/build/fire_gimbal_control
measure branch $T/software/build/fire_gimbal_control

38
tools/thermal-sim/cap-cpu.sh Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Cap the CPU clock, to see how the program copes with a CPU slowed to stay cool.
#
# sudo tools/thermal-sim/cap-cpu.sh 800 # every core at most 800 MHz
# sudo tools/thermal-sim/cap-cpu.sh restore # back to what it was
#
# Writes scaling_max_freq on every core. The values found the first time are
# kept in /run/fgc-cap-cpu.saved, so `restore` puts back the real ones however
# many caps were set in between. A reboot also clears the cap.
set -euo pipefail
saved=/run/fgc-cap-cpu.saved
cpus=(/sys/devices/system/cpu/cpu[0-9]*/cpufreq)
usage() { echo "usage: $0 <MHz> | restore" >&2; exit 2; }
[[ $# -eq 1 ]] || usage
[[ $EUID -eq 0 ]] || { echo "$0: needs root (sudo)" >&2; exit 1; }
[[ -e ${cpus[0]}/scaling_max_freq ]] || { echo "$0: no cpufreq on this machine" >&2; exit 1; }
if [[ $1 == restore ]]; then
[[ -f $saved ]] || { echo "nothing to restore (no cap set since boot)"; exit 0; }
while read -r dir khz; do echo "$khz" > "$dir/scaling_max_freq"; done < "$saved"
rm -f "$saved"
echo "CPU clock limits restored"
exit 0
fi
[[ $1 =~ ^[0-9]+$ ]] || usage
khz=$(( $1 * 1000 ))
if [[ ! -f $saved ]]; then
for d in "${cpus[@]}"; do echo "$d $(cat "$d/scaling_max_freq")"; done > "$saved"
fi
for d in "${cpus[@]}"; do
min=$(cat "$d/cpuinfo_min_freq")
(( khz < min )) && { echo "$0: $1 MHz is under this CPU's minimum ($((min / 1000)) MHz)" >&2; exit 1; }
echo "$khz" > "$d/scaling_max_freq"
done
echo "every core capped at $1 MHz ('$0 restore' to undo)"

31
tools/thermal-sim/cap-power.sh Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Cap the CPU package power, as a hot box or the 6 W TDP mode would.
#
# sudo tools/thermal-sim/cap-power.sh 6 # long-term limit 6 W
# sudo tools/thermal-sim/cap-power.sh restore
#
# Writes the RAPL long-term limit (constraint 0) of intel-rapl:0. The original is
# kept in /run/fgc-cap-power.saved. A reboot also clears it. Intel only.
set -euo pipefail
saved=/run/fgc-cap-power.saved
rapl=/sys/class/powercap/intel-rapl:0
limit=$rapl/constraint_0_power_limit_uw
usage() { echo "usage: $0 <watts> | restore" >&2; exit 2; }
[[ $# -eq 1 ]] || usage
[[ $EUID -eq 0 ]] || { echo "$0: needs root (sudo)" >&2; exit 1; }
[[ -w $limit ]] || { echo "$0: no writable RAPL limit at $limit" >&2; exit 1; }
if [[ $1 == restore ]]; then
[[ -f $saved ]] || { echo "nothing to restore (no cap set since boot)"; exit 0; }
cat "$saved" > "$limit"
rm -f "$saved"
echo "power limit restored to $(( $(cat "$limit") / 1000000 )) W"
exit 0
fi
[[ $1 =~ ^[0-9]+$ ]] || usage
[[ -f $saved ]] || cat "$limit" > "$saved"
echo $(( $1 * 1000000 )) > "$limit"
echo "package power capped at $1 W (was $(( $(cat "$saved") / 1000000 )) W; '$0 restore' to undo)"

77
tools/thermal-sim/fake-temp.sh Executable file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# A fake /sys for the CPU guard, so heat can be simulated without heat. Point the
# program at it with [Thermal] sysfs_root = <root> (config.ini or hardware.ini)
# and change the temperature while it runs.
#
# tools/thermal-sim/fake-temp.sh <root> init # build the tree (4 cores, 3.6 GHz, 70 C)
# tools/thermal-sim/fake-temp.sh <root> <C> # set the CPU temperature
# tools/thermal-sim/fake-temp.sh <root> <C> throttle # ...and count one throttle event
# tools/thermal-sim/fake-temp.sh <root> clock <MHz> # set every core's clock
# tools/thermal-sim/fake-temp.sh <root> ramp <C> <C> <step> <secs-per-step>
# # walk the temperature, throttling
# # at 90 C and above
#
# The tree has no proc/stat, so the program reads the real /proc/stat: how busy
# the CPU is stays real (raise it with heat-load.sh).
set -euo pipefail
usage() { sed -n '2,15p' "$0" >&2; exit 2; }
[[ $# -ge 2 ]] || usage
root=$1; shift
cpu=$root/sys/devices/system/cpu
zone=$root/sys/class/thermal/thermal_zone0
init() {
rm -f "$root/proc/stat" # an older tree's frozen copy would hide the real one
mkdir -p "$zone"
echo x86_pkg_temp > "$zone/type"
echo 70000 > "$zone/temp"
for c in 0 1 2 3; do
mkdir -p "$cpu/cpu$c/cpufreq" "$cpu/cpu$c/thermal_throttle"
echo 2800000 > "$cpu/cpu$c/cpufreq/scaling_cur_freq"
echo 3600000 > "$cpu/cpu$c/cpufreq/cpuinfo_max_freq"
echo 3600000 > "$cpu/cpu$c/cpufreq/scaling_max_freq"
echo 0 > "$cpu/cpu$c/thermal_throttle/package_throttle_count"
echo 0 > "$cpu/cpu$c/thermal_throttle/core_throttle_count"
done
echo "fake tree ready at $root: 70 C, 2.8 of 3.6 GHz"
}
set_temp() { # <C> [throttle]
echo $(( $1 * 1000 )) > "$zone/temp"
if [[ ${2:-} == throttle ]]; then
f=$cpu/cpu0/thermal_throttle/package_throttle_count
echo $(( $(cat "$f") + 1 )) > "$f"
fi
}
[[ $1 == init ]] && { init; exit 0; }
[[ -d $zone ]] || { echo "$0: no tree at $root - run '$0 $root init' first" >&2; exit 1; }
case $1 in
clock)
[[ $# -eq 2 ]] || usage
for c in "$cpu"/cpu[0-9]*; do echo $(( $2 * 1000 )) > "$c/cpufreq/scaling_cur_freq"; done
echo "clock $2 MHz" ;;
ramp)
[[ $# -eq 5 ]] || usage
from=$2 to=$3 step=$4 secs=$5
(( from > to )) && step=$(( -step ))
t=$from
while :; do
echo "$(date +%T) $t C"
# Hold the step; at 90 C and up the CPU throttles, one counted
# event every 2 s.
for (( i = 0; i < secs; i += 2 )); do
if (( t >= 90 )); then set_temp "$t" throttle; else set_temp "$t"; fi
sleep 2
done
(( t == to )) && break
t=$(( t + step ))
(( (step > 0 && t > to) || (step < 0 && t < to) )) && t=$to
done ;;
*)
[[ $1 =~ ^[0-9]+$ ]] || usage
set_temp "$1" "${2:-}"
echo "CPU $1 C${2:+, one throttle event}" ;;
esac

26
tools/thermal-sim/heat-load.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Keep some cores busy, standing in for heat and work competing with the program
# (a hot afternoon, the modem, something else running).
#
# tools/thermal-sim/heat-load.sh 2 600 # 2 cores flat out for 10 minutes
#
# Uses stress-ng when installed; otherwise one busy shell loop per core. Ctrl-C
# stops it early. Needs no root.
set -euo pipefail
[[ $# -eq 2 && $1 =~ ^[0-9]+$ && $2 =~ ^[0-9]+$ ]] || {
echo "usage: $0 <cores> <seconds>" >&2; exit 2; }
cores=$1 secs=$2
if command -v stress-ng >/dev/null; then
exec stress-ng --cpu "$cores" --timeout "${secs}s" --metrics-brief
fi
echo "stress-ng not found; using $cores busy loop(s) for $secs s"
pids=()
trap 'kill "${pids[@]}" 2>/dev/null' EXIT INT TERM
for _ in $(seq "$cores"); do
( end=$((SECONDS + secs)); while (( SECONDS < end )); do :; done ) &
pids+=($!)
done
wait