# Мониторинг питания (виджет «Монитор») Как «Монитор» читает мощность и почему «медленную зарядку» нельзя определить надёжно. Разобрано на Xiaomi Book Pro 14 (TM2424), пробы — `reference/probe-battery-flow.ps1` (в репозиторий не входит). ## Откуда берём ватты Через штатный **Battery API** — SetupAPI перечисляет устройства `GUID_DEVICE_BATTERY`, затем `IOCTL_BATTERY_QUERY_STATUS` (`PowerDraw`). IOCTL заставляет драйвер опросить ACPI `_BST` **в момент вызова**, поэтому значение всегда живое. `BATTERY_STATUS.Rate` — знаковый (мВт): `>0` заряд, `<0` разряд, `0`/`BATTERY_UNKNOWN_RATE` → NaN (от сети без обмена — «—»). Чистый Win32, driver-free. **Почему не WMI:** класс `root\wmi\BatteryStatus` (`ChargeRate`/`DischargeRate`) отдаёт **кэш** `_BST`, который Windows обновляет лениво — только когда батарею активно опрашивает кто-то ещё (например открытый HWiNFO). Без такого «будильника» показания в «Мониторе» **застывали**. Поэтому основной путь — IOCTL; `BatteryStatus` остался мягким фолбэком, когда батарейного устройства нет (десктоп). Значение хранится **со знаком** (для выбора цвета), но в тексте и высоте графика берётся модуль. ## Особенность датчика: ток беззнаковый, флаг заряда — от драйвера Ключевой факт про это железо: **датчик тока на батарее беззнаковый** — он измеряет только *величину* тока, а не направление. Признак «заряд/разряд» (и, соответственно, знак `Rate` в IOCTL) выставляет **драйвер по факту подключения адаптера**, а не по реальному знаку тока — поэтому переход на IOCTL живость починил, но саму «беззнаковость» не убирает. Модель: датчик стоит **«в разрыв» у аккумулятора** — в узле, где сходятся адаптер и батарея (как «смеситель»), — и показывает **модуль тока через батарею**, а не потребление от адаптера: - **мощная зарядка** (низкая нагрузка) → через батарею большой ток → показание высокое; - **мощный разряд** (от батареи под нагрузкой) → тоже большой ток → тоже высокое; - **мощная зарядка + высокое потребление ноутом** → ток уходит в основном на сам ноут, через батарею его мало → показание низкое. Знак при этом теряется, поэтому разряд под нагрузкой на слабом блоке датчик отдаёт как «заряд» на величину тока. Замер на TM2424 (`ChargeRate`, флаг всё время `Charging=true`, `DischargeRate=0`): | Состояние | ChargeRate | |----------------------|-----------| | простой | ~5–8 Вт | | бенч (пик CPU) | ~50 Вт | | спад после бенча | 15 → 4 Вт | В бенче ~50 Вт — это ток **разряда** батареи (слабый блок не тянет нагрузку), но без знака он выглядит как «заряд»; батарея не «заряжается то 50, то 4 Вт» в зависимости от CPU. ## Пример со слабым блоком Слабый адаптер (~20 Вт). В простое датчик покажет 5–10 Вт (немного тока в батарею). Запускаем бенч, потребление ноута растёт до ~40 Вт: реально это ~10 Вт от адаптера + **~30 Вт стекает с батареи**, но датчик без знака отдаёт эти 30 Вт как «заряд». На графике — зелёный «заряд» под нагрузкой, хотя по факту разряд. Это ограничение датчика, а не ошибка расчёта. ## Слабый / не-PD зарядник — через ADPW, а не ChargeRate Определять «слабый блок» по `ChargeRate`/IOCTL-`Rate` **нельзя** (и мы не пытаемся): это потребление, а не способность блока — в простое низкое у любого адаптера, под нагрузкой на слабом блоке знак врёт (см. выше). Порог по ваттам на этом сигнале дал бы ложные «медленно». Firmware-событие про зарядник на TM2424 не приходит (`0x01 value=2` из карты MIControl нет), а Windows UCSI «charging slowly» — только драйверный, user-mode API нет. **Но есть прямой сигнал — ADPW** (MIFS `cmd 0x10`, `arg 0x06`): прошивка отдаёт **номинал подключённого PD-адаптера в ваттах** (30 / 45 / 100…), `0` — не-PD/не сообщил. Проверено на TM2424: подключение 30/45/100 Вт PD даёт ровно эти значения, обычный USB — `0`. Это не потребление, а рейтинг блока — то, что и нужно. Driver-free (только чтение), на нём построены: - **Показ мощности** адаптера — строкой в зарядном OSD и подписью в «Мониторе» (под строкой Power). - **Бейдж-оверлей** поверх иконки заряда (сама база несёт лимит 80/100): `ADPW < порога` → красный «!» (медленно), `ADPW = 0` → серый «?» (PD не согласован, напр. обычный 5 В), быстрый → без бейджа. Порог настраивается (Настройки → Батарея), показ включается той же опцией, что и ватты. Рядом отсюда же **SOH1** (`arg 0x01`) — «здоровье» батареи в %; в паре со штатными WMI-классами (`BatteryStaticData`/`BatteryFullChargedCapacity`/`BatteryCycleCount`) даёт панель износа/циклов в настройках. ## График питания Красим график питания **по направлению**: заряд (ток в батарею) — **зелёный**, разряд — **оранжевый**, цвет по каждому тику; значение всегда положительное (`+45 Вт` зелёным / `12 Вт` оранжевым), направление читается цветом. Это работает на всех обычных сценариях (батарея, нормальная/быстрая зарядка). ## Температура Строка-график «горячей точки» в «Мониторе» — driver-free через **Intel DPTF** (WMI `root\wmi\EsifDeviceInformation`, `.Temperature` в °C, максимум активных доменов). Тема раньше числилась закрытой как «невозможно», переоткрыта на этом сигнале — см. ROADMAP и `05-open-questions.md`. ### Не-Intel машины: фолбэк на ACPI-термозону (XIC-41) Провайдер DPTF приезжает с драйверами Intel, и на AMD-моделях его нет вовсе — до XIC-41 строка температуры там просто отсутствовала. Фолбэк — штатная ACPI-термозона (`rootwmiMSAcpi_ThermalZoneTemperature`, `.CurrentTemperature` в десятых Кельвина: `v / 10 − 273,15`), максимум по зонам, санитарный диапазон тот же, что у DPTF (0 < t < 130 °C). Источник выбирается один раз по первому удавшемуся чтению; отсутствующий класс повторно не дёргается. Если DPTF на модели **есть**, но сейчас молчит, на зону не переключаемся — это его временная пустота, а не другое железо. **Это другая величина, и в UI она так и называется.** ACPI-зона — одно грубое число платы, а не максимум по доменам CPU, и её смысл зависит от вендора. Измерено в один момент: | Машина | DPTF (максимум доменов) | ACPI-зона | |---|---|---| | Book Pro 14 (TM2424, Intel) | **73 °C** (15 доменов: 73/53/50/50/48/37/…) | **27,9 °C** — заглушка | | Redmi Book Pro 15 2022 (TM2113, AMD) | нет провайдера | **49,1 °C** — правдоподобно | Поэтому подмена не молчаливая: подпись строки в «Мониторе» и название в тултипе значка меняются на «Температура платы» (`monitor.temp.zone` / `traymetric.temp.zone`) — тот же приём, что с ваттами пакета CPU в разделе про слабый зарядник. Прятать «неизменное» значение мы намеренно **не** стали: отличить заглушку от честной стабильной температуры простаивающего ноутбука по одной лишь неизменности нельзя — зону TM2424 разоблачила сверка с DPTF, которой там, где работает фолбэк, нет. Отладка: `"ForceAcpiTemperature": true` в `config.json` заставляет читать зону даже при живом DPTF — иначе путь не прогнать на Intel-машине. Выбор источника пишется в `log.txt` строкой «Температура: источник — …», по одной на потребителя (индикатор трея и «Монитор» — разные экземпляры). ### WMI-источники «Монитора» — на пуле (XIC-81) Температура (DPTF / ACPI-зона), ватты адаптера (MIFS `ADPW`) и запасной `BatteryStatus` — WMI, а WMI-провайдер умеет зависать (например, после сна). В UI-потоке это вешало бы весь интерфейс и снимало бы глобальный хук мыши панели, поэтому «Монитор» читает их через `LatestValue`: на каждом тике запускается чтение на пуле (не больше одного одновременно), а рисуется последнее готовое значение — на тик старее. В норме тик стоит мало (DPTF ~18 мс, MIFS ~10 мс на TM2424); если провайдер завис, новых чтений не плодим — значение просто стареет, и старше 5 с показывается прочерком, а не выдаётся за текущее. Первое чтение температуры при открытии виджета ждём ограниченно (от него зависит высота окна); не успело — окно перестроится, когда ответ придёт. ## GPU: загрузка, мощность, частота (Intel IGCL) Ряд GPU берётся из **Intel IGCL** — user-mode API графического драйвера. `ControlLib.dll` лежит в `System32` и приезжает со штатным драйвером Intel: **мы ничего не устанавливаем, и права администратора для чтения не нужны** — тот же driver-free принцип, что и WMI. Реализация — `src/SystemIntegration/GpuTelemetry.cs`, инициализация ленивая (пока виджет не открыли, IGCL в процесс не подгружается). Всё берётся одним вызовом `ctlPowerTelemetryGet`. Раскладка `ctl_power_telemetry_t` (проверено на TM2424, `ControlLib 1.2.291.0`, адаптер `0x8086:0xB080`): структура 512 байт, внутри цепочка элементов `ctl_oc_telemetry_item_t` = `uint bSupported; uint units; uint type; pad; double value`. | смещение | поле | единицы | есть? | |---|---|---|---| | 8 | `timeStamp` | секунды | да | | 32 | `gpuEnergyCounter` | джоули | да | | 56 | `gpuVoltage` | — | **нет** | | 80 | `gpuCurrentClockFrequency` | МГц | да | | 104 | `gpuCurrentTemperature` | — | **нет** | | 128 | `globalActivityCounter` | секунды активности | да | | 152 | `renderComputeActivityCounter` | секунды активности | да | | 176 | `mediaActivityCounter` | секунды активности | да | Счётчики **накопительные**, поэтому загрузка и ватты — приращения: `загрузка % = Δactivity / Δtime · 100`, `Вт = Δjoules / Δtime`. Первая выборка после открытия виджета только набирает базу и даёт `NaN` — график начинается со второй точки. При повторном открытии зовём `Reset()`, иначе первое значение размажется по времени, что виджет был закрыт. Тонкости и границы: - `ctl_version_info_t` — это **простой `uint32`** (`major << 16 | minor`), а не структура из двух `uint16`. `ctl_init_args_t` = 36 байт, `AppVersion = 1.0`; реализация рапортует 1.1. - На старте сверяем `bSupported`/`units`/`type` по всем четырём используемым смещениям. Не совпало — выключаемся навсегда: лучше не показать ряд, чем показать мусор. - **Температуры и оборотов вентиляторов IGCL на встроенном GPU не даёт**: все `ctlEnum*` (`TemperatureSensors`, `Fans`, `PowerDomains`, `EngineGroups`, `FrequencyDomains`) отвечают отказом — это пути для дискретных Arc. Температуры мы и так берём через DPTF (выше). - `renderCompute` почти дублирует `global` (разница ~1.3 п.п. под нагрузкой), `media` под 3D-нагрузкой всегда 0 — в UI выведен только `global`. - AMD-модели: IGCL там нет (`ctlInit` не поднимется, либо `ControlLib.dll` вообще отсутствует и P/Invoke бросит `DllNotFoundException`) — ряд просто не появится. Аналог у AMD — `atiadlxx.dll`, отдельная история. ### Проверено на железе (TM2424, 2026-07-29) Прогон бенчмарка, 829 выборок / 216 с: загрузка 0.9 → 99.7 %, мощность 0.01 → **24 Вт**, частота 900 → **2500 МГц** (под длительной нагрузкой оседает на 2390–2440 — троттлинг), корреляция загрузки и мощности **r = 0.988**. Сверка загрузки с собственным счётчиком Windows `\GPU Engine(*)\Utilization Percentage`: IGCL ~42.5 % против Windows ~45.4 % — сходится. Цена вызова: **медиана 47 мкс**, p95 77 мкс (кадр при 60 Гц — 16666 мкс), поэтому опрос живёт в том же UI-таймере 1 Гц, без воркера. `ctlInit` + enumerate — 8.9 мс однократно при первом показе; приватная память не растёт (`ControlLib.dll` 296 КБ и `IntelControlLib.dll` 712 КБ мапятся как образы). ## NPU: загрузка (XIC-86) Ряд NPU — то же число, что в диспетчере задач. Windows (WDDM/MCDM) отдаёт работу нейропроцессора в тех же perf-счётчиках, что и GPU: `\GPU Engine(*)\Utilization Percentage`, экземпляр на пару «процесс × движок», у движков NPU тип `engtype_Neural`. Читаем через PDH — обычный user-mode API, ни драйвера, ни прав администратора. Реализация — `src/SystemIntegration/NpuLoad.cs`. - **Как сводим** (`NpuLoad.Aggregate`, под тестами): внутри движка — сумма по процессам, между движками — максимум. Простая сумма всех экземпляров завышала бы загрузку NPU с несколькими движками (две половины работы на двух движках — это не 100%). - **Есть ли NPU** — по классу устройств *Compute accelerators* (`ComputeAccelerator`, `{f01a9d53-…}` в `HKLM\SYSTEM\CurrentControlSet\Control\Class`): туда Windows кладёт Intel AI Boost, Qualcomm Hexagon, AMD XDNA. Нет — нет ни ряда в «Мониторе», ни пункта NPU в выборе метрики индикатора трея: сами счётчики «GPU Engine» есть и без нейропроцессора, и ноль читался бы как «NPU простаивает». - **Цена:** открытие запроса ~215 мс на TM2424 (PDH разворачивает ~800 экземпляров «GPU Engine»), дальше ~2,6 мс на чтение. Поэтому «Монитор» читает его на пуле через `LatestValue` (как WMI выше) и держит запрос открытым между показами. Rate-счётчику нужны две точки — первое значение после открытия виджета приходит со второго тика.