feat(monitor): CPU 卡加温度和功耗——走 MSI Afterburner 的共享内存(MAHMSharedMemory):Windows 上免驱动的唯一口子(psutil 不支持、ACPI 热区只有板温、Power Meter 无实例);实测 CPU 98-100 °C / 222-227 W,用 GPU1 power 对 nvidia-smi 校准过(125.29 vs 124.57);数据比 10 秒旧就当读不到、Afterburner 没开就自动隐藏这两行;28 核条悬停可看每核温度

This commit is contained in:
cbb
2026-10-18 17:31:05 +08:00
parent b1cf4126fd
commit faf6dd2748
2 changed files with 55 additions and 3 deletions
+6 -2
View File
@@ -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) => `<div title="核 ${i + 1}: ${v}%" style="flex:1;min-width:0;height:100%;border-radius:2px;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.18 + 0.82 * Math.min(1, v / 100)}"></div>`).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) => `<div title="核 ${i + 1}: ${v}%${ct[i] != null ? ' · ' + ct[i] + ' °C' : ''}" style="flex:1;min-width:0;height:100%;border-radius:2px;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.18 + 0.82 * Math.min(1, v / 100)}"></div>`).join('');
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
<circle cx="58" cy="58" r="50" stroke="${col}" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="${C}" stroke-dashoffset="${C * (1 - tot / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b>${tot}%</b><span>CPU</span></div></div></div>
<div class="gpurows">${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 画)') : ''}
<div class="cores">${bars}</div></div></div>
<div class="gname">${CP.model || 'CPU'} · 每核占用(深色=忙)</div>`;
<div class="gname">${CP.model || 'CPU'}${CP.temp == null ? ' · 开 MSI Afterburner 才能读 CPU 温度/功耗' : ''} · 每核占用(深色=忙)</div>`;
})() : '';
$('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil');
+49 -1
View File
@@ -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('<f', e[1300:1304])[0]
if nm == 'CPU temperature' and 0 < val < 130: out['temp'] = round(val)
elif nm == 'CPU power' and 0 < val < 1000: out['power'] = round(val, 1)
elif nm.startswith('CPU') and nm.endswith(' temperature'):
try:
j = int(nm[3:-12])
if 0 < val < 130: core[j] = round(val)
except ValueError: pass
if core: out['coreT'] = [core[k] for k in sorted(core)]
except Exception:
out = {}
_MAHM['v'] = out
return out
_GPU_CACHE = {'t': 0.0, 'v': []}