SensorView: 3-thread architecture, embedded LAN dashboard, and Hex Viewer - #17
Merged
Conversation
Restructures SensorView around three cooperating threads and adds an
embedded HTTP/WebSocket server that streams telemetry to browsers.
Thread 1 (poll.rs) is now the single writer. It builds an immutable
TelemetryFrame and publishes it through the new TelemetryStore (state.rs),
which swaps an ArcSwap pointer and serializes to JSON exactly once per tick.
Thread 2 (GUI) and Thread 3 (web) read that pointer without locking, which
also removes the per-frame deep clone of the whole hardware tree the UI used
to do in every window. UI mutations travel to the poller as poll::Commands
over an mpsc channel instead of contending on a shared lock.
Thread 3 (web/) serves the bundled SPA plus /api/telemetry, /api/system,
/api/history/{id}, /api/health, /metrics and /ws/telemetry. Every WebSocket
client forwards the same Arc<String>, so N clients cost O(1) per tick, and a
slow client receives RecvError::Lagged and resyncs rather than applying
backpressure to the hardware loop.
Two deliberate departures from a naive reading of the brief:
- Polling is tiered. S.M.A.R.T. reads keep drives out of low-power states and
sub-500 ms SMBus polling provokes SMI storms, so storage/SPD/PCIe live on a
30 s slow lane (inventory.rs) on their own thread, where a multi-second read
cannot delay a 1 s sensor tick.
- The dashboard binds loopback by default. It exposes drive serials and raw
SPD/PCI dumps, so any non-loopback bind makes a per-run access token
mandatory on every route except /api/health.
Also fixes two defects found along the way: PollHandle::stop() now sends
Command::Shutdown instead of only clearing a flag (the loop parks in
recv_timeout, so exit could stall for a full interval, up to 10 s), and
rust-version was corrected from a stale 1.77 to 1.92 — cargo honours it during
resolution and was silently downgrading new dependencies.
Model gains the shapes the telemetry work needs: NVMe Health Log 0x02 decoded
at spec offsets, ATA S.M.A.R.T. attributes with vendor overrides, PCIe link
state with degradation detection, and raw hex blobs.
57 tests pass; clippy is clean with -D warnings and -D await_holding_lock; the
GUI-only (--no-default-features) build is checked in CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ev hooks Two follow-ups from reviewing the milestone: TelemetryStore::publish serialized the whole hardware tree every tick even when the web tier was compiled out, because json() was cfg-gated but the work that fed it was not. Nothing read the result, so a --no-default-features build paid a full-tree serialization per second for no consumer — defeating the point of the feature flag. The field and the serialization are now both behind feature="web". The main.rs rewrite had dropped SENSORVIEW_OPEN_GRAPH and SENSORVIEW_START_LOGGING; restored on top of the new store, and added SENSORVIEW_SETTINGS_TAB. These exist so windows that normally require a click — the graph window and each settings tab — can be rendered from a script. Used them to smoke-test the three windows the unit tests can't reach: sensors, graph, and all six settings tabs (including the new Remote Access tab with the token block) render without panic, and CSV logging writes rows from the poll thread as intended. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Symptom: no sensors at all — the app silently fell back to demo data. Cause: LHM reports the per-core Clock and Factor (multiplier) sensors as NaN/Infinity on this machine, because the blocked WinRing0 driver leaves the effective-clock calculation dividing by a zero delta. System.Text.Json refuses to write non-finite floats and throws, so the very first snapshot raised an unhandled exception and the sidecar exited with 0xE0434352 before emitting a single tree. The Rust side correctly reported "sidecar exited early" and fell back to demo data. Two fixes, both needed: - Map non-finite readings to null in MapSensor. A NaN is not a reading, and the Rust model already expresses "no reading" as Option<f32>::None. - Catch non-IO exceptions around the per-tick write, so one bad snapshot skips a tick instead of taking the process down. Losing 151 sensors because one of them divided by zero is not an acceptable failure mode. Verified: the sidecar now streams continuously; the app reports source "LibreHardwareMonitor bridge" with 151 sensors across 12 devices (CPU, both GPUs, memory, six network adapters) instead of demo data. Note this is the same blocked-driver root cause as the existing 0 W / 0 MHz issue, which had escalated from "reads zero" to "kills the sensor engine". The 17 sensors that now read null are the 8 per-core clocks, 8 per-core multipliers and one Wi-Fi utilization counter; those stay unavailable until PawnIO is installed. Everything else reads correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hex inspector from the SYS_KERN_MONITOR mockups, as a native egui window (toolbar "0x Hex", or SENSORVIEW_SHOW_HEX=1). Read "HEX disassembler" as the hex viewer the mockups actually specify — the "Follow in Disassembler" button and "ACPI Disasm" tab there are stubs, and ACPI/SMBIOS bytes are not x86 instructions, so no instruction decoder is implied. Layout follows the reference: offset · 16 bytes in 4-byte groups with a mid-row gutter · ASCII gutter, selection synced across both columns. Rows go through egui_extras::TableBuilder so only visible ones are built — the VFCT table here is 44 KiB. Three things make it an inspector rather than a wall of hex: - Field annotations. Blobs carry HexRegions; the ACPI header's signature/length/checksum/OEM fields are tinted by kind and named under the cursor. - Byte decoding. The selected offset is decoded as int8..uint64, float32/64, binary, char and ascii[8], in either byte order. Types that would read past the end are omitted rather than shown as zero. - Search (hex or text, wrapping) and go-to-offset. Data source: src/source/firmware.rs reads ACPI and SMBIOS via GetSystemFirmwareTable — plain kernel32, no driver and no elevation, so the viewer works on this machine despite the blocked Ring-0 driver. PCI config space, the mockup's literal subject, needs that driver and is not readable yet. Linux reads /sys/firmware/acpi/tables (root only). Demo mode ships a synthetic PCI-config blob so the window is exercisable anywhere. Read-only by design: no path writes to hardware. Committing bytes back to SPD EEPROM or PCI config can brick a board and needs the same missing driver, so Export writes a .bin and .txt copy instead. Two defects found while building it: - HexBlob::find clamped `from` down to the last possible match, so searching past the final hit silently searched backwards and re-reported it. Find-next could never advance off the last match. - Windows keys ACPI tables by signature, so requesting 'SSDT' fifteen times returns the same bytes fifteen times. Those were being listed as fifteen distinct 34 KiB tables. Now read once and labelled "SSDT (1 of 15)". On Linux sysfs does expose them separately, so index and count are both real there. Also corrected a test that assumed a DSDT: Windows does not publish it through this API, verified against the 29 tables this machine lists. Verified on real firmware: 16 blobs (MCFG, FACP, APIC, IVRS, SSDT, VFCT, SMBIOS 3.8, …), window renders and titles itself from the selected blob, 72 tests pass, clippy clean, GUI-only build checked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ✕, 💾 and ▶ glyphs aren't in Segoe UI or Cascadia Mono as loaded, so they rendered as empty boxes in the toolbar. Verified by screenshotting the running window rather than assuming the font had them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-checked against HWiNFO's own manual (§10.5, Core Frequency reading in Windows 11) and verified on this machine: two independent blockers are active, not one. - VulnerableDriverBlocklistEnable = 1 blocks WinRing0, killing SMU/MSR power. - VirtualizationBasedSecurityStatus = 2 (VBS running) blocks the BCLK reads every ratio-based clock depends on — even though Memory Integrity is already off (HVCI Enabled = 0). The manual notes exactly this case and says the Virtual Machine Platform and Windows Hypervisor Platform features must go too. The second one is what produced the NaN clocks that crashed the sidecar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the three-thread architecture, an embedded LAN telemetry server, and the Hex Viewer pane — plus a fix for a crash that was taking out every sensor.
Thread architecture
The UI previously locked the poller and deep-cloned the whole hardware tree every frame, in every window. Adding a web server on that lock would have made contention the dominant cost. Instead the poller is the sole writer and publishes an immutable
Arc<TelemetryFrame>viaArcSwap; readers never block, and the deep clone is gone.Why it can't deadlock (
src/state.rs,src/main.rs):poll::Commandover anmpscchannel..await— enforced by#![deny(clippy::await_holding_lock)]plus a CI gate.Laggedand resyncs; it can never slow the hardware loop.Web tier (
src/web/, featureweb, default on)GET /(embedded SPA) ·/api/telemetry·/api/system·/api/history/{id}·/api/health·/metrics(Prometheus) ·/ws/telemetry.Each frame is serialized once into an
Arc<String>that every client forwards, so N clients cost O(1) per tick, not O(N).Security: binds
127.0.0.1:8080by default. This endpoint exposes drive serials and raw SPD/PCI dumps, so any non-loopback bind makes a per-run token mandatory on every route except/api/health. Verified 401 on/api/telemetry,/, and the WS upgrade without it; 200 via query string,Authorization: Bearer, and WS with it.Hex Viewer (
src/ui/hex_window.rs)Offset · 16 bytes in 4-byte groups with a mid-row gutter · ASCII gutter, selection synced across columns, rows virtualized through
egui_extras::TableBuilder. Beyond a plain dump: ACPI header fields are tinted by kind and named under the cursor, and the selected offset decodes asint8..uint64,float32/64, binary, char andascii[8]in either byte order. Search (hex or text, wrapping) and go-to-offset.Data comes from
GetSystemFirmwareTable(ACPI + SMBIOS) — plain kernel32, no driver and no elevation — so it works despite the blocked Ring-0 driver on the dev machine. Linux reads/sys/firmware/acpi/tables. Read-only by design: nothing writes to hardware; Export writes a.bin/.txtcopy.Two deliberate departures from the brief
Bugs found and fixed along the way
PollHandle::stop()only cleared a flagrecv_timeoutrust-version = "1.77"was staleegui_tilesto a version built against egui 0.30HexBlob::findclamped its start offset downwardStringper frameThe sensor outage is worth its own note: with VBS blocking BCLK reads, LHM returned NaN for the per-core clocks, and
System.Text.Jsonthrows rather than write non-finite floats — so the sidecar exited0xE0434352on its first snapshot. It now maps non-finite readings tonulland survives a bad tick.Verification
-D warnings -D clippy::await_holding_lock; GUI-only (--no-default-features) build checked. CI now gates all three.seq, priming frame in 65 ms,ping→pong; REST + Prometheus + history (404 on unknown sensor); token gate verified both ways; graceful close releases the port with no orphans.source: LibreHardwareMonitor bridge, 151 sensors across 12 devices (Ryzen 7 7700, RTX 5060, Radeon iGPU, memory, six NICs) — previously demo data.Not included
Docking (
egui_tilesresolves but is unused), the topology and storage panes, and the per-OS sensor backends.feature/portable-buildremains an open item — the branch exists but its workflow was never written.Known environmental issue (not a code defect)
Two blockers on the dev machine, confirmed against HWiNFO's own manual §10.5:
VulnerableDriverBlocklistEnable17 sensors report "no value" until those are resolved; the other 134 read correctly.
🤖 Generated with Claude Code