feat: autotune PSU capacity from the same full-load run as fan ceilings

Extract the fan peak-tracking into observedPeakStore (observe max under
load, hold >= minHold to reject spikes, round, persist JSON) and add a
second instance for PSU draw (psu-observation.json, keyed by PSU
ordinal). Fed from samplePSUPower like fans are from sampleFanSpeeds, so
any full-load run refines it — the Fan Ceiling Check (which also samples
PSU power at a slow cadence off its loop and writes psu_<i>_peak_w), a
burn, thermal cycling, and the 5s web metrics collector.

/topo PSU cards now scale the load fill by wattage_w when the BMC
reports it, else by the observed peak draw — marked "~N% load". This
MSI stand's BMC gives only instantaneous input power, so the observed
peak is the only capacity figure available.

Fan behaviour is unchanged (tests exercise updateFanObservation /
estimateFanDutyCyclePctFromObservation / ResolveFanMaxRPM through the
new store).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
This commit is contained in:
Mikhail Chusavitin
2026-09-04 12:53:13 +03:00
co-authored by Claude Sonnet 5
parent e713504fc9
commit 7ed652c5b1
7 changed files with 396 additions and 175 deletions
@@ -57,10 +57,33 @@ The `fan` SAT test (`platform.RunFanCheck`, formerly the unwired
fan rose meaningfully above baseline, declare the ceiling found and stop —
**success**. `MaxLoadSec` (default 900 s) is a hard cap; hitting it is also
success (the highest RPM seen is still recorded).
4. Persist each peak through the existing `updateFanObservation` path
4. Persist each peak through `updateFanObservation`
(`/var/log/bee-sat/fan-observation.json`), which is what `ResolveFanMaxRPM`
reads.
## The observed-peak store (autotune primitive)
`platform.observedPeakStore` (`observed_peaks.go`) is the shared mechanism:
observe the max value per key while the box is under load, require a candidate
to hold ≥ `minHold` before it sticks (rejects spikes), round up, persist to a
`{"<jsonKey>": {key: peak}}` JSON file. Two instances:
- `fanPeaks``fan-observation.json` `max_rpm`, keyed by fan name, round-up 1000.
- `psuPeaks``psu-observation.json` `max_w`, keyed by **PSU ordinal**, round-up 50.
Both are fed from the ordinary telemetry paths — `sampleFanSpeeds` and
`samplePSUPower` call `update*Observation` — so **any** full-load run refines
them: the Fan Ceiling Check itself, a burn, thermal cycling, power
calibration, and the 5 s web metrics collector while any of those run. There
is no separate "PSU autotune" test: the fan check's max-CPU+GPU load is
already the right moment to observe peak PSU draw, and it samples PSU power at
a slow cadence off its own loop (`psu_<i>_peak_w` in the summary).
This exists because BMCs like the MSI stand's report only instantaneous PSU
input power, no nameplate rating. `/topo` PSU cards scale the load fill by the
real `wattage_w` when present, otherwise by `ObservedPSUMaxW()` — marked as an
estimate (`~N% load`).
## Verdict mapping
- A fan reading **0 RPM**, or IPMI status **cr/nr**, while under full load →