diff --git a/ml/monitor.html b/ml/monitor.html index 99fd25a..4bc8a65 100644 --- a/ml/monitor.html +++ b/ml/monitor.html @@ -557,13 +557,17 @@ function renderStatus(s) { }).join(hr) : ''; const cpuBlk = CP.n ? (() => { const tot = CP.total ?? 0, C = 2 * Math.PI * 50, col = tot > 85 ? css('--bad') : tot > 60 ? css('--warn') : css('--ok'); - const bars = (CP.per || []).map((v, i) => `
`).join(''); + const ct = CP.coreT || [], ctMax = ct.length ? Math.max(...ct) : null; + const tcol = v => v > 98 ? css('--bad') : v > 90 ? css('--warn') : css('--c6'); + const bars = (CP.per || []).map((v, i) => `
`).join(''); return `
${tot}%CPU
${gr('占用', `${tot}% · ${CP.n} 核`, tot, col)} + ${CP.temp != null ? gr('温度', `${CP.temp} °C${ctMax != null && ctMax !== CP.temp ? `(最高核 ${ctMax})` : ''}`, CP.temp, tcol(CP.temp), 'MSI Afterburner 的共享内存;鼠标停在核条上还能看每核温度') : ''} + ${CP.power != null ? gr('功耗', `${Math.round(CP.power)} W`, 100 * CP.power / 253, css('--c7'), 'CPU 封装功耗(满标度按 14700K 的 PL2 253 W 画)') : ''}
${bars}
-
${CP.model || 'CPU'} · 每核占用(深色=忙)
`; +
${CP.model || 'CPU'}${CP.temp == null ? ' · 开 MSI Afterburner 才能读 CPU 温度/功耗' : ''} · 每核占用(深色=忙)
`; })() : ''; $('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil'); diff --git a/ml/monitor.py b/ml/monitor.py index 0097ab8..0388f5b 100644 --- a/ml/monitor.py +++ b/ml/monitor.py @@ -106,7 +106,55 @@ class CpuSampler(threading.Thread): def cpu(): - return _CPU['v'] + return {**_CPU['v'], **mahm()} + + +_MAHM = {'t': 0.0, 'v': {}} + + +def mahm(): + """借 MSI Afterburner 的共享内存(MAHMSharedMemory)读 CPU 温度 / 封装功耗——Windows 上没有别的免驱动口子: + psutil 在 Windows 没有 sensors_temperatures;ACPI 热区只有个 27.9 °C 的板温;Power Meter 计数器没有实例。 + 条目布局实测:头 32 字节(签名 MAHM / 版本 / 头大小 / 条目数 / 条目大小 / 时间戳),每条 1324 字节, + 名字在前 260 字节、单位在其后 260、数值是偏移 1300 的 float(用 GPU1 power 对 nvidia-smi 校准过)。 + 读数比 10 秒旧就当没读到:Afterburner 关了或停了就别显示一个冻住的数。""" + now = time.time() + if now - _MAHM['t'] < 1.0: return _MAHM['v'] + _MAHM['t'] = now + out = {} + try: + import ctypes, struct + k = ctypes.windll.kernel32 + k.OpenFileMappingW.restype = ctypes.c_void_p + k.OpenFileMappingW.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_wchar_p] + k.MapViewOfFile.restype = ctypes.c_void_p + k.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t] + h = k.OpenFileMappingW(0x0004, 0, 'MAHMSharedMemory') + p = k.MapViewOfFile(h, 0x0004, 0, 0, 0) if h else 0 + if p: + hd = ctypes.string_at(p, 32) + num, esize, hsize = (int.from_bytes(hd[i * 4:i * 4 + 4], 'little') for i in (3, 4, 2)) + upd = int.from_bytes(hd[20:24], 'little') + if 0 < num < 5000 and 32 < esize < 65536 and now - upd <= 10: + buf = ctypes.string_at(p, hsize + num * esize) + core = {} + for i in range(num): + e = buf[hsize + i * esize: hsize + (i + 1) * esize] + nm = e[:260].split(b'\x00')[0].decode('latin1') + if not nm: continue + val = struct.unpack('