The fan curve previously only controlled fan index 0; secondary fans
stayed on driver control. The curve can now target all fans (new
default) or any individual fan(s).
Backend:
- hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info()
returns per-fan speeds; set_fan_speed() accepts a fan index list
(None = all fans; all-fans mode is lenient toward driver-locked
fans, explicit lists are strict); reset_fan() restores all fans.
- server.py: per-GPU fan_targets state; the poller applies the curve to
all target fans and logs write failures (once per distinct error);
activation validates targets against the hardware (stale indices fall
back to all fans); POST /api/fans accepts fans, POST /api/fans/speed
accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets.
- Persistence format is now {"curve": ..., "fans": ...}; legacy
bare-curve entries migrate to "all fans" at startup.
- Profiles save/apply fan_targets alongside fan_curve.
- MonitoringSample carries per-fan speeds for live gauges.
Frontend:
- Fans tab: All / Fan 1 / Fan 2 / ... selector with live per-fan %;
the selection is applied together with the curve.
- Live monitor: per-fan gauges with sparklines for multi-fan GPUs.
82 lines
2.2 KiB
Python
82 lines
2.2 KiB
Python
"""Clean Python dataclasses for V/F curve data.
|
||
|
||
Raw struct manipulation stays in the call layer (bootstrap.py + hal/).
|
||
Everything above HAL works with these types.
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
|
||
|
||
@dataclass
|
||
class VFPoint:
|
||
index: int
|
||
freq_khz: int # Base frequency from VFP curve
|
||
volt_uv: int # Voltage from VFP curve
|
||
delta_khz: int # Offset from ClockBoostTable (signed)
|
||
domain: str = "gpu" # "gpu" or "memory"
|
||
|
||
@property
|
||
def effective_freq_khz(self) -> int:
|
||
return self.freq_khz + self.delta_khz
|
||
|
||
@property
|
||
def freq_mhz(self) -> float:
|
||
return self.freq_khz / 1000.0
|
||
|
||
@property
|
||
def effective_freq_mhz(self) -> float:
|
||
return self.effective_freq_khz / 1000.0
|
||
|
||
@property
|
||
def volt_mv(self) -> float:
|
||
return self.volt_uv / 1000.0
|
||
|
||
@property
|
||
def delta_mhz(self) -> float:
|
||
return self.delta_khz / 1000.0
|
||
|
||
|
||
@dataclass
|
||
class CurveState:
|
||
points: list[VFPoint]
|
||
timestamp: float
|
||
gpu_name: str
|
||
|
||
|
||
@dataclass
|
||
class MonitoringSample:
|
||
timestamp: float
|
||
voltage_uv: int | None
|
||
clock_mhz: float | None
|
||
temp_c: float | None
|
||
power_w: float | None
|
||
fan_pct: float | None
|
||
pstate: int | None # Performance state: 0 (P0, max) – 15 (P15, min)
|
||
mem_used_bytes: int | None # VRAM used (bytes)
|
||
mem_total_bytes: int | None # VRAM total (bytes)
|
||
gpu_util_pct: float | None # GPU core utilization (0–100)
|
||
mem_util_pct: float | None # Memory bus utilization (0–100)
|
||
mem_clock_mhz: float | None = None # Current memory clock (NVML_CLOCK_MEM)
|
||
throttle_reasons: int | None = None # Active clock-throttle bitmask (NVML)
|
||
pcie_link_width: int | None = None # Current PCIe link width (x1..x16)
|
||
pcie_link_generation: int | None = None # Current PCIe link generation (1..5)
|
||
mem_temp_c: float | None = None # VRAM temperature (if the GPU exposes it)
|
||
fans: list[float | None] | None = None # Per-fan speed % (index-aligned)
|
||
|
||
|
||
@dataclass
|
||
class GpuInfo:
|
||
name: str
|
||
index: int
|
||
uuid: str | None = None
|
||
pci_bus_id: int | None = None
|
||
|
||
|
||
@dataclass
|
||
class SnapshotInfo:
|
||
filepath: str
|
||
timestamp: str
|
||
gpu: str
|
||
nonzero_offsets: int
|
||
size: int
|