feat: native WireView Pro II monitoring (detection, serial protocol, live tab)

Standalone support for the ThermalGrizzly WireView Pro II without
depending on the external wireview_reporter exporter:

- wireview.py: serial protocol (STX/ETX + 16-bit CRC16-CCITT) with
  vendor-data product identification, config version, UID, build
  string, screen layout, and temperature/power/current sensor reads;
  hwmon fallback with per-channel index resolution; udev-based
  detection (vendor 0x2560 / product 0x0101) with fallback port
  probing; serial read timeout and watchdog reconnect
- server.py: startup detection (root only), 1 Hz poller gated on
  subscribed clients, WS 'wireview' channel, GET /api/wireview,
  rejected-product memoization, stale-read race guard
- frontend: WireView tab (visible when a device is detected) with
  live temperature/power/current cards, sparkline history, and
  device info; nullable temp channels
- tests: 76 tests covering parser, CRC, fault classification,
  hwmon resolution, JSON safety, and the serial transport against
  a pty-based fake device

Verified live: tab appears with the device connected, disappears
when unplugged, reconnects on re-plug, no serial traffic when idle.
This commit is contained in:
ARIA committed 2026-10-03 13:20:56 +02:00
1 parent cc102f26c1
commit 526b71b45f
10 files changed
+1792 -1

No files matched your search

+19 -1
View File
@@ -11,10 +11,12 @@ import { PerformancePanel } from "./components/Limits/PerformancePanel.js";
import { PerformanceMonitor } from "./components/Monitor/PerformanceMonitor.js";
import { FanMonitor } from "./components/Monitor/FanMonitor.js";
import { FanCurveEditor } from "./components/Fans/FanCurveEditor.js";
import { WireViewPanel } from "./components/WireView/WireViewPanel.js";
import { ProfilePanel } from "./components/Profiles/ProfilePanel.js";
import { api, onUnauthorized } from "./api/client.js";
import { LoginScreen } from "./components/Auth/LoginScreen.js";
import { useCurveStore } from "./store/curveStore.js";
import { useWireview } from "./hooks/useWireview.js";
import { Toaster } from "sonner";
import { Loader, ChevronDown } from "lucide-react";
import { useState, useRef, useEffect } from "react";
@@ -94,9 +96,10 @@ function MainApp({
const { dashboard, loading: dashboardLoading } = useDashboard();
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } =
useCurveStore();
const wireview = useWireview();
const [activeTab, setActiveTab] = useState<
"dashboard" | "curve" | "performance" | "fans"
"dashboard" | "curve" | "performance" | "fans" | "wireview"
>("dashboard");
const [fanState, setFanState] = useState<FanState | null>(null);
const [activeDomain, setActiveDomain] = useState<"gpu" | "memory">("gpu");
@@ -195,6 +198,15 @@ function MainApp({
>
Fans
</button>
{wireview.available && (
<button
onClick={() => setActiveTab("wireview")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors flex items-center gap-2 ${activeTab === "wireview" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
WireView
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
</button>
)}
</div>
<div className="relative" ref={profileRef}>
<button
@@ -289,6 +301,12 @@ function MainApp({
/>
</div>
</div>
) : activeTab === "wireview" ? (
<WireViewPanel
info={wireview.info}
sample={wireview.sample}
history={wireview.history}
/>
) : (
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">