Compare commits
20 Commits
v1.0.0-rc.
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
2faa4508d9 | |
|
|
44b8a2e84a | |
|
|
df11808d78 | |
|
|
7332c47d6d | |
|
|
c62d8270b3 | |
|
|
fdb3c50b68 | |
|
|
8850337250 | |
|
|
ed72f97fe5 | |
|
|
a326a758c4 | |
|
|
0076e623d0 | |
|
|
fc507f7407 | |
|
|
ff89643a4f | |
|
|
1c2f0a0506 | |
|
|
28ff027950 | |
|
|
474e3cac8a | |
|
|
3d90af8490 | |
|
|
a03da2d8f4 | |
|
|
e1f63108f2 | |
|
|
2737811e2e | |
|
|
6fa43f1e08 |
|
|
@ -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}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,13 +77,19 @@ struct LoadInfo {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- live readers ----
|
// ---- live readers ----
|
||||||
ThermalInfo readThermal(); // scans /sys/class/thermal
|
ThermalInfo readThermal(const std::string& root = "/"); // scans sys/class/thermal
|
||||||
DiskInfo readDisk(const std::string& path); // statvfs() on the filesystem holding `path`
|
CpuFreqInfo readCpuFreq(const std::string& root = "/");
|
||||||
MemInfo readMeminfo(); // /proc/meminfo
|
ThrottleCount readThrottleCount(const std::string& root = "/");
|
||||||
LoadInfo readLoad(); // getloadavg() + nproc
|
CpuTimes readCpuTimes(const std::string& root = "/");
|
||||||
|
DiskInfo readDisk(const std::string& path); // statvfs() on the filesystem holding `path`
|
||||||
|
MemInfo readMeminfo(); // /proc/meminfo
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
7
main.cpp
7
main.cpp
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,22 +214,31 @@ 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;
|
||||||
}
|
}
|
||||||
// The camera's BayerRG is OpenCV's BayerBG (see RawImage.h).
|
// The camera's BayerRG is OpenCV's BayerBG (see RawImage.h).
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,11 +2171,76 @@ 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;
|
||||||
: ": reset FAILED (see above)");
|
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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `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
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"}},
|
||||||
|
|
|
||||||
|
|
@ -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,26 +58,161 @@ 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);
|
||||||
|
std::string type = readFile(e.path() / "type");
|
||||||
|
// strip trailing newline
|
||||||
|
while (!type.empty() && (type.back() == '\n' || type.back() == '\r')) type.pop_back();
|
||||||
|
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) {
|
if (!t.ok || c > t.max_temp_c) {
|
||||||
t.ok = true;
|
t.ok = true;
|
||||||
t.max_temp_c = c;
|
t.max_temp_c = c;
|
||||||
std::string type = readFile(e.path() / "type");
|
t.hottest_zone = type;
|
||||||
// strip trailing newline
|
|
||||||
while (!type.empty() && (type.back() == '\n' || type.back() == '\r')) type.pop_back();
|
|
||||||
t.hottest_zone = type.empty() ? name : type;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (t.ok && !have_pkg) {
|
||||||
|
t.cpu_temp_c = t.max_temp_c;
|
||||||
|
t.cpu_zone = t.hottest_zone;
|
||||||
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
129
src/ui/TuiUi.cpp
129
src/ui/TuiUi.cpp
|
|
@ -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))
|
||||||
: (text("stopped") | color(Color::Yellow));
|
: d.streaming ? (text("streaming") | color(Color::Green))
|
||||||
|
: (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());
|
||||||
rows.push_back(hbox({text("CAMERAS") | bold | color(Color::Blue),
|
{
|
||||||
text(cf.mock ? " MOCK (simulated)" : " Vimba X") | dim}));
|
// 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),
|
||||||
|
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,8 +513,12 @@ 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",
|
||||||
" effort " + std::to_string(v.jxl_effort))));
|
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))));
|
||||||
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")
|
||||||
: std::to_string(v.rotation_deg) + " deg CW")));
|
: std::to_string(v.rotation_deg) + " deg CW")));
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)"
|
||||||
|
|
@ -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)"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue