From faf6dd274853cf61a35e6b617f56ed53be0ac724 Mon Sep 17 00:00:00 2001
From: cbb <2625274797@qq.com>
Date: Sun, 18 Oct 2026 17:31:05 +0800
Subject: [PATCH] =?UTF-8?q?feat(monitor):=20CPU=20=E5=8D=A1=E5=8A=A0?=
=?UTF-8?q?=E6=B8=A9=E5=BA=A6=E5=92=8C=E5=8A=9F=E8=80=97=E2=80=94=E2=80=94?=
=?UTF-8?q?=E8=B5=B0=20MSI=20Afterburner=20=E7=9A=84=E5=85=B1=E4=BA=AB?=
=?UTF-8?q?=E5=86=85=E5=AD=98=EF=BC=88MAHMSharedMemory=EF=BC=89=EF=BC=9AWi?=
=?UTF-8?q?ndows=20=E4=B8=8A=E5=85=8D=E9=A9=B1=E5=8A=A8=E7=9A=84=E5=94=AF?=
=?UTF-8?q?=E4=B8=80=E5=8F=A3=E5=AD=90=EF=BC=88psutil=20=E4=B8=8D=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E3=80=81ACPI=20=E7=83=AD=E5=8C=BA=E5=8F=AA=E6=9C=89?=
=?UTF-8?q?=E6=9D=BF=E6=B8=A9=E3=80=81Power=20Meter=20=E6=97=A0=E5=AE=9E?=
=?UTF-8?q?=E4=BE=8B=EF=BC=89=EF=BC=9B=E5=AE=9E=E6=B5=8B=20CPU=2098-100=20?=
=?UTF-8?q?=C2=B0C=20/=20222-227=20W=EF=BC=8C=E7=94=A8=20GPU1=20power=20?=
=?UTF-8?q?=E5=AF=B9=20nvidia-smi=20=E6=A0=A1=E5=87=86=E8=BF=87=EF=BC=8812?=
=?UTF-8?q?5.29=20vs=20124.57=EF=BC=89=EF=BC=9B=E6=95=B0=E6=8D=AE=E6=AF=94?=
=?UTF-8?q?=2010=20=E7=A7=92=E6=97=A7=E5=B0=B1=E5=BD=93=E8=AF=BB=E4=B8=8D?=
=?UTF-8?q?=E5=88=B0=E3=80=81Afterburner=20=E6=B2=A1=E5=BC=80=E5=B0=B1?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=9A=90=E8=97=8F=E8=BF=99=E4=B8=A4=E8=A1=8C?=
=?UTF-8?q?=EF=BC=9B28=20=E6=A0=B8=E6=9D=A1=E6=82=AC=E5=81=9C=E5=8F=AF?=
=?UTF-8?q?=E7=9C=8B=E6=AF=8F=E6=A0=B8=E6=B8=A9=E5=BA=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ml/monitor.html | 8 ++++++--
ml/monitor.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 55 insertions(+), 3 deletions(-)
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 `
${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('