mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-30 19:36:24 +00:00
update hardware.tsx
This commit is contained in:
@@ -104,18 +104,64 @@ const formatClock = (clockString: string | number): string => {
|
|||||||
|
|
||||||
const getDeviceTypeColor = (type: string): string => {
|
const getDeviceTypeColor = (type: string): string => {
|
||||||
const lowerType = type.toLowerCase()
|
const lowerType = type.toLowerCase()
|
||||||
|
|
||||||
|
// UPS / battery — amber: warm orange-yellow, distinct from the orange used
|
||||||
|
// for Storage and avoids the "warning" connotation of pure yellow.
|
||||||
|
if (lowerType === "ups" || lowerType.includes("battery")) {
|
||||||
|
return "bg-amber-500/10 text-amber-500 border-amber-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage family — orange (Mass Storage USB class + PCI storage controllers)
|
||||||
if (lowerType.includes("storage") || lowerType.includes("sata") || lowerType.includes("raid")) {
|
if (lowerType.includes("storage") || lowerType.includes("sata") || lowerType.includes("raid")) {
|
||||||
return "bg-orange-500/10 text-orange-500 border-orange-500/20"
|
return "bg-orange-500/10 text-orange-500 border-orange-500/20"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printer — rose, unmistakable
|
||||||
|
if (lowerType.includes("printer")) {
|
||||||
|
return "bg-rose-500/10 text-rose-500 border-rose-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio family — teal (Audio, Audio/Video); placed before video so that
|
||||||
|
// combined "Audio/Video" class labels read as audio-family.
|
||||||
|
if (lowerType.includes("audio")) {
|
||||||
|
return "bg-teal-500/10 text-teal-500 border-teal-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graphics / Video / Imaging — green (cameras, webcams, displays, GPUs).
|
||||||
|
if (
|
||||||
|
lowerType.includes("graphics") ||
|
||||||
|
lowerType.includes("vga") ||
|
||||||
|
lowerType.includes("display") ||
|
||||||
|
lowerType.includes("video") ||
|
||||||
|
lowerType.includes("imaging")
|
||||||
|
) {
|
||||||
|
return "bg-green-500/10 text-green-500 border-green-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network family — blue (Ethernet / Wi-Fi PCI controllers, USB Communications,
|
||||||
|
// CDC Data, Wireless Controllers like Bluetooth dongles).
|
||||||
|
if (
|
||||||
|
lowerType.includes("network") ||
|
||||||
|
lowerType.includes("ethernet") ||
|
||||||
|
lowerType.includes("communications") ||
|
||||||
|
lowerType.includes("wireless") ||
|
||||||
|
lowerType === "cdc data"
|
||||||
|
) {
|
||||||
|
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HID — purple: keyboards, mice, game controllers.
|
||||||
|
if (lowerType === "hid") {
|
||||||
|
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB host controllers (PCI-level) keep the existing purple identity.
|
||||||
if (lowerType.includes("usb")) {
|
if (lowerType.includes("usb")) {
|
||||||
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
|
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
|
||||||
}
|
}
|
||||||
if (lowerType.includes("network") || lowerType.includes("ethernet")) {
|
|
||||||
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
|
// Smart Card, Billboard, Diagnostic, Hub, Physical, Content Security,
|
||||||
}
|
// Personal Healthcare, Miscellaneous, Application/Vendor Specific, unknown.
|
||||||
if (lowerType.includes("graphics") || lowerType.includes("vga") || lowerType.includes("display")) {
|
|
||||||
return "bg-green-500/10 text-green-500 border-green-500/20"
|
|
||||||
}
|
|
||||||
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1511,9 +1557,67 @@ return (
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 rounded-md border border-border/50 bg-muted/30 p-3 text-xs text-muted-foreground">
|
{typeof selectedCoral.temperature === "number" && (() => {
|
||||||
<strong className="text-foreground">Note:</strong> Coral TPUs do not expose temperature, utilization or power telemetry through standard interfaces. Monitoring is limited to device presence and driver state.
|
const trips = selectedCoral.temperature_trips
|
||||||
|
// Dynamic thresholds when the driver exposes trip points.
|
||||||
|
// Otherwise fall back to conservative hardcoded limits.
|
||||||
|
// trips are reported warn→critical, so [N-1] is critical (red)
|
||||||
|
// and [N-2] is the throttle/warn level (amber).
|
||||||
|
const redAt = trips && trips.length >= 1 ? trips[trips.length - 1] : 85
|
||||||
|
const amberAt =
|
||||||
|
trips && trips.length >= 2
|
||||||
|
? trips[trips.length - 2]
|
||||||
|
: trips && trips.length === 1
|
||||||
|
? redAt - 10
|
||||||
|
: 75
|
||||||
|
const color =
|
||||||
|
selectedCoral.temperature >= redAt
|
||||||
|
? "text-red-500"
|
||||||
|
: selectedCoral.temperature >= amberAt
|
||||||
|
? "text-amber-500"
|
||||||
|
: "text-green-500"
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Temperature</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className={`text-sm font-semibold ${color}`}>
|
||||||
|
{selectedCoral.temperature.toFixed(1)} °C
|
||||||
|
</span>
|
||||||
|
{trips && trips.length > 0 && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Thresholds: {trips.map((t) => `${t.toFixed(0)}°C`).join(" · ")}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{selectedCoral.thermal_warnings && selectedCoral.thermal_warnings.length > 0 && (
|
||||||
|
<div className="flex justify-between items-start border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Hardware Warnings</span>
|
||||||
|
<div className="flex flex-col gap-1 items-end">
|
||||||
|
{selectedCoral.thermal_warnings.map((w) => (
|
||||||
|
<div key={w.name} className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono text-muted-foreground">
|
||||||
|
{w.name}
|
||||||
|
{typeof w.threshold_c === "number" && ` @ ${w.threshold_c.toFixed(0)}°C`}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
w.enabled
|
||||||
|
? "text-green-500 border-green-500/20"
|
||||||
|
: "text-muted-foreground/70"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{w.enabled ? "enabled" : "disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!selectedCoral.drivers_ready && (
|
{!selectedCoral.drivers_ready && (
|
||||||
<Button
|
<Button
|
||||||
@@ -2035,7 +2139,6 @@ return (
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-xs text-muted-foreground">Click on an interface for detailed information</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2230,7 +2333,6 @@ return (
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-xs text-muted-foreground">Click on a device for detailed hardware information</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1397,6 +1397,24 @@ _USB_CLASS_LABELS = {
|
|||||||
'ff': 'Vendor Specific',
|
'ff': 'Vendor Specific',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# USB vendor IDs of known UPS manufacturers. UPSs communicate via HID (USB
|
||||||
|
# class 0x03) using the "Power Device" usage page, so raw class detection
|
||||||
|
# labels them as generic "HID" — which is technically correct but useless
|
||||||
|
# for the user. When the device class is HID and the vendor matches this
|
||||||
|
# set, we relabel as "UPS".
|
||||||
|
_USB_UPS_VENDORS = {
|
||||||
|
'0463', # MGE UPS Systems / Eaton (Ellipse, Pulsar, 5P, 9PX, Protection Station)
|
||||||
|
'051d', # American Power Conversion (APC) / Schneider (Back-UPS, Smart-UPS)
|
||||||
|
'0764', # CyberPower Systems
|
||||||
|
'0d9f', # PowerCom
|
||||||
|
'06da', # Phoenixtec Power / Liebert (some models)
|
||||||
|
'09ae', # Tripp Lite
|
||||||
|
'047c', # Dell (select UPS models)
|
||||||
|
'075d', # Salicru
|
||||||
|
'10af', # Liebert / Vertiv
|
||||||
|
'0665', # Cypress Semi (OEM silicon in several UPS brands)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _read_sysfs(path, default=''):
|
def _read_sysfs(path, default=''):
|
||||||
"""Read and trim a sysfs attribute file. Returns default on error."""
|
"""Read and trim a sysfs attribute file. Returns default on error."""
|
||||||
@@ -1498,6 +1516,11 @@ def get_usb_devices():
|
|||||||
device_class = _interface_class_from_usb_device(dev_path).lower()
|
device_class = _interface_class_from_usb_device(dev_path).lower()
|
||||||
class_label = _USB_CLASS_LABELS.get(device_class, 'Unknown')
|
class_label = _USB_CLASS_LABELS.get(device_class, 'Unknown')
|
||||||
|
|
||||||
|
# Refine HID → UPS for known UPS vendors. USB UPSs advertise class
|
||||||
|
# HID with a Power Device usage page; labeling them "HID" looks wrong.
|
||||||
|
if device_class == '03' and vendor_id.lower() in _USB_UPS_VENDORS:
|
||||||
|
class_label = 'UPS'
|
||||||
|
|
||||||
serial = _read_sysfs(os.path.join(dev_path, 'serial'))
|
serial = _read_sysfs(os.path.join(dev_path, 'serial'))
|
||||||
driver = _get_usb_driver(dev_path)
|
driver = _get_usb_driver(dev_path)
|
||||||
|
|
||||||
@@ -1618,6 +1641,108 @@ def _coral_pci_speed(dev_path):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _coral_parse_temp_value(raw):
|
||||||
|
"""Interpret a sysfs temperature reading as float °C.
|
||||||
|
|
||||||
|
The apex driver reports millidegrees (e.g. 54050 → 54.0 °C). Hwmon nodes
|
||||||
|
also use millidegrees. Plain integers like "42" are treated as °C.
|
||||||
|
Returns None on unparsable / empty input.
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
val = int(raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
if abs(val) >= 1000:
|
||||||
|
return round(val / 1000.0, 1)
|
||||||
|
return float(val)
|
||||||
|
|
||||||
|
|
||||||
|
def _coral_pcie_thermal(pci_dev_path):
|
||||||
|
"""Read full thermal picture for a PCIe Coral via the apex driver's sysfs.
|
||||||
|
|
||||||
|
The gasket/apex driver exposes (at least on current feranick / kernel 6.12):
|
||||||
|
temp current die temperature (millidegrees)
|
||||||
|
trip_point{0..2}_temp configured alarm thresholds (millidegrees)
|
||||||
|
hw_temp_warn{1,2} hardware-signalled warning thresholds
|
||||||
|
hw_temp_warn{1,2}_en whether those hw warnings are enabled
|
||||||
|
|
||||||
|
Returns a dict with the fields our payload needs, or an empty dict when
|
||||||
|
no apex_N entry matches this PCI slot (e.g. drivers not loaded, or a USB
|
||||||
|
Coral where this whole sysfs tree doesn't exist).
|
||||||
|
"""
|
||||||
|
result = {'temperature': None, 'temperature_trips': None, 'thermal_warnings': None}
|
||||||
|
try:
|
||||||
|
apex_root = '/sys/class/apex'
|
||||||
|
if not os.path.isdir(apex_root):
|
||||||
|
return result
|
||||||
|
|
||||||
|
target_pci = os.path.basename(pci_dev_path)
|
||||||
|
for apex_name in os.listdir(apex_root):
|
||||||
|
apex_path = os.path.join(apex_root, apex_name)
|
||||||
|
device_link = os.path.join(apex_path, 'device')
|
||||||
|
if not os.path.islink(device_link):
|
||||||
|
continue
|
||||||
|
resolved = os.path.realpath(device_link)
|
||||||
|
if os.path.basename(resolved) != target_pci:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Current temperature (prefer direct attr, fall back to hwmon)
|
||||||
|
candidates = [
|
||||||
|
os.path.join(apex_path, 'temp'),
|
||||||
|
os.path.join(apex_path, 'device', 'temp'),
|
||||||
|
]
|
||||||
|
hwmon_dir = os.path.join(apex_path, 'device', 'hwmon')
|
||||||
|
if os.path.isdir(hwmon_dir):
|
||||||
|
try:
|
||||||
|
for hwmon in os.listdir(hwmon_dir):
|
||||||
|
candidates.append(os.path.join(hwmon_dir, hwmon, 'temp1_input'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for c in candidates:
|
||||||
|
temp = _coral_parse_temp_value(_read_sysfs(c, ''))
|
||||||
|
if temp is not None:
|
||||||
|
result['temperature'] = temp
|
||||||
|
break
|
||||||
|
|
||||||
|
# ── Trip points (alarm thresholds). Typically 3 ascending values:
|
||||||
|
# warn / throttle / critical. Kept ordered as-reported by the driver.
|
||||||
|
trips = []
|
||||||
|
for i in range(3):
|
||||||
|
t = _coral_parse_temp_value(
|
||||||
|
_read_sysfs(os.path.join(apex_path, f'trip_point{i}_temp'), ''))
|
||||||
|
if t is not None:
|
||||||
|
trips.append(t)
|
||||||
|
if trips:
|
||||||
|
result['temperature_trips'] = trips
|
||||||
|
|
||||||
|
# ── Hardware warnings (threshold + enabled flag). Surface only the
|
||||||
|
# pairs that are actually configured so the UI can show a badge
|
||||||
|
# when a warning is enabled.
|
||||||
|
warnings = []
|
||||||
|
for i in (1, 2):
|
||||||
|
threshold = _coral_parse_temp_value(
|
||||||
|
_read_sysfs(os.path.join(apex_path, f'hw_temp_warn{i}'), ''))
|
||||||
|
enabled_raw = _read_sysfs(
|
||||||
|
os.path.join(apex_path, f'hw_temp_warn{i}_en'), '')
|
||||||
|
enabled = enabled_raw.strip() in ('1', 'true', 'yes')
|
||||||
|
if threshold is not None or enabled_raw:
|
||||||
|
warnings.append({
|
||||||
|
'name': f'hw_temp_warn{i}',
|
||||||
|
'threshold_c': threshold,
|
||||||
|
'enabled': enabled,
|
||||||
|
})
|
||||||
|
if warnings:
|
||||||
|
result['thermal_warnings'] = warnings
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_coral_info():
|
def get_coral_info():
|
||||||
"""Detect Coral TPU accelerators (PCIe/M.2 + USB).
|
"""Detect Coral TPU accelerators (PCIe/M.2 + USB).
|
||||||
|
|
||||||
@@ -1649,6 +1774,8 @@ def get_coral_info():
|
|||||||
# "drivers_ready" for PCIe = apex bound and kernel modules present
|
# "drivers_ready" for PCIe = apex bound and kernel modules present
|
||||||
drivers_ready = (driver_name == 'apex') and apex_loaded
|
drivers_ready = (driver_name == 'apex') and apex_loaded
|
||||||
|
|
||||||
|
thermal = _coral_pcie_thermal(dev_path)
|
||||||
|
|
||||||
coral_devices.append({
|
coral_devices.append({
|
||||||
'type': 'pcie',
|
'type': 'pcie',
|
||||||
'name': _coral_pcie_name_from_lspci(slot),
|
'name': _coral_pcie_name_from_lspci(slot),
|
||||||
@@ -1666,6 +1793,11 @@ def get_coral_info():
|
|||||||
'device_nodes': device_nodes,
|
'device_nodes': device_nodes,
|
||||||
'edgetpu_runtime': runtime,
|
'edgetpu_runtime': runtime,
|
||||||
'drivers_ready': drivers_ready,
|
'drivers_ready': drivers_ready,
|
||||||
|
# PCIe/M.2 Coral thermal data from the apex driver (same source
|
||||||
|
# Frigate reads). All null when drivers aren't loaded.
|
||||||
|
'temperature': thermal.get('temperature'),
|
||||||
|
'temperature_trips': thermal.get('temperature_trips'),
|
||||||
|
'thermal_warnings': thermal.get('thermal_warnings'),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -1726,6 +1858,12 @@ def get_coral_info():
|
|||||||
'device_nodes': [], # No /dev/apex_* for USB
|
'device_nodes': [], # No /dev/apex_* for USB
|
||||||
'edgetpu_runtime': runtime,
|
'edgetpu_runtime': runtime,
|
||||||
'drivers_ready': drivers_ready,
|
'drivers_ready': drivers_ready,
|
||||||
|
# USB Coral does not expose temperature, trip points or hardware
|
||||||
|
# warnings via sysfs; those are only readable from within the Edge
|
||||||
|
# TPU runtime through libusb (Frigate and similar get them there).
|
||||||
|
'temperature': None,
|
||||||
|
'temperature_trips': None,
|
||||||
|
'thermal_warnings': None,
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -132,6 +132,14 @@ export interface CoralTPU {
|
|||||||
edgetpu_runtime?: string
|
edgetpu_runtime?: string
|
||||||
programmed?: boolean // USB only: runtime has interacted with the device
|
programmed?: boolean // USB only: runtime has interacted with the device
|
||||||
drivers_ready: boolean
|
drivers_ready: boolean
|
||||||
|
// Thermal data — PCIe/M.2 only (apex driver). Always null for USB Coral.
|
||||||
|
temperature?: number | null // °C current die temperature
|
||||||
|
temperature_trips?: number[] | null // trip_point0/1/2_temp, ordered warn→critical
|
||||||
|
thermal_warnings?: Array<{
|
||||||
|
name: string // e.g. "hw_temp_warn1"
|
||||||
|
threshold_c: number | null
|
||||||
|
enabled: boolean
|
||||||
|
}> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsbDevice {
|
export interface UsbDevice {
|
||||||
|
|||||||
Reference in New Issue
Block a user