# Уровни лимита заряда — переоткрытие «произвольного %» Восстановлено: **живой мониторинг MIFS + реестра** во время переключения лимита заряда в `Xiaomi PC Manager 5.8.0.74` + перечисление значений на TM2424, **2026-07-29**. Отменяет прежний вывод «порог фиксирован ≈80%, произвольный % недоступен» (был закрыт в [05](05-open-questions.md), CLAUDE.md, памяти). > **TL;DR.** Команда защиты заряда `MIFS 0x10/0x02`, которую мы считали бинарной (80/100), на самом > деле **многоуровневая**. Прошивка driver-free держит пороги **40 / 50 / 60 / 70 / 80 / 100 %** и > **сама валидирует** набор. Это наш существующий канал — фича делается без единого нового драйвера. --- ## Как нашли OEM (`Xiaomi PC Manager`) даёт селектор лимита заряда (40/50/60/70/80 %). Запустили driver-free монитор (GET-опрос всех поддерживаемых MIFS-функций + снимок реестра `HKLM/HKCU\SOFTWARE\MI` каждые 2 с, лог только дельт), пользователь пощёлкал позиции. Менялся ровно один байт — **`0x10/0x02`** — и зеркально ключ реестра **`SvrCModule\PerformanceMode\POWER\ChargingThreshold`**. Порядок 40→50→60→70→80 дал коды `8→7→6→5→1`. ## Маппинг (подтверждён перечислением SET 0…12 + readback) Прошивка **валидирует**: валидные коды принимает (SET-статус `0x80`, значение держится), невалидные отвергает (`0x00`) и зажимает к ближайшему (`2,3`→`1`; `9…`→`8`). | код `0x10/02` | 0 | 1 | 4 | 5 | 6 | 7 | 8 | |---------------|---|---|---|---|---|---|---| | порог, % | **100** | **80** (legacy) | **80** (granular) | **70** | **60** | **50** | **40** | - granular-шкала (коды 4–8): **% = 120 − код·10**. - **90 % нет** — код `3` отвергается. - `0` и `1` — legacy-коды (наш прежний «100/беречь-80»); `4` — granular-дубль 80 %. - OEM для 80 % пишет legacy `1`; для 40–70 % — granular `8…5`. ## Что подтверждено, а что нет - ✅ Прошивка **принимает** уровни (SET `0x80`, `readback` = записанному) и **валидирует** набор — значит это настоящие уровни, не «сохранённый мусор». - ✅ Механизм отсечки для `0x02` доказан ещё в v0.1 (переключением на 80 %); новые значения — та же команда, поэтому отсечка на 40–70 % практически наверняка работает. - ✅ **EC физически режет заряд на granular-уровнях — подтверждено на TM2424 2026-07-29.** Проверка током, а не readback'ом: при заряде **66 %** и подключённом адаптере ставим лимит **70 %** — ток в «Мониторе» пошёл (заряд идёт, 66 < 70); ставим **60/50/40 %** — ток падает **в ноль** (66 выше порога, EC держит). Прежняя оговорка «readback ≠ поведение» снята: команда управляет железом. - Реальный % наружу отдаёт только код в `0x10/02`; `0x10/01` намертво `100`, `0x10/00/03/04/05` — нули. → процент для UI считаем по таблице выше. - ❌ **Группы `0x10` есть не у всех моделей.** На TM2113 (Redmi Book Pro 15 Ryzen) команда отвергается — подтверждено пробами владельца машины в [issue #51](https://github.com/Oksion/XiControl/issues/51). Отсюда XIC-74: приложение запоминает отказ (`ChargeLimitUnsupported` в config.json) и предлагает там **программный порог** — предупреждение «пора выдернуть», а не лимит. Признак выучивается по ответу прошивки на явное действие пользователя; сверка `ChargeGuard` в это обучение не ходит специально, иначе разовый сбой на просыпающемся EC пометил бы рабочую машину как «не умеет». **Признак отсутствия лимита — «команды нет», а не отказ записи** (XIC-100). Отказ уровня (SET-статус `0x00`) значит лишь, что модель не держит этот процент, а исключение WMI — разовый сбой. Поэтому после отказа прошивку спрашивают чтением `GET 0x10/0x02`: `0x80` — группа есть, `0xE0` или диалект без группы — нет, всё прочее — вывода не делаем. Ложно выученный признак снимается на старте тем же чтением (ставится — только по действию пользователя). ## Как это выглядит снаружи (наблюдения 2026-07-29) - **Windows про наш порог не знает.** Индикатор батареи в системном трее строится по ACPI-статусу от EC (питание от сети / идёт ли заряд), а не по значению `0x10/02`. При активном лимите и подключённом адаптере Windows не показывает «заряжается» — с её точки зрения заряд просто не идёт, и это честно. Чем именно рапортует EC в этот момент (флаги `PowerOnLine`/`Charging`/`Rate` из `IOCTL_BATTERY_QUERY_STATUS`) — не снимали, если понадобится точная картина, это отдельная проба. - **С OEM-софтом состояние на лету не синхронизируется.** В MIFS нет канала уведомления об изменении порога: и Xiaomi PC Manager, и мы читаем `0x10/02` в свои моменты (при открытии окна/меню), поэтому две открытые рядом панели показывают каждая своё, пока их не переоткрыть. Единственный источник истины — сама прошивка (`GET 0x10/02`), а не то, что нарисовано в любой из программ. ## Дизайн фичи для XiControl (driver-free, в характере проекта) Заряд — флагманская фича; сейчас бинарно «беречь 80 / 100». Расширяем до выбора порога: - **Панель — бинарный тумблер «X % / 100 %»** (как сейчас), где `X` — выбранный порог. Быстрое переключение, не перегружаем панель. - **Выбор `X` — в Настройках → Батарея** (пресеты 40/50/60/70/80 %). Это «медленная», редкая настройка. - **Показывать реальный процент** везде (OSD, тултип, меню, «беречь X %») — не хардкодить 80. - Хранение: `X` в `AppConfig`; запись `SET 0x10/0x02 val=<код по таблице>`; `100 %` = `0`. `ChargeGuard` переустанавливает выбранный код (а не жёстко `1`) после сна/смены питания. - Совместимость: 80 % писать legacy `1` (как OEM), остальные — granular. На моделях, где granular не поддержан (валидация вернёт `0x00`/зажатие) — деградируем к бинарному 80/100 (рантайм-проба набора). > Порог **зашит по модели**: на TM2424 это 40–100 % шагом кроме 90. На других моделях набор может > отличаться — определять в рантайме, не хардкодить. --- ## Как реализовано (XIC-4) - `Mifs.ChargeCarePresets` + `ChargeCodeForPercent`/`ChargePercentForCode` — таблица уровней. - Шов `IMifsClient`: `GetChargeLimit()` / `SetChargeLimit(percent)` (вместо прежних bool-методов). `SetChargeLimit` пишет код с ре-армом off→on и **возвращает, приняла ли прошивка**. - `AppConfig.CareLimitPercent` (дефолт 80 → старые `config.json` мигрируются сами) + `CarePercent()` с фолбэком, если значение правили руками мимо набора. - `AppController.SetCareLimit` — применяет на железе, только когда защита активна и не перебита «В дорогу»; иначе просто запоминает. `ChargeGuard` армит **выбранный** порог. - UI: панель/меню/OSD показывают реальный %, пикер — Настройки → Батарея. ## Примирение с внешней сменой (XIC-17) Канала уведомления в MIFS нет, поэтому «на лету» две программы не синхронизируются (см. выше). Но подхватить чужое изменение **в момент показа UI** можно — и нужно, иначе внешняя смена до нас не доходит вовсе. `AppController.SyncCareFromFirmware()` читает `GET 0x10/02` и приводит конфиг к прошивке; зовётся из `QuickPanelForm.RefreshState()` (до `Show()`, рядом с чтением режима и тачпада) и из `TrayMenuBuilder` при пересборке меню. Фонового опроса не появилось — только когда UI открывают. Тонкости, без которых это ломается: - **Прошивка молчит** (`null`) → конфиг не трогаем: лучше показать своё значение, чем затереть нулём. - **Принимаем только валидный порог < 100** (`ChargeCodeForPercent != null`): другой уровень EC сам не породит — это всегда чей-то осознанный SET, его принимать безопасно. - **«100 %» НЕ принимаем — это защита от гонки с ChargeGuard.** EC читается как 100 и когда защиту выключили снаружи, и — транзиентно — после сна/смены питания: лимит на этих событиях теряется, а гард переармирует его с дебаунсом 1.5 с. Отличить эти случаи нельзя. Приняв 100, мы сбросили бы `ChargeCare` в конфиге — и гард, который читает желаемый порог из конфига, разоружился бы навсегда: панель, открытая сразу после resume или выдёргивания зарядника, молча убивала бы защиту. Внешнее «выключить» гард всё равно перебивает на следующем событии питания (документированное поведение), поэтому UI показывает намерение пользователя, а не мгновенное состояние EC. - **При активном «В дорогу» примирение не выполняется**: там прошивка намеренно держит 100 % при `ChargeCare = true`, и примирение сбросило бы флаг, сломав режим. - **`CareChanged` не дёргаем.** Это не действие пользователя, а подхват чужого; при скрытой панели `TrayApp` показывает на это событие OSD — всплывашка вылетала бы на каждое открытие панели. - Конфиг пишем только при фактическом отличии, факт принятия — в лог. Заодно исправлена та же ошибка в меню трея: галочка считалась от живого значения, а процент в подписи брался из конфига, поэтому меню могло показать «беречь 60 %» с галочкой при 50 % в железе. Теперь и панель, и меню рисуют из конфига — он уже приведён к прошивке. Тултип трея сознательно оставлен на конфиге: он живёт на 30-секундном опросе, и правдивость потребовала бы фонового WMI-вызова. **Деградация на моделях без granular** сделана рантаймом, а не гейтом по SKU (в духе «набор функций определяем в рантайме»): прошивка сама валидирует уровень, отказ (`OUT[1] != 0x80`) → `SetChargeLimit` возвращает `false` → выбор откатывается, пользователь получает честный error-OSD, а 80/100 (legacy-коды `1`/`0`) работают всегда. Известное ограничение: пикер показывает все пресеты и на такой модели узнать о неподдержке можно только попыткой — read-only запроса «какие уровни держит прошивка» в MIFS нет. ## Чтение порога после холодного старта врёт — и это НЕ потеря лимита (замер 2026-09-21, XIC-64) Формулировка выше («лимит на этих событиях теряется») оказалась неточной. Замер на TM2424 развёл два разных явления: **отражение порога в регистре** и **сам порог в железе**. Условия: порог 60 %, заряд 72 %, приложение со свежим `ChargeGuard`. Машина выключена (Windows 11, быстрый запуск — то есть гибернация ядра), подключён адаптер, ~50 минут в покое. | Момент | EC (`GET 0x10/02`) | Заряд | Ток | | --- | --- | --- | --- | | 21:55, до выключения | код 6 → 60 % | 72 % | разряд | | 22:47, после включения | код 0 → **100 %** | **72 %** | 0 | В журнале при старте: `ChargeGuard (старт): 100% → 60%`. **Ключ в том, что заряд не вырос ни на процент.** Если бы EC действительно забыл порог, батарея за пятьдесят минут на адаптере набрала бы полтора десятка процентов. Значит железо лимит держало, а обнулилось только то значение, которое мы читаем: `GET 0x10/02` отражает не состояние контроллера, а то, что записала ОС **в текущей сессии**. Рядом — независимое подтверждение того же механизма: в том же запуске `GET 0x08` вернул `0x09` (Auto) вместо записанного накануне `0x02` (Quiet). Режим производительности «сбрасывается» после перезагрузки ровно так же. Что из этого следует: - **Не строить на этом чтении вывод «защита слетела».** После старта (и, вероятно, после любого перехода питания) оно скажет 100 % там, где железо держит 60 %. Логика примирения UI это уже учитывает — «100 % не принимаем» (см. выше), и теперь понятно, что причина глубже гонки с гардом. - **Переармливание после старта безвредно и полезно**: лишняя запись возвращает регистр к правде, а UI и API перестают показывать «лимита нет». - **Порог `hurry` в `ChargeGuard.Reapply`** (не сбрасывать в «выкл» перед сном) опирается на это же чтение: если оно показывает нужный порог, значит его записали в этой сессии — трогать нечего. Открытым остаётся, теряется ли регистр только при холодном старте или ещё и при обычном сне: для этого нужен отдельный цикл сна со снимками до и после.