hardware sensors — HardwareSensorPoller
sample/HardwareSensorPoller.java. Opt-in bridge to
LibreHardwareMonitor's
(LHM) built-in Remote Web Server plugin. Backs the four GPU/VRAM stats — see
stats.html#performance. No native code, no vendor SDK, no
GPU driver calls of any kind live in this codebase; it is exclusively an HTTP client against
LHM's own JSON endpoint.
lifecycle
| call | when | effect |
|---|---|---|
startIfEnabled() | MineTunerClient.onInitializeClient() | no-op if hardwareSensorsEnabled is false or a poller is already running. Otherwise builds an HttpClient, spawns a daemon thread named minetuner-hardware-sensor-poller. |
stop() | disabled via config | sets stopRequested, interrupts the thread. Thread exits at its next Thread.sleep wakeup. |
reconcileWithConfig() | after any Cloth Config save touching sensor settings | stops+restarts if base URL or request-timeout changed (timeout is baked into the HttpClient at construction, can't be swapped live); starts fresh if newly enabled; stops + resets published values if newly disabled. |
Poll interval, unlike the request timeout, is read live every cycle
(MineTunerConfig.getInstance().hardwareSensorPollIntervalMs inside the sleep call) — a
config change to it takes effect on the very next sleep with no restart needed. This asymmetry
is deliberate, not an oversight; see the inline comment in runLoop() if you're
touching this.
setDaemon(true)) — it never keeps the JVM
alive on its own, no explicit shutdown-on-exit logic exists or is needed.poll cycle (pollOnce())
- Build a GET request to
<baseUrl>/data.jsonwith the configured request timeout. - Malformed base URL → logged once per outage as an "invalid base URL" message, sensors marked unavailable, return.
- Any network exception (connection refused, timeout, unknown host, anything
java.net.httpthrows) → logged once per outage as "could not reach", sensors marked unavailable, return. - Non-200 response → logged once as "unexpected HTTP <code>", sensors marked unavailable, return.
- Unparseable JSON (e.g. LHM returned an HTML error page, or a future LHM version changed its shape) → logged once as "could not parse data.json", sensors marked unavailable, return.
- Success → extract readings (below), write all five
MineTunerDataHolderfields, fliphardwareSensorsReachable = true, clear the outage-warned flag so the next failure logs again.
The whole loop body (not just pollOnce()'s own internals) is additionally wrapped
in a catch-Throwable at the runLoop() level — belt and suspenders
against literally anything, including an Error subtype, so one bad response cycle
can never silently kill the polling thread and leave stats stuck at stale values forever.
outage logging
Exactly one warning is logged per outage, not per poll — a boolean
warnedThisOutage flag gates it, cleared only on the next successful poll. If LHM is
simply not running, that's one log line total for as long as it stays down, not one every
1.5 seconds.
wire format — LHM's sensor tree
LHM's data.json is a recursive node tree: every node optionally has a
"Children" array of more nodes, and leaf sensor nodes carry "SensorId",
"Text", and "Value" string fields.
SensorId: "/gpu-nvidia/0/temperature/0"
Text: "GPU Core"
Value: "45.2 °C"
sensorTypeFromId(sensorId) takes the second-to-last /-delimited
segment as the sensor "type" (temperature, clock, load,
smalldata in the example above). findFirstSensor(root, type, nameContains)
does an iterative breadth-first walk — explicitly not recursive, since LHM's tree depth
isn't bounded by anything this mod controls, and an unbounded recursive walk risks a stack
overflow on a pathological tree. A hard cap (MAX_NODES = 20_000) bounds total work
regardless, so one poll can never turn into an unbounded scan even on a huge tree — this runs on
the background thread, not the render thread, but it's still capped.
Matching is case-insensitive on both the sensor-type segment and a substring check against
Text:
| published field | sensor type | text contains |
|---|---|---|
gpuTempC | temperature | "gpu core" |
gpuClockMhz | clock | "gpu core" |
gpuUsagePercent | load | "gpu core" |
vramUsedMb | smalldata | "gpu memory used" |
vramMaxMb | smalldata | "gpu memory total" |
parseLeadingNumber(raw) handles LHM's "45.2 °C"-style formatting —
walks leading sign/digits/decimal point, normalizes a comma decimal separator to a dot (for
locales that format that way), stops at the first non-numeric character (the unit suffix), and
returns null if no digit was ever seen. No thousands-grouping handling — not needed,
since sensor values here (temps, clocks, percentages) are always small enough not to have one.
failure state
Any failure — network, parse, missing sensor — sets all five fields to sentinel
-1.0 and hardwareSensorsReachable = false. Consuming
StatDefinitions (GpuTempStat etc.) treat the sentinel as "don't render
this line," the same degrade-silently convention MSPT uses on remote servers. There is no
separate "error" rendering state in the HUD — a missing sensor is just absent, not shown as an
error string.
setup, for completeness
- Install and run LibreHardwareMonitor.
- In LHM: Options → Remote Web Server → Run. Default port serves
http://localhost:8085/data.json. - Enable in minetuner via
/minetuner config→ Hardware Sensors, set base URL to match.
Debug checklist if a stat doesn't show up (mirrors what markUnavailable()/outage
logging would tell you if you're watching the client log):
hardwareSensorsEnabledis actuallytrue.- LHM's own UI shows the Remote Web Server as running.
hardwareSensorBaseUrlmatches the port LHM is actually serving on.- Nothing local (firewall/AV) is blocking the loopback connection.