index architecture stat-reference template-mode config-schema hardware-sensors build+ci | repo↗ homepage↗

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

callwheneffect
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 configsets stopRequested, interrupts the thread. Thread exits at its next Thread.sleep wakeup.
reconcileWithConfig()after any Cloth Config save touching sensor settingsstops+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.

Thread is a daemon (setDaemon(true)) — it never keeps the JVM alive on its own, no explicit shutdown-on-exit logic exists or is needed.

poll cycle (pollOnce())

  1. Build a GET request to <baseUrl>/data.json with the configured request timeout.
  2. Malformed base URL → logged once per outage as an "invalid base URL" message, sensors marked unavailable, return.
  3. Any network exception (connection refused, timeout, unknown host, anything java.net.http throws) → logged once per outage as "could not reach", sensors marked unavailable, return.
  4. Non-200 response → logged once as "unexpected HTTP <code>", sensors marked unavailable, return.
  5. 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.
  6. Success → extract readings (below), write all five MineTunerDataHolder fields, flip hardwareSensorsReachable = 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 fieldsensor typetext contains
gpuTempCtemperature"gpu core"
gpuClockMhzclock"gpu core"
gpuUsagePercentload"gpu core"
vramUsedMbsmalldata"gpu memory used"
vramMaxMbsmalldata"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

  1. Install and run LibreHardwareMonitor.
  2. In LHM: Options → Remote Web Server → Run. Default port serves http://localhost:8085/data.json.
  3. Enable in minetuner via /minetuner configHardware 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):