Skip to content

Add PMIC temperature reading (vcgencmd measure_temp pmic) - #164

Merged
LarsLaskowski merged 4 commits into
mainfrom
claude/jolly-darwin-daq8fw
Sep 19, 2026
Merged

LarsLaskowski merged 4 commits into
mainfrom
claude/jolly-darwin-daq8fw

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

📖 Description

Adds the Raspberry Pi 4/5 Power-Management IC temperature as an optional,
additive reading — a feature, not a fix, and not a breaking change.

temperature and gpu_temperature both read the single SoC die sensor (CPU
and GPU share the die), so the PMIC is the only genuinely additional on-board
sensor available without extra hardware, and it is useful for spotting
power-delivery / board-level heat distinct from CPU load. Raspberry Pi OS does
not expose it through sysfs hwmon, so the hwmon collector (D7) cannot pick it
up — this is the vcgencmd path the two issues split between them.

What changed:

  • internal/collector/temperature.go runs vcgencmd measure_temp pmic in
    addition to the plain measure_temp, reusing the existing runner and parser.
    The two readings are independent: a board that answers one but not the other
    (a Pi 3 has no PMIC) still reports the one it has, and neither failure fails
    collection.
  • parseVcgencmdTemp now returns (float64, error) instead of
    (GPUTemperature, error), so the PMIC reading does not have to borrow the
    GPU type's name. GPUTemperature is still constructed in Collect, so the
    gpu_temperature JSON shape is untouched.
  • vcgencmdRunner.run became variadic to carry the pmic argument — still a
    fixed argv, no shell, and the explicit empty child environment is preserved.
  • internal/collector/types.go / collector.go: new PMICTemperature type and
    an omitempty pmic_temperature object on the snapshot, wired into the fast
    tick next to the GPU reading.
  • WorstCaseTickOverhead grows from 2x to 3x vcgencmdTimeout. This is the
    one non-obvious consequence: a fast tick now makes three sequential firmware
    calls, and leaving the budget at two would let /healthz flap on a Pi whose
    only problem is slow firmware. The default /healthz staleness bound widens
    from 39 s to 49 s at a 5 s poll interval, for every board.
  • Parity surfaces: pimonitor_pmic_temperature_celsius on /metrics, a PMIC:
    entry in the dashboard's Temperature card sub-line, and docs/API.md,
    docs/ARCHITECTURE.md, README.md.
  • Follow-up commits (da1e44d, 2ce3dfe): a pmicUnsupported latch on
    TemperatureCollector so a board without a PMIC sensor stops re-invoking
    vcgencmd measure_temp pmic on every fast tick once it has answered with
    something other than a temperature reading (a real exec/timeout failure
    stays unlatched and keeps retrying, since that's transient). The three
    PMIC-specific Collect() tests, and the four parseVcgencmdTemp tests, are
    now table-driven per docs/TESTS.md, plus two new tests for the latch
    itself (it engages after exactly one invocation; a failed exec never
    engages it).

🎫 Issues

Closes #56

👩‍💻 Reviewer Notes

Hardware verification is still outstanding. No test here touches real Pi
firmware, and two claims cannot be checked off-Pi:

  1. That vcgencmd measure_temp pmic returns temp=NN.N'C on a Pi 4/5 — the
    fixtures assume the issue's description is accurate.
  2. That a board without a PMIC errors rather than silently ignoring the
    unknown argument. If some firmware ignores it and returns the SoC
    temperature instead, pmic_temperature would carry the die reading on that
    board. Both new failure-path tests assume the error behaviour. A smoke test
    on a real Pi 4/5 (expect a PMIC value, typically a few degrees above the
    die) and, if possible, a Pi 3 (expect the field to be absent, with
    gpu_temperature still present) would settle both.

Focus areas: the parseVcgencmdTemp signature change and its callers; the
WorstCaseTickOverhead reasoning above; and the pmicUnsupported latch added
in the follow-up commits.

This change went through three in-session pimonitor-reviewer rounds before
each push:

  • Round 1 (initial, 89b13e5): 0 blocking / 2 non-blocking — no latch for
    an unsupported PMIC sensor, and three near-identical PMIC tests that should
    have been table-driven. Recorded below at the time as deliberately deferred.
  • Round 2 (delta of da1e44d, addressing both of the above): 1 blocking /
    1 non-blocking
    — the one line whose behavior actually changed (a temp=
    prefix followed by a number strconv can't parse, which now also latches)
    had no test reaching it; and the pmicUnsupported field comment overclaimed
    that the latch fires only when "the board has no PMIC", when it actually
    fires on any unrecognized output.
  • Round 3 (delta of 2ce3dfe, fixing both): clean pass, independently
    verified by coverage diff (parseVcgencmdTemp 90.0% → 100.0%) and a
    mutation check (removing the new sentinel wrap makes only the new test row
    fail).

📑 Test Plan

All fixture-based per docs/TESTS.md — no real /proc, /sys or firmware
access. The existing writeFakeVcgencmd helper is reused; a new
pmicAwareVcgencmd variant answers measure_temp pmic differently from
measure_temp so both sensors can be simulated independently.

  • TestParseVcgencmdTemp (table-driven) — the reused parser decodes a PMIC
    temp=52.1'C fixture, and rejects three non-reading shapes (error=1 error_msg=..., garbage output, and a temp= prefix with an unparseable
    number) as errVcgencmdUnsupportedOutput.
  • TestTemperatureCollector_Collect_PMIC (table-driven) — a real PMIC
    reading, an "unsupported" error line, and a non-zero exit: in every case the
    GPU/SoC reading and overall collection survive, only the PMIC field's
    presence differs.
  • TestTemperatureCollector_Collect_PMICUnsupportedLatches — once vcgencmd
    answers "unsupported" for pmic, three subsequent Collect() calls invoke
    the binary exactly once in total.
  • TestTemperatureCollector_Collect_PMICTransientFailureKeepsRetrying — a
    non-zero exit never latches, since the same board might still have a PMIC.
  • Existing _Collect and _VcgencmdExecFails extended to assert the PMIC
    field is absent off-Pi and when the binary fails.
  • TestVcgencmdRunner_Run_PassesSubcommandArguments — pmic reaches the
    binary as its own argv entry.
  • TestCollector_FastTick_PMICTemperature — the reading reaches the published
    snapshot.
  • TestWorstCaseTickOverhead_CoversEveryVcgencmdInvocation — counts the
    vcgencmd invocations a fast tick actually makes and requires the constant to
    budget for all of them, so a future firmware call cannot silently break
    /healthz.
  • Prometheus: present-when-set and omitted-when-absent both extended;
    TestAppJS_RendersPMICTemperature guards the dashboard line.

go build ./..., go vet ./... and go test ./... -race -cover pass locally
(collector 92.0%, httpapi 98.5%). golangci-lint run could not be executed in
this environment
— the installed binary is built against go1.25 while the repo
targets go1.26.7, so it refuses to load the config. CI covers it.

✅ Checklist

General

  • I have added/updated tests for my changes (go test ./... -race -cover passes locally).
  • go vet ./... and golangci-lint run are clean. — go vet is clean; golangci-lint could not run locally (toolchain mismatch, see Test Plan), left to CI.
  • I have tested my changes. — verified by the test suite only; no Raspberry Pi hardware available, see Reviewer Notes.
  • I have read the CONTRIBUTING documentation and followed the project's code style guidelines.
  • I have updated ARCHITECTURE.md if this changes a documented design decision.

REST API / configuration / packaging

  • I have updated docs/API.md to reflect a REST API change.
  • No breaking change to /api/v1/... response shapes, or a new API version (/api/v2/...) was introduced instead.
  • I have updated README.md / packaging/pimonitor.example.yaml to reflect a new or changed configuration option. — README updated; no new config option, so the example YAML is unchanged.
  • I have updated packaging/install.sh or the systemd units if this changes installation/packaging, and kept the unprivileged/privileged service split intact (see SECURITY.md). — no packaging change needed: vcgencmd was already in the service's permitted shell-out set, and no new privilege is required.

⏭ Next Steps

Both items originally listed here (the missing "stop trying" latch, and
table-driven tests) were addressed in the follow-up commits described above.
Nothing outstanding beyond the hardware verification called out in Reviewer
Notes.

The Pi 4/5 carry a Power-Management IC with its own temperature sensor,
the only genuinely additional on-board sensor available without extra
hardware: `temperature` and `gpu_temperature` both read the single SoC
die sensor, and Raspberry Pi OS does not expose the PMIC through sysfs
hwmon, so the hwmon collector cannot pick it up either.

Read it via `vcgencmd measure_temp pmic` and expose it as an additive,
optional `pmic_temperature` object in the snapshot (plus the matching
Prometheus gauge and the dashboard's Temperature card). The output form
is identical to plain `measure_temp`, so parseVcgencmdTemp is reused; it
now returns a plain float64 so neither reading has to borrow the other's
type. Boards without the sensor answer with an error line or a non-zero
exit, which is treated as "not present" — the field is omitted and
neither collection nor the GPU/SoC reading is disturbed.

WorstCaseTickOverhead grows from 2x to 3x vcgencmdTimeout: the added
call is a third sequential firmware invocation per fast tick, and
leaving the budget at two would let /healthz flap on a Pi whose only
problem is slow firmware. A new test counts the invocations a tick makes
rather than trusting that constant's comment.
Addresses the two non-blocking findings from PR #164's review round 1:

- TemperatureCollector.pmicUnsupported latches true once `vcgencmd
  measure_temp pmic` answers with something that isn't a temperature
  reading (a board with no PMIC sensor, Pi 3 and earlier). That is a
  permanent condition for the life of the process, so a board without
  one no longer pays a pointless vcgencmd invocation on every fast tick
  forever. A real exec/timeout failure does not latch: it's transient
  and worth retrying, same as the GPU/SoC reading.

  parseVcgencmdTemp signals this via a new sentinel,
  errVcgencmdUnsupportedOutput, so Collect can tell "ran and answered,
  just not with a reading" apart from "didn't run at all" using
  errors.Is. Deliberately not extended to the GPU/SoC reading or to
  ThrottledCollector's get_throttled: neither ever answers
  "unsupported" on real Pi hardware (vcgencmd's own detection layer
  already covers "vcgencmd absent" for both), so latching them would
  add code for a state that can't occur.

- TestParseVcgencmdTemp and TestTemperatureCollector_Collect_PMIC are
  now table-driven (docs/TESTS.md) instead of four and three
  near-identical top-level functions. Two new tests cover the latch
  itself: one counts vcgencmd invocations across three Collect() calls
  to confirm only the first one actually runs the PMIC subcommand, the
  other confirms a non-zero exit never sets the latch.
Round-2 review of the PMIC latch found that its one behavior-changing
line (a temp= prefix followed by a number strconv can't parse) was
never exercised by any test, even though it now permanently disables
the PMIC reading for the process's lifetime, same as the "error="
response. Add that case to TestParseVcgencmdTemp's table, which
brings parseVcgencmdTemp to 100% coverage.

Also softened the pmicUnsupported field comment: the latch actually
fires on any unrecognized vcgencmd output, not only its "error=1"
response, so "the board itself has no PMIC sensor" is now phrased as
"in practice" rather than as a categorical guarantee.
SonarCloud flagged TestTemperatureCollector_Collect_PMIC's cognitive
complexity at 19 against the default threshold of 15: it counts a
t.Run closure's branching into its enclosing function, and the
for-loop-plus-closure combination from the table-driven refactor
crept past that. Move the closure's body into a standalone helper,
assertPMICCollect, called with one line from the closure; behavior
and assertions are unchanged.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit be5c766 into main Sep 19, 2026
12 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/jolly-darwin-daq8fw branch September 19, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

D6: PMIC temperature reading (Raspberry Pi 4/5)

2 participants