diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0db1c67..516f6ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@
## [Unreleased]
+### Добавлено
+- **Прошивки на вкладке «Драйверы» — с текущей версией.** Раньше строка говорила только «в каталоге
+ есть прошивка X». Теперь XiControl читает установленную версию сам, без драйверов. Тачпад
+ сравнивается как драйвер: «обновление», «актуальна» или «новее каталога». Прошивок у него две —
+ контроллер касаний и сила нажатия с вибрацией, сверяются обе (на TM2424 стоят 3.5.9.1 и 44.25.11.0
+ при 3.5.13.1 и 44.25.13.0 в каталоге). Запросы версий подсказал gxcreator в issue #99 и #100. У экрана обе версии показываются рядом, но без вывода: номер прошивки
+ панели зависит от её варианта, и по нему не сказать, новее ли каталожная. Версия экрана читается
+ только по кнопке проверки, в локальном сеансе при включённом экране; по удалённому рабочему столу
+ она неизвестна, и это нормально (XIC-104).
+
### Исправлено
- **Проверка драйверов отбрасывает пакеты чужой модели.** Сервер Xiaomi называет в ответе модель,
для которой пакет. Если она не совпала с моделью ноутбука, пакет не показывается. Для прошивок
diff --git a/CLAUDE.md b/CLAUDE.md
index b3826ce..20b545b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -252,7 +252,12 @@ WMI-событий). `Program.cs`: single-instance mutex → DI-контейне
`OemDrivers` (XIC-95: `OemProtocol` — тело/подпись/разбор ответа сервера Xiaomi, `DriverMatch` —
сверка строго попарно по hardware-id — кроме прошивок `FWTP`/`FWSC`: их id в каталоге синтетические,
строка «в каталоге есть прошивка X» даётся по факту пакета для модели, одна на модуль (XIC-102); модули
-ответа с чужой `product` отбрасываются ещё при разборе — для прошивок это единственная сверка (XIC-103); оба
+ответа с чужой `product` отбрасываются ещё при разборе — для прошивок это единственная сверка (XIC-103);
+ текущая версия прошивки (XIC-104): тачпад — две прошивки, контроллер касаний BL7853
+ `TouchpadHaptics.ReadControllerVersion` (COL04, единственный константный кадр OEM — больше в COL04 ничего не шлём)
+ и MCU силы `ReadFirmwareVersion` (COL05, команда `0x10`), сверка по обеим частям каталожной; экран — `PanelFirmware` (ROM Ver TCON Himax через IGCL `ctlAUXAccess`,
+ записи в контроллер байт в байт как у OEM, только по кнопке; по RDP панели нет — «неизвестна»), только рядом,
+ без вывода; оба
чистые и под тестами; `OemDriverCheck` — WMI + сеть, только по
кнопке, без `sn`, ничего не скачивает и не ставит; протокол — `docs/16-oem-driver-check.md`),
`OemService` (XIC-87: службы Xiaomi — MiDeviceService на выходе из Eco откатывает яркость и сама
diff --git a/ROADMAP.md b/ROADMAP.md
index 73e9023..d862521 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -339,7 +339,17 @@ Xiaomi-PC-Manager-Extended) и v0.16.1 (исправления авто-ярко
ответу прошивки «команды нет» (XIC-100); мелочи с код-ревью (XIC-101).
## 🔜 Ближайшее
-- Пока пусто — состав v0.19.0 не отобран; кандидаты — в «Идеях».
+Состав **v0.19.0** — три просьбы с одного ноутбука, Redmi Book Pro 2025 (TM2409):
+- **Выбор датчика температуры CPU** (XIC-105) — первая зона DPTF оказалась не ядром, цифра
+ расходится с реальной температурой: перечислять зоны с именами и дать выбрать, какую показывать
+ в «Мониторе», компактном виде и индикаторе трея.
+- **Ярлык в меню «Пуск»** (XIC-106) — просят пользователи; по клику при уже запущенной копии
+ поднимать существующий экземпляр (мьютекс + вывод окна вперёд), а не второй процесс.
+- **Частота 165 Гц** (XIC-107) — Windows её предлагает, а у нас в списке только 144: не опираться
+ на зашитый набор частот, проверить фильтр режимов и округление.
+
+Своего TM2409 нет: по температуре и частоте проверка упирается в живого тестера — это главный
+риск для состава версии.
## 💡 Идеи
- Камера/приватность (`0xA0` из карты MIControl) — на TM2424 событие не замечено, ждём тестеров.
diff --git a/docs/05-open-questions.md b/docs/05-open-questions.md
index 93e6d85..acc3309 100644
--- a/docs/05-open-questions.md
+++ b/docs/05-open-questions.md
@@ -209,8 +209,9 @@ Report ID `0x0D`, 33 байта in/out) — канал Xiaomi PC Manager к пр
команды, и все — глобальные настройки: версия прошивки `0x10`, сила вибрации `0x5D`, порог
нажатия `0x5B` (второе значение прошивка пересчитывает сама), «сильное нажатие» `0x59`. Вибрация
и порог сделаны (Настройки → Тачпад → «Тактильный отклик»). **Вывод этого вопроса не меняется:**
-зонального параметра для кнопки нет и там. `COL04` (`0xFF00`, Report ID `0x0E`) — другой канал,
-не трогаем.
+зонального параметра для кнопки нет и там. `COL04` (`0xFF00`, Report ID `0x0E`) — канал самого
+контроллера касаний BL7853 (`COL05` — MCU силы и вибрации за ним). В него шлём ровно один кадр OEM —
+запрос версии прошивки (XIC-104, docs/16); больше ничего.
**Импульс «вибрировать сейчас» нашёлся отдельно (XIC-73).** Он не в `SvrCModule.dll`, а в плагине
краевых жестов `Plugins\PluginGesture.dll` (`XiaomiPcHost.exe`, отдельный архив в установщике):
diff --git a/docs/16-oem-driver-check.md b/docs/16-oem-driver-check.md
index 0e064e5..0c6bfe5 100644
--- a/docs/16-oem-driver-check.md
+++ b/docs/16-oem-driver-check.md
@@ -138,13 +138,34 @@ XMAPT4B0P0A0A → "0A0A" → 0x0A0A = 2570 → "1.0.0.2570"
> `Win32_PnPSignedDriver` не встречаются — до XIC-102 строка была недостижима, и XiControl молчал там,
> где PC Manager предлагал прошивку. Версия — из `hardwareIds[0].version` (на TM2424: `740B08` у экрана,
> `3.5.13.01.44.25.13.0` у тачпада), имя устройства — локализованная константа по коду модуля, не из PnP.
-> С версией драйвера не сравниваем и «у вас старее» не говорим — текущую версию прошивки читать нечем
-> (см. «Ограничения»). Для обычных драйверов правило прежнее: нет совпавшего устройства — нет строки.
+> С версией драйвера не сравниваем. Текущую версию прошивки с XIC-104 читаем сами (ниже). Для обычных
+> драйверов правило прежнее: нет совпавшего устройства — нет строки.
>
> **Сверка модели в ответе (XIC-103).** Каждый модуль ответа несёт поле `product`. Модуль, где сервер
> явно назвал не ту модель, о которой спрашивали, отбрасывается (`OemProtocol.ParseResponse`, без учёта
> регистра; модуль без поля остаётся — доказать «чужую модель» нечем). Для драйверов это страховка поверх
> сверки по hardware-id, для прошивок — **единственная** проверка, что пакет про этот ноутбук.
+>
+> **Текущая версия прошивки (XIC-104)** — по разбору ниже, только для модулей, на которые в каталоге
+> есть пакет, по кнопке проверки, на пуле (`OemDriverCheck.RunAsync(currentFirmware)` →
+> `DriverMatch.Compare(..., firmware)`):
+>
+> - **тачпад** — две прошивки, обе читаются: контроллер касаний BL7853 —
+> `TouchpadHaptics.ReadControllerVersion()` через `COL04`, MCU силы и вибрации —
+> `ReadFirmwareVersion()`, команда `0x10` канала `COL05` (разбор — `TouchpadHapticsProtocol`).
+> Каталожная версия делится на части (`DriverMatch.TouchpadCatalogParts`: `3.5.13.01.44.25.13.0` →
+> `3.5.13.1` + `44.25.13.0`), текущая собирается так же (`TouchpadCurrent`, «контроллер / сила»), и
+> строка получает обычное «обновление / актуальна / новее каталога»: обновление — если старее хоть
+> одна часть. Не ответил `COL04` — сравниваем одну силу, без силы — «версия неизвестна». На TM2424
+> живьём: установлено `3.5.9.1 / 44.25.11.0`, в каталоге `3.5.13.1 / 44.25.13.0` → «обновление»;
+> - **экран** — `PanelFirmware.ReadRomVersion()`, ROM Ver контроллера Himax через IGCL; чистая часть
+> (последовательность операций, раскладка `ctl_aux_access_args_t`, разбор ответа) —
+> `PanelFirmwareProtocol`, тесты сверяют записи с OEM байт в байт. Версия в формате каталога
+> (`570B06`) показывается **рядом** с каталожной, состояние остаётся «сравнить нельзя» — см. «Чего это
+> не решает». Своя инициализация IGCL на один вызов, записи — только на выходе, где DPCD `0x400`
+> ответил OUI Himax; первая неудачная операция обрывает последовательность;
+> - не прочиталось → «версия неизвестна, в каталоге X» (у экрана — с подсказкой про локальный сеанс).
+> Результат каждого чтения пишется в журнал (`OemDriverCheck: текущая прошивка FWTP — …`).
Сверка «что установлено против того, что есть у OEM» делается без сети и без Xiaomi-софта:
@@ -172,8 +193,8 @@ XMAPT4B0P0A0A → "0A0A" → 0x0A0A = 2570 → "1.0.0.2570"
- каталог живёт у них на сервере и устаревает в кэше: полагаться только на локальный
`driver_cache.dat` нельзя — он даст «всё актуально» там, где это уже не так;
- метаданные местами мусорные (у чипсета `releaseTime` = `1968-07-18`) — даты показывать с оглядкой;
-- **текущие версии прошивок в приложении пока не читаем** — говорим «в каталоге есть прошивка X»
- (XIC-102), но не «у вас старее». Driver-free прочитать их **можно**, разбор ниже (XIC-103);
+- **текущая версия прошивки** читается driver-free (XIC-103, XIC-104), но «у вас старее» говорим только
+ про тачпад; у экрана — обе версии рядом, и только когда панель активна;
- hardware-id прошивок в каталоге **синтетические** и ни с чем в системе не совпадают — сверять их
по PnP бессмысленно, строка даётся по факту пакета для модели.
@@ -181,10 +202,23 @@ XMAPT4B0P0A0A → "0A0A" → 0x0A0A = 2570 → "1.0.0.2570"
**Тачпад (`FWTP`) — читается, стабильно.** Команда `0x10` нашего канала `COL05` (тот же, что у
тактильного отклика XIC-77, без повышения прав): ответ `2C 19 0B 00` = **44.25.11.0**. Работает и по
-RDP — HID-устройство от сеанса не зависит. В каталоге `3.5.13.01.44.25.13.0`: префикс `3.5.13.01`
-совпадает с именем пакета (`P701_TouchPad_0728_01_351301.zip`), а хвост — версия прошивки,
-**44.25.13.0**. То есть на TM2424 стоит прошивка старее каталожной. Служба Xiaomi эту версию
-не видит вовсе: ищет её в реестре и пишет «Failed to get current firmware version for FWTP».
+RDP — HID-устройство от сеанса не зависит. В каталоге `3.5.13.01.44.25.13.0` — **две** прошивки
+(см. «Что внутри пакета тачпада» ниже): `3.5.13.01` — контроллер касаний BL7853, `44.25.13.0` —
+MCU силы и вибрации. Команда `0x10` отдаёт версию второго: на TM2424 стоит 44.25.11.0, старее
+каталожной. Служба Xiaomi FWTP не видит вовсе: ищет версию в реестре и пишет «Failed to get current
+firmware version for FWTP».
+
+**Контроллер касаний BL7853 — через `COL04` (XIC-104).** Канал `0xFF00`, Report ID `0x0E`, 33 байта.
+Первым запрос опубликовал gxcreator в [issue #99](https://github.com/Oksion/XiControl/issues/99); мы
+сверили его с кодом `FirmwareTouchPadTool.exe` из пакета (запись кадра по смещению файла `0x1C7EA`):
+
+- запрос — константный кадр `0E 07 B4 00 00 01 00 04 B6` + нули до 33 байт, `HidD_SetOutputReport`;
+- пауза **30 мс**, затем `HidD_GetInputReport` с `[0] = 0x0E`;
+- годен ответ с `[1] = 0x06` и `[2] = 0xB6` (эхо команды, суммы в этом канале нет), версия —
+ байты `[7].[6].[4].[5]` десятичными.
+
+Это **единственный** кадр, который XiControl шлёт в `COL04`, и он не собирается, а взят как есть.
+На TM2424: **3.5.9.1** при `3.5.13.01` в каталоге; у автора #99 после прошивки — 3.5.13.1 и 44.25.13.0.
**Экран (`FWSC`) — читается, но нестабильно и не без записи.** Это прошивка **TCON-контроллера
панели Himax HX8874**, а не «монитора». `FirmwareScreenTool.exe --version` (так его зовёт служба,
@@ -212,9 +246,12 @@ RDP — HID-устройство от сеанса не зависит. В ка
- чтение регистра включает **записи в контроллер** (`0D 00`, `20 01`, адрес). Это ровно то, что OEM
делает при каждом скане, регистры прошивки не меняются, но это всё же запись в TCON — если делать
в приложении, то только по кнопке проверки;
-- живьём нашим кодом не проверено: на момент ресерча машина была доступна только по RDP. Проба —
- `panel-fw-version.ps1` в рабочем каталоге проекта (вне git): без ключей только читает DPCD,
- с `-ReadVersion` повторяет последовательность OEM.
+- живьём за ноутбуком нашим кодом **ещё не проверено**: и ресерч XIC-103, и реализация XIC-104 шли по
+ RDP. По RDP проверено только «панели нет → версия неизвестна, ни одной AUX-операции» (в журнале
+ `PanelFirmware: контроллер Himax не найден`). Сам алгоритм независимо подтвердил gxcreator в
+ [issue #100](https://github.com/Oksion/XiControl/issues/100): своим скриптом за ноутбуком получил
+ ROM Ver `0x570B06` и FW Ver `0x2701`, как в логе OEM-утилиты. Проба — `panel-fw-version.ps1` в рабочем каталоге
+ проекта (вне git): без ключей только читает DPCD, с `-ReadVersion` повторяет последовательность OEM.
**Ревизии в синтетических id (`REV_1236/1237`, `REV_1836/1837`) расшифровать нельзя.** Служба
сопоставляет прошивку по коду модуля (`fw_id`) и версии, а hardware-id из каталога лишь пишет в лог
@@ -227,6 +264,39 @@ RDP — HID-устройство от сеанса не зависит. В ка
последняя часть. Поэтому даже с чтением версии «у вас старее» честно говорить можно только про
тачпад; для экрана — показывать обе версии рядом, без вывода.
+### Что внутри пакета тачпада (разбор 2026-10-20)
+
+`P701_TouchPad_0728_01_351301.zip` (1 973 496 байт, md5 совпал с каталогом) скачан для разбора, ничего
+не запускалось. Внутри:
+
+- `TP\BL7853_V3.5.13.01_20260615(67D2).bin`, 64 КБ — прошивка **контроллера касаний Betterlife
+ BL7853**. Код начинается с таблицы переходов `48 00 xx xx`, похоже на Andes NDS32; rizin эту
+ архитектуру не знает, код не дизассемблирован;
+- `Force\44.25.13.0.bin` (она же в корне), 33 КБ — прошивка **отдельного MCU силы и вибрации**,
+ Cortex-M (векторы: SP `0x20003C60`, reset `0x0100108D`). HID-дескриптора в нём нет: это MCU за
+ контроллером касаний, наружу он виден только через `COL05`. Утилита так и пишет: «current force
+ version», «read force calibrate data»;
+- `FirmwareTouchPadTool.exe` + `btl_upgrade.dll`/`btl_hid_transfer.dll` (BetterlifeForwardTool) —
+ прошивальщик. Ходит в `col04` и `col05`, вызывает MIFS `MiInterface`, ищет окно
+ `mi_touch_gesture`/`MiTouchGestureClass` (`FindWindowExW`) — по всей видимости, чтобы на время
+ прошивки унять жесты PC Manager;
+- `Install.bat` вызывает `FirmwareScreenTool.exe --update HX_V0057_0B06.bin` — команду прошивки
+ **экрана**, которой в архиве нет: батник скопирован из пакета панели. Совпадение: `0057_0B06` — это
+ ровно текущий ROM Ver нашей панели `570B06`;
+- `CCP\ibtusb.*` — посторонний драйвер, к тачпаду отношения не имеет.
+
+**Аппаратных «умных жестов» в прошивке нет.** В BL7853 два HID-дескриптора: обычный PTP и вариант с
+силой — на TM2424 стоит второй. Он даёт только: мышь (ID 1, legacy-режим), PTP на 5 касаний с
+16-битным Tip Pressure (ID 4), служебные feature-репорты (ID 2/3/5/6/7) и два вендорских канала по
+32 байта — `0xFF00` (ID 14, `COL04`) и `0xFF01` (ID 13, `COL05`). Ни клавиатуры, ни Consumer Control,
+ни usage жестов: распознанный жест прошивке передать в Windows просто нечем, кроме вендорских
+каналов, а в `COL05` input-репорты — ответы на запросы. Значит, жесты Windows (2–4 пальца) считает
+сама Windows по контактам, а краевые жесты и сильное нажатие Xiaomi — PC Manager (`PluginGesture.dll`)
+по тем же контактам и давлению. На «железе» остаётся только тактильная часть: порог клика `0x5B` и
+второй щелчок на сильном нажатии `0x59` прошивка отрабатывает сама, но событием наружу не сообщает.
+Не проверено: шлёт ли прошивка что-то непрошенное в `COL04` (сам канал — команды контроллеру, им же
+читается версия, см. выше) — для этого нужен код BL7853.
+
## 5. Где лежат данные
Захват не в репозитории (сырые ответы содержат хеш серийника машины) — он в рабочем каталоге
diff --git a/src/Localization/lang/en.json b/src/Localization/lang/en.json
index 05301b7..0b0e6c7 100644
--- a/src/Localization/lang/en.json
+++ b/src/Localization/lang/en.json
@@ -282,9 +282,16 @@
"settings.drivers.update.desc": "{0} → {1} · {2}",
"settings.drivers.package": "{0}, {1} MB",
"settings.drivers.firmware.group": "Firmware",
- "settings.drivers.firmware.warn": "Flashing the display or touchpad firmware is at your own risk. XiControl has not checked the package and does not install it: the link opens in your browser. The current firmware version cannot be read — you may already have this one. Install only with the tool inside this archive, on AC power, and do not interrupt it.",
+ "settings.drivers.firmware.warn": "Flashing the display or touchpad firmware is at your own risk. XiControl has not checked the package and does not install it: the link opens in your browser. Install only with the tool inside this archive, on AC power, and do not interrupt it.",
"settings.drivers.firmware.title": "Firmware: {0}",
- "settings.drivers.firmware.desc": "Catalog version {0} · {1}. The current firmware version cannot be read, so it cannot be compared.",
+ "settings.drivers.firmware.desc": "Catalog version {0} · {1}. The current firmware version could not be read, so it cannot be compared.",
+ "settings.drivers.firmware.unknown.panel": "Catalog version {0} · {1}. Current version unknown: the display controller only answers in a local session with the screen on, not over Remote Desktop.",
+ "settings.drivers.firmware.side": "Installed {0}, catalog {1} · {2}. Firmware numbers depend on the panel variant, so they do not tell whether the catalog one is newer.",
+ "settings.drivers.firmware.same": "Installed {0}, same as the catalog · {1}",
+ "settings.drivers.firmware.newer": "Installed {0}, catalog {1} — yours is newer · {2}",
+ "settings.drivers.firmware.v.same": "up to date",
+ "settings.drivers.firmware.v.newer": "newer than catalog",
+ "settings.drivers.firmware.tp.parts": "The touchpad has two firmwares: the first version is the touch controller, the second is force and haptics.",
"settings.drivers.download": "Open link",
"settings.drivers.download.confirm": "Open in browser?",
"settings.drivers.nolink": "no link",
diff --git a/src/Localization/lang/ru.json b/src/Localization/lang/ru.json
index 8f79af7..d56bab3 100644
--- a/src/Localization/lang/ru.json
+++ b/src/Localization/lang/ru.json
@@ -282,9 +282,16 @@
"settings.drivers.update.desc": "{0} → {1} · {2}",
"settings.drivers.package": "{0}, {1} МБ",
"settings.drivers.firmware.group": "Прошивки",
- "settings.drivers.firmware.warn": "Прошивка экрана или тачпада — на ваш страх и риск. XiControl пакет не проверял и не устанавливает: ссылка откроется в браузере. Текущую версию прошивки прочитать нечем — возможно, у вас уже она. Ставьте только штатной утилитой из этого архива, на питании от сети и не прерывая процесс.",
+ "settings.drivers.firmware.warn": "Прошивка экрана или тачпада — на ваш страх и риск. XiControl пакет не проверял и не устанавливает: ссылка откроется в браузере. Ставьте только штатной утилитой из этого архива, на питании от сети и не прерывая процесс.",
"settings.drivers.firmware.title": "Прошивка: {0}",
- "settings.drivers.firmware.desc": "В каталоге версия {0} · {1}. Текущую версию прошивки прочитать нечем — сравнить не можем.",
+ "settings.drivers.firmware.desc": "В каталоге версия {0} · {1}. Текущую версию прошивки прочитать не удалось — сравнить не можем.",
+ "settings.drivers.firmware.unknown.panel": "В каталоге версия {0} · {1}. Текущая версия неизвестна: контроллер экрана отвечает только в локальном сеансе при включённом экране, не по удалённому рабочему столу.",
+ "settings.drivers.firmware.side": "Установлена {0}, в каталоге {1} · {2}. Номер прошивки зависит от варианта панели, поэтому новее ли каталожная — по номерам не сказать.",
+ "settings.drivers.firmware.same": "Установлена {0}, как в каталоге · {1}",
+ "settings.drivers.firmware.newer": "Установлена {0}, в каталоге {1} — у вас новее · {2}",
+ "settings.drivers.firmware.v.same": "актуальна",
+ "settings.drivers.firmware.v.newer": "новее каталога",
+ "settings.drivers.firmware.tp.parts": "В тачпаде две прошивки: первая версия — контроллер касаний, вторая — сила нажатия и вибрация.",
"settings.drivers.download": "Открыть ссылку",
"settings.drivers.download.confirm": "Открыть в браузере?",
"settings.drivers.nolink": "нет ссылки",
diff --git a/src/Localization/lang/zh.json b/src/Localization/lang/zh.json
index a131b19..ff5eefc 100644
--- a/src/Localization/lang/zh.json
+++ b/src/Localization/lang/zh.json
@@ -282,9 +282,16 @@
"settings.drivers.update.desc": "{0} → {1} · {2}",
"settings.drivers.package": "{0},{1} MB",
"settings.drivers.firmware.group": "固件",
- "settings.drivers.firmware.warn": "刷写屏幕或触控板固件风险自负。XiControl 未检查该固件包,也不会安装:链接将在浏览器中打开。无法读取当前固件版本——你可能已经是这个版本。请仅使用此压缩包内的官方工具、在接通电源的情况下刷写,且不要中断。",
+ "settings.drivers.firmware.warn": "刷写屏幕或触控板固件风险自负。XiControl 未检查该固件包,也不会安装:链接将在浏览器中打开。请仅使用此压缩包内的官方工具、在接通电源的情况下刷写,且不要中断。",
"settings.drivers.firmware.title": "固件:{0}",
- "settings.drivers.firmware.desc": "目录版本 {0} · {1}。无法读取当前固件版本,因此无法比较。",
+ "settings.drivers.firmware.desc": "目录版本 {0} · {1}。未能读取当前固件版本,因此无法比较。",
+ "settings.drivers.firmware.unknown.panel": "目录版本 {0} · {1}。当前版本未知:屏幕控制器仅在本地会话且屏幕开启时响应,远程桌面下无法读取。",
+ "settings.drivers.firmware.side": "已安装 {0},目录 {1} · {2}。固件编号因面板型号而异,无法据此判断目录版本是否更新。",
+ "settings.drivers.firmware.same": "已安装 {0},与目录一致 · {1}",
+ "settings.drivers.firmware.newer": "已安装 {0},目录 {1}——你的版本更新 · {2}",
+ "settings.drivers.firmware.v.same": "已是最新",
+ "settings.drivers.firmware.v.newer": "比目录新",
+ "settings.drivers.firmware.tp.parts": "触控板有两套固件:第一个版本是触控控制器,第二个是压感与振动。",
"settings.drivers.download": "打开链接",
"settings.drivers.download.confirm": "在浏览器中打开?",
"settings.drivers.nolink": "无链接",
diff --git a/src/SystemIntegration/OemDrivers.cs b/src/SystemIntegration/OemDrivers.cs
index 0ee5f68..b5953a6 100644
--- a/src/SystemIntegration/OemDrivers.cs
+++ b/src/SystemIntegration/OemDrivers.cs
@@ -19,8 +19,10 @@ public sealed record OemHardwareId(string Id, string Version);
/// Установленный драйвер: один из hardware-/compatible-id устройства и версия его драйвера.
public sealed record InstalledDriver(string HardwareId, string Version, string Device);
-/// Сверка одного устройства с каталогом. — прошивка: её текущую
-/// версию читать нечем и устройства под её id в системе нет, поэтому только «в каталоге есть такая».
+/// Сверка одного устройства с каталогом. — прошивка, которую сравнить
+/// нельзя: текущая версия не прочиталась или (у экрана) номера не сопоставимы — «в каталоге есть такая»,
+/// и, если прочиталась, текущая рядом. Прошивка тачпада с прочитанной версией получает обычные
+/// Same/Update/Newer (XIC-104).
public enum DriverState { Same, Update, Newer, Firmware }
public sealed record DriverRow(string Module, string Device, string? Installed, string Catalog, DriverState State, OemPackage? Package);
@@ -62,6 +64,8 @@ public static class OemProtocol
/// Прошивки: версии в каталоге — прошивочные, сравнивать их с версией драйвера нельзя.
internal static bool IsFirmware(string module) => module is "FWTP" or "FWSC";
+ internal const string TouchpadFirmware = "FWTP", PanelFirmware = "FWSC";
+
///
/// Тело запроса. Ключи строго по алфавиту и компактно: служба собирает JSON на std::map, и
/// подпись считается по ровно такой строке. Без sn: сервер отвечает так же, а хеш
@@ -191,16 +195,45 @@ internal static int CompareVersions(string a, string b) =>
? va.CompareTo(vb)
: string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
+ /// Разделитель частей версии тачпада в строке: «контроллер / сила».
+ internal const string PartSeparator = " / ";
+
+ ///
+ /// Каталожная версия тачпада по частям. В пакете две прошивки: 3.5.13.01.44.25.13.0 —
+ /// это контроллер касаний Betterlife BL7853 (3.5.13.01, он же 3.5.13.1) и отдельный
+ /// MCU силы и вибрации (44.25.13.0, папка Force архива). Сила — последние четыре
+ /// числа, контроллер — первые четыре, если они есть. null — строка другого вида.
+ ///
+ internal static (string? Controller, string Force)? TouchpadCatalogParts(string catalog)
+ {
+ var parts = catalog.Split('.');
+ if (parts.Length is not (4 or 8) || !parts.All(p => p.Length > 0 && p.All(char.IsAsciiDigit))) return null;
+ string force = string.Join('.', parts[^4..]);
+ // ведущие нули — как в каталоге (01), сам контроллер отдаёт числами (1)
+ string? controller = parts.Length == 8 ? Version.Parse(string.Join('.', parts[..4])).ToString() : null;
+ return (controller, force);
+ }
+
+ /// Текущая версия тачпада одной строкой, в том же порядке, что каталог: «контроллер / сила»
+ /// или одна сила, если COL04 не ответил. Без версии силы — null: сравнивать не с чем.
+ internal static string? TouchpadCurrent(string? controller, string? force) =>
+ force is null ? null : controller is null ? force : controller + PartSeparator + force;
+
///
/// Строки сверки. Версия — из записи ЭТОГО hardware-id, не «пакета»: один zip везёт драйверы
/// разных вендоров (у аудио 10 разных версий на 125 id). Несколько id каталога могут указывать
/// на одно устройство — строку даём одну.
/// Прошивки (XIC-102) — без сверки по hardware-id: их id в каталоге синтетические
- /// (USB\VID_6789&PID_1234&REV_1236 у экрана), такого устройства в системе нет,
- /// а текущую версию прошивки читать нечем. Поэтому строка — одна на модуль с пакетом,
- /// «в каталоге есть прошивка X»; в Device — код модуля, имя ему даёт UI по локализации.
+ /// (USB\VID_6789&PID_1234&REV_1236 у экрана), такого устройства в системе нет.
+ /// Строка — одна на модуль с пакетом; в Device — код модуля, имя ему даёт UI по локализации.
+ /// Текущая версия прошивки — из (код модуля → прочитанное, XIC-104):
+ /// у тачпада сравнивается по частям (контроллер и сила; «обновление», если старее хоть одна),
+ /// у экрана только встаёт рядом — ROM Ver панели
+ /// отличается от каталожного старшим байтом, и «старее» это или другой вариант панели, по номеру
+ /// не сказать (docs/16).
///
- internal static List Compare(IEnumerable modules, IReadOnlyList installed)
+ internal static List Compare(IEnumerable modules, IReadOnlyList installed,
+ IReadOnlyDictionary? firmware = null)
{
var rows = new List();
var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
@@ -209,7 +242,7 @@ internal static List Compare(IEnumerable modules, IReadOnl
if (OemProtocol.IsFirmware(m.Name))
{
if (m.Package is not null && seen.Add(m.Name))
- rows.Add(new DriverRow(m.Name, m.Name, null, m.HardwareIds.Count > 0 ? m.HardwareIds[0].Version : "", DriverState.Firmware, m.Package));
+ rows.Add(FirmwareRow(m, firmware?.GetValueOrDefault(m.Name)));
continue;
}
foreach (var h in m.HardwareIds)
@@ -223,6 +256,27 @@ internal static List Compare(IEnumerable modules, IReadOnl
}
return rows;
}
+
+ private static DriverRow FirmwareRow(OemModule m, string? current)
+ {
+ string catalog = m.HardwareIds.Count > 0 ? m.HardwareIds[0].Version : "";
+ var state = DriverState.Firmware;
+ if (m.Name == OemProtocol.TouchpadFirmware && TouchpadCatalogParts(catalog) is { } cat)
+ {
+ // часть, которую не прочитали, не сравниваем и не показываем — пары должны совпадать
+ string? ctl = null, force = current;
+ if (current?.Split(PartSeparator) is [var a, var b]) (ctl, force) = (a, b);
+ bool both = ctl is not null && cat.Controller is not null;
+ catalog = both ? cat.Controller + PartSeparator + cat.Force : cat.Force;
+ if (force is not null)
+ {
+ int f = CompareVersions(cat.Force, force), c = both ? CompareVersions(cat.Controller!, ctl!) : 0;
+ state = f > 0 || c > 0 ? DriverState.Update : f < 0 || c < 0 ? DriverState.Newer : DriverState.Same;
+ current = both ? current : force;
+ }
+ }
+ return new DriverRow(m.Name, m.Name, current, catalog, state, m.Package);
+ }
}
///
@@ -235,8 +289,10 @@ public static class OemDriverCheck
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(15);
private const int Parallel = 4; // вежливо к их серверу: полный скан — 25 запросов
- /// Звать только из фонового потока.
- public static async Task RunAsync(CancellationToken ct = default)
+ /// Звать только из фонового потока. — чтение текущей
+ /// версии прошивки по коду модуля (XIC-104); зовётся только для прошивок, на которые в каталоге
+ /// есть пакет: у экрана чтение включает записи в контроллер, без повода их не делаем.
+ public static async Task RunAsync(Func? currentFirmware = null, CancellationToken ct = default)
{
var sys = SystemInfo.Current;
string? product = sys.Board;
@@ -280,7 +336,7 @@ public static async Task RunAsync(CancellationToken ct = defa
int failed = answers.Count(a => a is null);
var modules = answers.Where(a => a is not null).SelectMany(a => a!).ToList();
- var rows = DriverMatch.Compare(modules, installed);
+ var rows = DriverMatch.Compare(modules, installed, ReadFirmware(modules, currentFirmware));
var status = failed == answers.Length ? DriverCheckStatus.Failed
: modules.Count == 0 ? DriverCheckStatus.NoCatalog
: rows.Count == 0 ? DriverCheckStatus.NoMatch
@@ -291,6 +347,24 @@ public static async Task RunAsync(CancellationToken ct = defa
bios is not null, failed, DateTime.Now);
}
+ // По очереди, а не параллельно: тачпад и контроллер панели — разные устройства, но спешить
+ // некуда, а чтение версии у каждого — секунды пауз между кадрами
+ private static Dictionary? ReadFirmware(List modules, Func? read)
+ {
+ if (read is null) return null;
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (string name in modules.Where(m => OemProtocol.IsFirmware(m.Name) && m.Package is not null)
+ .Select(m => m.Name).Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ string? v = null;
+ try { v = read(name); }
+ catch (Exception ex) { Log.Ex($"OemDriverCheck.ReadFirmware({name})", ex); }
+ Log.Write($"OemDriverCheck: текущая прошивка {name} — {v ?? "не прочиталась"}");
+ if (v is not null) result[name] = v;
+ }
+ return result;
+ }
+
private static string Trim(string s) => s.Length <= 120 ? s : s[..120] + "…";
private static string WindowsBuild()
diff --git a/src/SystemIntegration/PanelFirmware.cs b/src/SystemIntegration/PanelFirmware.cs
new file mode 100644
index 0000000..d5b06df
--- /dev/null
+++ b/src/SystemIntegration/PanelFirmware.cs
@@ -0,0 +1,178 @@
+using System.Buffers.Binary;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using static XiControl.SystemIntegration.PanelFirmwareProtocol;
+
+namespace XiControl.SystemIntegration;
+
+///
+/// Протокол чтения версии прошивки TCON-контроллера панели Himax HX8874 (XIC-103/104): чистая
+/// логика, без IGCL. Восстановлен из IGCLCSharpDLL.dll (класс IGCLAUXFunc), которой
+/// пользуется FirmwareScreenTool.exe службы Xiaomi, — последовательность повторена байт в
+/// байт, ничего сверх. Разбор — docs/16-oem-driver-check.md, «Текущие версии прошивок».
+///
+public static class PanelFirmwareProtocol
+{
+ /// Одна операция ctlAUXAccess: запись или чтение, канал, адрес, данные.
+ public sealed record AuxOp(int Op, int Flags, uint Address, byte[] Data, int Size);
+
+ public const int OpRead = 1, OpWrite = 2;
+ public const int FlagNative = 1, FlagI2c = 2, FlagI2cMot = 4; // native AUX, I2C-over-AUX, I2C с MOT
+ public const uint DpcdSinkOui = 0x400; // DPCD: OUI приёмника, 3 байта
+ public const uint HimaxI2c = 0xF2; // I2C-адрес контроллера за AUX
+ public const uint RegRomVersion = 0xE7800210; // ROM Ver — его служба сверяет с каталогом
+
+ // ctl_aux_access_args_t, 0xB0 байт: Size@0, Version@4, OpType@8, Flags@0xC, Address@0x10,
+ // RAD@0x18, PortID@0x20, DataSize@0x24, Data[132]@0x28 — сверено с IGCLCSharpDLL.dll
+ public const int ArgsSize = 0xB0, OffDataSize = 0x24, OffData = 0x28, DataMax = 132;
+
+ /// Чтение OUI приёмника — только чтение, им контроллер и опознаётся.
+ public static AuxOp ReadOui() => new(OpRead, FlagNative, DpcdSinkOui, [], 3);
+
+ /// Himax: OUI 94 0B D5. Только на нём делаем записи чтения регистра.
+ public static bool IsHimax(byte[] oui) => oui is [0x94, 0x0B, 0xD5];
+
+ ///
+ /// Чтение регистра (HX8874RegRead): запись 0D 00, запись 20 01, запись с MOT
+ /// 10 байт 00 <адрес LE> 00…, чтение 4 байт. Записи идут в контроллер — так делает OEM
+ /// при каждом скане; регистры прошивки от этого не меняются.
+ ///
+ public static AuxOp[] RegisterRead(uint reg)
+ {
+ var addr = new byte[10];
+ BinaryPrimitives.WriteUInt32LittleEndian(addr.AsSpan(1), reg);
+ return
+ [
+ new(OpWrite, FlagI2c, HimaxI2c, [0x0D, 0x00], 2),
+ new(OpWrite, FlagI2c, HimaxI2c, [0x20, 0x01], 2),
+ new(OpWrite, FlagI2cMot, HimaxI2c, addr, 10),
+ new(OpRead, FlagI2c, HimaxI2c, [], 4),
+ ];
+ }
+
+ /// Аргументы ctlAUXAccess под операцию — ровно 0xB0 байт.
+ public static byte[] Args(AuxOp op)
+ {
+ var a = new byte[ArgsSize];
+ BinaryPrimitives.WriteInt32LittleEndian(a.AsSpan(0x00), ArgsSize);
+ BinaryPrimitives.WriteInt32LittleEndian(a.AsSpan(0x08), op.Op);
+ BinaryPrimitives.WriteInt32LittleEndian(a.AsSpan(0x0C), op.Flags);
+ BinaryPrimitives.WriteUInt32LittleEndian(a.AsSpan(0x10), op.Address);
+ BinaryPrimitives.WriteInt32LittleEndian(a.AsSpan(OffDataSize), op.Size);
+ op.Data.CopyTo(a, OffData);
+ return a;
+ }
+
+ /// Данные ответа: DataSize после вызова, не больше буфера. Негодный размер — пусто.
+ public static byte[] Result(byte[] args)
+ {
+ int got = BinaryPrimitives.ReadInt32LittleEndian(args.AsSpan(OffDataSize));
+ return got is > 0 and <= DataMax ? args[OffData..(OffData + got)] : [];
+ }
+
+ /// ROM Ver в виде каталога: 4 байта LE → hex без префикса (06 0B 57 00 →
+ /// 570B06, в каталоге так же — 740B08). Ноль или короткий ответ — null: так
+ /// контроллер отвечает, когда ничего не прочиталось.
+ public static string? RomVersion(byte[] r)
+ {
+ if (r.Length < 4) return null;
+ uint v = BinaryPrimitives.ReadUInt32LittleEndian(r);
+ return v == 0 ? null : v.ToString("X6", CultureInfo.InvariantCulture);
+ }
+}
+
+///
+/// Текущая версия прошивки панели (ROM Ver TCON Himax) через Intel IGCL ctlAUXAccess — eDP AUX,
+/// user-mode API драйвера Intel, тот же ControlLib.dll, что у .
+/// Зовётся только по кнопке проверки драйверов и только если в каталоге есть прошивка экрана: чтение
+/// включает записи в контроллер. Видна панель лишь в локальном сеансе при включённом экране — по RDP
+/// IGCL отдаёт ноль выходов, и null («версия неизвестна») здесь нормальный ответ, не ошибка.
+/// Своя инициализация IGCL на один вызов: держать её открытой между проверками незачем.
+///
+public static class PanelFirmware
+{
+ private const int InitArgsSize = 36, AppVersion = 1 << 16; // как в GpuTelemetry
+
+ /// ROM Ver контроллера («570B06») или null. Звать из фонового потока.
+ public static string? ReadRomVersion()
+ {
+ IntPtr api = IntPtr.Zero;
+ try
+ {
+ if (!Init(out api)) return null;
+ foreach (IntPtr output in Outputs(api))
+ {
+ if (Run(output, ReadOui()) is not { } oui || !IsHimax(oui)) continue;
+ byte[]? r = null;
+ foreach (var op in RegisterRead(RegRomVersion))
+ if ((r = Run(output, op)) is null) break; // операция не прошла — дальше не пишем
+ string? v = r is null ? null : RomVersion(r);
+ if (v is null) Log.Write("PanelFirmware: контроллер Himax найден, но ROM Ver не прочитался");
+ return v;
+ }
+ Log.Write("PanelFirmware: контроллер Himax не найден (RDP или экран выключен)");
+ return null;
+ }
+ catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or BadImageFormatException)
+ {
+ Log.Write($"PanelFirmware: IGCL недоступен — {ex.GetType().Name}"); // не Intel
+ return null;
+ }
+ finally
+ {
+ if (api != IntPtr.Zero)
+ try
+ {
+ int r = ctlClose(api);
+ if (r != 0) Log.Write($"PanelFirmware: ctlClose -> 0x{r:X8}");
+ }
+ catch (Exception ex) { Log.Ex("PanelFirmware.Close", ex); }
+ }
+ }
+
+ private static bool Init(out IntPtr api)
+ {
+ var args = new byte[InitArgsSize];
+ BinaryPrimitives.WriteInt32LittleEndian(args.AsSpan(0), InitArgsSize);
+ BinaryPrimitives.WriteInt32LittleEndian(args.AsSpan(8), AppVersion);
+ int r = ctlInit(args, out api);
+ if (r == 0) return true;
+ Log.Write($"PanelFirmware: ctlInit -> 0x{r:X8}");
+ api = IntPtr.Zero;
+ return false;
+ }
+
+ private static List Outputs(IntPtr api)
+ {
+ var list = new List();
+ uint n = 0;
+ if (ctlEnumerateDevices(api, ref n, null) != 0 || n == 0) return list;
+ var devices = new IntPtr[n];
+ if (ctlEnumerateDevices(api, ref n, devices) != 0) return list;
+ foreach (IntPtr d in devices.Take((int)n))
+ {
+ uint m = 0;
+ if (ctlEnumerateDisplayOutputs(d, ref m, null) != 0 || m == 0) continue;
+ var outs = new IntPtr[m];
+ if (ctlEnumerateDisplayOutputs(d, ref m, outs) == 0) list.AddRange(outs.Take((int)m));
+ }
+ return list;
+ }
+
+ /// Одна операция; null — IGCL вернул ошибку.
+ private static byte[]? Run(IntPtr output, AuxOp op)
+ {
+ byte[] args = Args(op);
+ int r = ctlAUXAccess(output, args);
+ if (r == 0) return Result(args);
+ Log.Write($"PanelFirmware: ctlAUXAccess op={op.Op} flags={op.Flags} addr=0x{op.Address:X} -> 0x{r:X8}");
+ return null;
+ }
+
+ private const string Lib = "ControlLib.dll";
+ [DllImport(Lib)] private static extern int ctlInit(byte[] args, out IntPtr api);
+ [DllImport(Lib)] private static extern int ctlClose(IntPtr api);
+ [DllImport(Lib)] private static extern int ctlEnumerateDevices(IntPtr api, ref uint count, IntPtr[]? devices);
+ [DllImport(Lib)] private static extern int ctlEnumerateDisplayOutputs(IntPtr device, ref uint count, IntPtr[]? outputs);
+ [DllImport(Lib)] private static extern int ctlAUXAccess(IntPtr output, [In, Out] byte[] args);
+}
diff --git a/src/SystemIntegration/TouchpadHaptics.cs b/src/SystemIntegration/TouchpadHaptics.cs
index 7327360..1fdb913 100644
--- a/src/SystemIntegration/TouchpadHaptics.cs
+++ b/src/SystemIntegration/TouchpadHaptics.cs
@@ -23,9 +23,14 @@ public sealed class TouchpadHaptics
private const int SettleMs = 120;
private const int ReadAttempts = 3;
+ // COL05 — наш рабочий канал (MCU силы и вибрации); COL04 — канал контроллера касаний BL7853,
+ // в него шлём ровно один кадр OEM — запрос версии (XIC-104)
+ private const ushort ForcePage = 0xFF01, ControllerPage = 0xFF00;
+ private const int ControllerSettleMs = 30; // пауза OEM-прошивальщика перед ответом COL04
+
private readonly object _lock = new();
- private string? _path;
- private bool _searched;
+ private string? _path, _ctlPath;
+ private bool _searched, _ctlSearched;
/// Прочитать обе настройки. null — тачпада нет или он не ответил.
public TouchpadHapticsState? Read()
@@ -42,6 +47,39 @@ public sealed class TouchpadHaptics
}
}
+ /// Версия прошивки MCU силы и вибрации (команда 0x10 в COL05, только чтение — XIC-104).
+ /// null — тачпада нет или он не ответил. Зовётся по кнопке проверки драйверов, с пула.
+ public string? ReadFirmwareVersion()
+ {
+ lock (_lock)
+ {
+ using var h = Open();
+ return h is not null && ReadValues(h, CmdFirmware, 2) is { } v ? FirmwareVersion(v) : null;
+ }
+ }
+
+ /// Версия прошивки контроллера касаний BL7853 через COL04 (XIC-104): кадр OEM как есть,
+ /// 30 мс, ответ. Пустой или чужой ответ — повтор того же кадра, как у чтения COL05. null — нет
+ /// канала или ответа. Только по кнопке проверки драйверов.
+ public string? ReadControllerVersion()
+ {
+ lock (_lock)
+ {
+ using var h = Open(controller: true);
+ if (h is null) return null;
+ for (int i = 0; i < ReadAttempts; i++)
+ {
+ if (!Send(h, ControllerVersionRequest())) return null;
+ Thread.Sleep(ControllerSettleMs);
+ var r = new byte[FrameLength];
+ r[0] = ControllerReportId;
+ if (HidD_GetInputReport(h, r, FrameLength) && ParseControllerVersion(r) is { } v) return v;
+ }
+ Log.Write("TouchpadHaptics: COL04 не ответил на запрос версии");
+ return null;
+ }
+ }
+
/// Записать силу вибрации; true — тачпад подтвердил значение чтением.
public bool SetVibration(HapticsVibration level) =>
Write(CmdVibration, VibrationValues(level), v => MatchVibration(v[0], v[1]) == level);
@@ -118,20 +156,22 @@ private static bool Send(SafeFileHandle h, byte[] frame)
// Хэндл на операцию, как у PC Manager: держать открытым незачем, а после сна/переподключения
// старый хэндл протухает молча
- private SafeFileHandle? Open()
+ private SafeFileHandle? Open(bool controller = false)
{
- if (!_searched) { _path = FindCollection(); _searched = true; }
- if (_path is null) return null;
- var h = CreateFileW(_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ ref string? path = ref controller ? ref _ctlPath : ref _path;
+ ref bool searched = ref controller ? ref _ctlSearched : ref _searched;
+ if (!searched) { path = FindCollection(controller ? ControllerPage : ForcePage); searched = true; }
+ if (path is null) return null;
+ var h = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
if (!h.IsInvalid) return h;
- Log.Write($"TouchpadHaptics: открыть коллекцию не удалось ({Marshal.GetLastWin32Error()})");
+ Log.Write($"TouchpadHaptics: открыть коллекцию 0x{(controller ? ControllerPage : ForcePage):X4} не удалось ({Marshal.GetLastWin32Error()})");
h.Dispose();
- _searched = false; // устройство могло переподключиться под другим путём — поищем снова
+ searched = false; // устройство могло переподключиться под другим путём — поищем снова
return null;
}
- private static string? FindCollection()
+ private static string? FindCollection(ushort usagePage)
{
HidD_GetHidGuid(out Guid guid);
IntPtr set = SetupDiGetClassDevsW(ref guid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
@@ -143,7 +183,7 @@ private static bool Send(SafeFileHandle h, byte[] frame)
{
string? path = InterfacePath(set, ref di);
if (path is null || !path.Contains("bltp7853", StringComparison.OrdinalIgnoreCase)) continue;
- if (IsVendorChannel(path)) return path;
+ if (IsVendorChannel(path, usagePage)) return path;
}
return null;
}
@@ -164,16 +204,16 @@ private static bool Send(SafeFileHandle h, byte[] frame)
finally { Marshal.FreeHGlobal(buf); }
}
- /// Вендорская коллекция с нашим протоколом: UsagePage 0xFF01, отчёты по 33 байта.
- /// Открываем без прав доступа — только спросить дескриптор.
- private static bool IsVendorChannel(string path)
+ /// Вендорская коллекция с нашим протоколом: нужная UsagePage (0xFF01 — COL05, 0xFF00 —
+ /// COL04), отчёты по 33 байта. Открываем без прав доступа — только спросить дескриптор.
+ private static bool IsVendorChannel(string path, ushort usagePage)
{
using var h = CreateFileW(path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
if (h.IsInvalid || !HidD_GetPreparsedData(h, out IntPtr pp)) return false;
try
{
return HidP_GetCaps(pp, out HIDP_CAPS c) == HIDP_STATUS_SUCCESS &&
- c.UsagePage == 0xFF01 && c.OutputReportByteLength == FrameLength && c.InputReportByteLength == FrameLength;
+ c.UsagePage == usagePage && c.OutputReportByteLength == FrameLength && c.InputReportByteLength == FrameLength;
}
finally { HidD_FreePreparsedData(pp); }
}
diff --git a/src/SystemIntegration/TouchpadHapticsProtocol.cs b/src/SystemIntegration/TouchpadHapticsProtocol.cs
index 563a2a7..f4fac83 100644
--- a/src/SystemIntegration/TouchpadHapticsProtocol.cs
+++ b/src/SystemIntegration/TouchpadHapticsProtocol.cs
@@ -29,6 +29,7 @@ public static class TouchpadHapticsProtocol
public const byte CmdPressure = 0x5B; // 4 short: порог, порог×0.33, 500, 400
public const byte CmdSlide = 0x58; // 1 short: сила щелчка PulseFrame (XIC-73)
public const byte CmdHeavyPress = 0x59; // 1 short: второй щелчок прошивки на сильном нажатии (XIC-78)
+ public const byte CmdFirmware = 0x10; // 2 short (4 байта): версия прошивки, только чтение (XIC-104)
/// Значения 0x59 — ровно как шлёт PC Manager (WriteEnableHeavyPress):
/// 1 — прошивка щёлкает второй раз, когда нажатие продавлено, 2 — не щёлкает (заводское).
@@ -130,6 +131,41 @@ public static byte[] ReadRequest(byte cmd, int count)
return any ? values : null;
}
+ /// Версия прошивки из ответа на : четыре байта данных по
+ /// порядку, каждый — часть версии (2C 19 0B 00 → 44.25.11.0, XIC-103). Байты
+ /// приходят как два short little-endian — раскладываем обратно.
+ public static string FirmwareVersion(IReadOnlyList v) =>
+ $"{v[0] & 0xFF}.{v[0] >> 8}.{v[1] & 0xFF}.{v[1] >> 8}";
+
+ // ---- COL04: канал контроллера касаний BL7853 (XIC-104) ----
+
+ /// Report ID коллекции COL04 (UsagePage 0xFF00) — канал самого контроллера касаний;
+ /// COL05 — канал MCU силы и вибрации за ним.
+ public const byte ControllerReportId = 0x0E;
+ private const byte CmdControllerVersion = 0xB6;
+
+ ///
+ /// Запрос версии прошивки BL7853 — единственный кадр, который мы шлём в COL04, и он не
+ /// собирается, а взят как есть из FirmwareTouchPadTool.exe OEM (запись по смещению
+ /// 0x1C7EA: 0E 07 B4 00 00 01 00 04 B6, дальше нули). Разбор — docs/16; первым его
+ /// опубликовал gxcreator в issue #99.
+ ///
+ public static byte[] ControllerVersionRequest()
+ {
+ var f = new byte[FrameLength];
+ byte[] oem = [0x0E, 0x07, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x04, CmdControllerVersion];
+ oem.CopyTo(f, 0);
+ return f;
+ }
+
+ /// Ответ на : проверка как у OEM — ID 0x0E,
+ /// [1] = 6, [2] = эхо команды (суммы в этом канале нет); версия — байты
+ /// [7].[6].[4].[5] десятичными (3.5.13.1). null — ответ не тот.
+ public static string? ParseControllerVersion(byte[] r) =>
+ r.Length >= 8 && r[0] == ControllerReportId && r[1] == 6 && r[2] == CmdControllerVersion
+ ? $"{r[7]}.{r[6]}.{r[4]}.{r[5]}"
+ : null;
+
/// XOR байтов [3..31] плюс 1 — одна формула для кадров и ответов.
public static byte Checksum(byte[] f)
{
diff --git a/src/Ui/AppController.cs b/src/Ui/AppController.cs
index 63ab657..ffddbea 100644
--- a/src/Ui/AppController.cs
+++ b/src/Ui/AppController.cs
@@ -950,7 +950,7 @@ public async Task CheckDriversAsync()
DriverCheck = previous with { Status = DriverCheckStatus.Running };
// Task.Run: синхронная часть проверки (WMI-перечень драйверов) длится секунды — вызывающий
// должен сразу получить задачу и успеть показать «Проверяю…»
- try { DriverCheck = await Task.Run(() => OemDriverCheck.RunAsync()).ConfigureAwait(false); }
+ try { DriverCheck = await Task.Run(() => OemDriverCheck.RunAsync(CurrentFirmware)).ConfigureAwait(false); }
catch (Exception ex)
{
Log.Ex("CheckDrivers", ex);
@@ -959,6 +959,15 @@ public async Task CheckDriversAsync()
finally { Volatile.Write(ref _driverCheckRunning, 0); }
}
+ // Текущие версии прошивок (XIC-104): тачпад — контроллер касаний через COL04 и MCU силы через
+ // COL05 (в тестах _haptics нет), экран — ROM Ver контроллера панели через IGCL
+ private string? CurrentFirmware(string module) => module switch
+ {
+ OemProtocol.TouchpadFirmware when _haptics is { } h => DriverMatch.TouchpadCurrent(h.ReadControllerVersion(), h.ReadFirmwareVersion()),
+ OemProtocol.PanelFirmware => PanelFirmware.ReadRomVersion(),
+ _ => null,
+ };
+
// ---- Службы Xiaomi: MiDeviceService и MiService (XIC-87) ----
// Одна операция за раз на обе службы: остановка ждёт до ~15 с, параллельно им незачем
diff --git a/src/Ui/Settings/DriversTab.cs b/src/Ui/Settings/DriversTab.cs
index c2d00c9..7fb44ce 100644
--- a/src/Ui/Settings/DriversTab.cs
+++ b/src/Ui/Settings/DriversTab.cs
@@ -54,9 +54,12 @@ private void AddResult(SettingsToolkit ui, DriverCheckResult r)
if (r.Rows.Count == 0) return;
+ // прошивки — своей группой ниже, в обновления и сводку драйверов не попадают
+ var drivers = r.Rows.Where(x => !OemProtocol.IsFirmware(x.Module)).ToList();
+
// ---- Обновления: устройство, было → стало, пакет; один пакет — одна кнопка ----
ui.AddGroup(this, "settings.drivers.updates.group");
- var updates = r.Rows.Where(x => x.State == DriverState.Update).ToList();
+ var updates = drivers.Where(x => x.State == DriverState.Update).ToList();
var offered = new HashSet(StringComparer.OrdinalIgnoreCase);
if (updates.Count == 0)
ui.AddNote(this, "settings.drivers.updates.none");
@@ -70,24 +73,23 @@ private void AddResult(SettingsToolkit ui, DriverCheckResult r)
}
}
- // ---- Прошивки экрана и тачпада (XIC-102): только «в каталоге есть», без сравнения — текущую
- // версию читать нечем, а устройства под их hardware-id в системе нет. Имя — по коду модуля, не
- // из PnP. Предупреждение жёсткое, как у BIOS: сбой прошивки панели или тачпада — тоже ремонт ----
- var firmware = r.Rows.Where(x => x.State == DriverState.Firmware).ToList();
+ // ---- Прошивки экрана и тачпада (XIC-102/104): по строке на модуль, имя — по коду модуля, не
+ // из PnP. Тачпад сравнивается как драйвер, у экрана обе версии рядом без вывода (номера
+ // вариантов панели несопоставимы), не прочиталась — «в каталоге есть X». Предупреждение
+ // жёсткое, как у BIOS: сбой прошивки панели или тачпада — тоже ремонт ----
+ var firmware = r.Rows.Where(x => OemProtocol.IsFirmware(x.Module)).ToList();
if (firmware.Count > 0)
{
ui.AddGroup(this, "settings.drivers.firmware.group");
ui.AddNote(this, "settings.drivers.firmware.warn");
foreach (var f in firmware)
- {
- string desc = Loc.T("settings.drivers.firmware.desc", f.Catalog, PackageText(f.Package));
- Controls.Add(ui.Row(Loc.T("settings.drivers.firmware.title", ModuleName(f.Module)), desc, DownloadButton(ui, f.Package, offered)));
- }
+ Controls.Add(ui.Row(Loc.T("settings.drivers.firmware.title", ModuleName(f.Module)),
+ FirmwareText(f), FirmwareControl(ui, f, offered)));
}
// ---- Сводка по модулям: под сотню строк «совпадает» читать незачем ----
ui.AddGroup(this, "settings.drivers.summary.group");
- foreach (var g in r.Rows.Where(x => x.State != DriverState.Firmware).GroupBy(x => x.Module).OrderBy(g => ModuleName(g.Key), StringComparer.CurrentCulture))
+ foreach (var g in drivers.GroupBy(x => x.Module).OrderBy(g => ModuleName(g.Key), StringComparer.CurrentCulture))
{
var parts = new List();
int up = g.Count(x => x.State == DriverState.Update), same = g.Count(x => x.State == DriverState.Same), newer = g.Count(x => x.State == DriverState.Newer);
@@ -98,9 +100,39 @@ private void AddResult(SettingsToolkit ui, DriverCheckResult r)
if (up > 0) value.ForeColor = ui.T.Accent;
Controls.Add(ui.Row(ModuleName(g.Key), g.First().Device, value));
}
- if (r.Rows.Any(x => x.State == DriverState.Newer)) ui.AddNote(this, "settings.drivers.summary.note");
+ if (drivers.Any(x => x.State == DriverState.Newer)) ui.AddNote(this, "settings.drivers.summary.note");
+ }
+
+ // у тачпада две прошивки в одной строке — объясняем, где какая
+ private static string FirmwareText(DriverRow f) =>
+ f.Module == OemProtocol.TouchpadFirmware && f.Catalog.Contains(DriverMatch.PartSeparator, StringComparison.Ordinal)
+ ? FirmwareVerdict(f) + "\n" + Loc.T("settings.drivers.firmware.tp.parts")
+ : FirmwareVerdict(f);
+
+ private static string FirmwareVerdict(DriverRow f)
+ {
+ string pkg = PackageText(f.Package);
+ return f.State switch
+ {
+ DriverState.Update => Loc.T("settings.drivers.update.desc", f.Installed ?? "—", f.Catalog, pkg),
+ DriverState.Same => Loc.T("settings.drivers.firmware.same", f.Catalog, pkg),
+ DriverState.Newer => Loc.T("settings.drivers.firmware.newer", f.Installed ?? "—", f.Catalog, pkg),
+ // экран: версия прочиталась, но сравнивать её с каталожной нельзя
+ _ when f.Installed is { } current => Loc.T("settings.drivers.firmware.side", current, f.Catalog, pkg),
+ _ when f.Module == OemProtocol.PanelFirmware => Loc.T("settings.drivers.firmware.unknown.panel", f.Catalog, pkg),
+ _ => Loc.T("settings.drivers.firmware.desc", f.Catalog, pkg),
+ };
}
+ // Ссылка — где пакет может пригодиться; где стоит та же или новее — только вердикт
+ private static Control FirmwareControl(SettingsToolkit ui, DriverRow f, HashSet offered) =>
+ f.State switch
+ {
+ DriverState.Same => ui.ValueLabel(Loc.T("settings.drivers.firmware.v.same")),
+ DriverState.Newer => ui.ValueLabel(Loc.T("settings.drivers.firmware.v.newer")),
+ _ => DownloadButton(ui, f.Package, offered),
+ };
+
///
/// Службы Xiaomi MiDeviceService и MiService (XIC-87, docs/17): состояние и «Отключить / Включить»
/// в два нажатия. Здесь, а не в «О программе»: MiDeviceService — та самая служба, что проверяет
diff --git a/tests/XiControl.Tests/OemDriversTests.cs b/tests/XiControl.Tests/OemDriversTests.cs
index e421fc4..57398b4 100644
--- a/tests/XiControl.Tests/OemDriversTests.cs
+++ b/tests/XiControl.Tests/OemDriversTests.cs
@@ -202,12 +202,92 @@ private static OemModule Module(string name, params (string Id, string Version)[
rows.Should().AllSatisfy(r =>
{
r.State.Should().Be(DriverState.Firmware);
- r.Installed.Should().BeNull("текущую версию прошивки читать нечем");
+ r.Installed.Should().BeNull("текущие версии не передавали");
r.Device.Should().Be(r.Module, "имя устройства даёт UI по коду модуля, не PnP");
r.Package.Should().NotBeNull();
});
rows.Single(r => r.Module == "FWSC").Catalog.Should().Be("740B08");
- rows.Single(r => r.Module == "FWTP").Catalog.Should().Be("3.5.13.01.44.25.13.0");
+ rows.Single(r => r.Module == "FWTP").Catalog.Should().Be("44.25.13.0", "у тачпада показываем версию прошивки Force-MCU — её тачпад и отдаёт");
+ }
+
+ // ---- текущие версии прошивок (XIC-104) ----
+
+ [Theory]
+ [InlineData("3.5.13.01.44.25.13.0", "3.5.13.1", "44.25.13.0")] // контроллер — без ведущего нуля, как отдаёт сам
+ [InlineData("44.25.13.0", null, "44.25.13.0")]
+ public void Каталожная_версия_тачпада_по_частям(string catalog, string? controller, string force) =>
+ DriverMatch.TouchpadCatalogParts(catalog).Should().Be((controller, force));
+
+ [Theory]
+ [InlineData("44.25.13")]
+ [InlineData("3.5.13.01.44.25.13.x")]
+ [InlineData("1.3.5.13.01.44.25.13.0")]
+ [InlineData("740B08")]
+ [InlineData("")]
+ public void Чужая_каталожная_версия_тачпада_не_разбирается(string catalog) =>
+ DriverMatch.TouchpadCatalogParts(catalog).Should().BeNull();
+
+ [Theory]
+ [InlineData("3.5.13.1", "44.25.11.0", "3.5.13.1 / 44.25.11.0")]
+ [InlineData(null, "44.25.11.0", "44.25.11.0")]
+ [InlineData("3.5.13.1", null, null)]
+ [InlineData(null, null, null)]
+ public void Текущая_версия_тачпада_одной_строкой(string? controller, string? force, string? expected) =>
+ DriverMatch.TouchpadCurrent(controller, force).Should().Be(expected);
+
+ [Theory]
+ [InlineData("3.5.13.1", "44.25.11.0", DriverState.Update)] // TM2424 на момент XIC-104: сила старее
+ [InlineData("3.5.12.1", "44.25.13.0", DriverState.Update)] // старее только контроллер
+ [InlineData("3.5.13.1", "44.25.13.0", DriverState.Same)] // машина автора issue #99
+ [InlineData("3.5.14.1", "44.25.13.0", DriverState.Newer)]
+ [InlineData("3.5.14.1", "44.25.12.0", DriverState.Update)] // хоть одна старее — обновление
+ public void Тачпад_сверяется_по_обеим_прошивкам(string controller, string force, DriverState state)
+ {
+ var row = DriverMatch.Compare([Touchpad], [],
+ new Dictionary { ["FWTP"] = DriverMatch.TouchpadCurrent(controller, force)! }).Single();
+
+ row.State.Should().Be(state);
+ row.Installed.Should().Be($"{controller} / {force}");
+ row.Catalog.Should().Be("3.5.13.1 / 44.25.13.0");
+ }
+
+ private static readonly OemModule Touchpad = Module("FWTP", (@"USB\VID_6989&PID_1234&REV_1836", "3.5.13.01.44.25.13.0"));
+ private static readonly OemModule Panel = Module("FWSC", (@"USB\VID_6789&PID_1234&REV_1236", "740B08"));
+
+ [Theory]
+ [InlineData("44.25.11.0", DriverState.Update)] // TM2424 на момент XIC-103
+ [InlineData("44.25.13.0", DriverState.Same)]
+ [InlineData("44.25.14.0", DriverState.Newer)]
+ public void Тачпад_с_прочитанной_версией_сравнивается_как_драйвер(string current, DriverState state)
+ {
+ var row = DriverMatch.Compare([Touchpad], [], new Dictionary { ["FWTP"] = current }).Single();
+
+ row.State.Should().Be(state);
+ row.Installed.Should().Be(current);
+ row.Catalog.Should().Be("44.25.13.0");
+ row.Package.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Экран_с_прочитанной_версией_только_рядом_без_вывода()
+ {
+ var row = DriverMatch.Compare([Panel], [], new Dictionary { ["FWSC"] = "570B06" }).Single();
+
+ row.State.Should().Be(DriverState.Firmware, "старший байт ROM Ver отличается — может быть другой вариант панели");
+ row.Installed.Should().Be("570B06");
+ row.Catalog.Should().Be("740B08");
+ }
+
+ [Fact]
+ public void Не_прочиталась_версия_остаётся_в_каталоге_есть()
+ {
+ var rows = DriverMatch.Compare([Touchpad, Panel], [], new Dictionary());
+
+ rows.Should().HaveCount(2).And.AllSatisfy(r =>
+ {
+ r.State.Should().Be(DriverState.Firmware);
+ r.Installed.Should().BeNull();
+ });
}
[Fact]
diff --git a/tests/XiControl.Tests/PanelFirmwareProtocolTests.cs b/tests/XiControl.Tests/PanelFirmwareProtocolTests.cs
new file mode 100644
index 0000000..8736a40
--- /dev/null
+++ b/tests/XiControl.Tests/PanelFirmwareProtocolTests.cs
@@ -0,0 +1,77 @@
+using FluentAssertions;
+using XiControl.SystemIntegration;
+using Xunit;
+using static XiControl.SystemIntegration.PanelFirmwareProtocol;
+
+namespace XiControl.Tests;
+
+///
+/// Версия прошивки контроллера панели (XIC-104). Эталон — последовательность из
+/// IGCLCSharpDLL.dll OEM (docs/16): записи в контроллер должны совпадать с ней байт в байт,
+/// ничего сверх. Сам IGCL — глазами, за ноутбуком (по RDP панели не видно).
+///
+public sealed class PanelFirmwareProtocolTests
+{
+ private static byte[] Hex(string s) => Convert.FromHexString(s.Replace(" ", ""));
+
+ [Fact]
+ public void Чтение_регистра_повторяет_OEM_байт_в_байт()
+ {
+ var ops = RegisterRead(RegRomVersion);
+
+ ops.Select(o => (o.Op, o.Flags, o.Address, o.Size)).Should().Equal(
+ (OpWrite, FlagI2c, 0xF2u, 2),
+ (OpWrite, FlagI2c, 0xF2u, 2),
+ (OpWrite, FlagI2cMot, 0xF2u, 10),
+ (OpRead, FlagI2c, 0xF2u, 4));
+ ops[0].Data.Should().Equal(Hex("0D 00"));
+ ops[1].Data.Should().Equal(Hex("20 01"));
+ ops[2].Data.Should().Equal(Hex("00 10 02 80 E7 00 00 00 00 00"), "адрес регистра — little-endian со второго байта");
+ ops[3].Data.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void Опознание_только_по_OUI_Himax()
+ {
+ ReadOui().Should().Match(o => o.Op == OpRead && o.Flags == FlagNative && o.Address == 0x400 && o.Size == 3);
+ IsHimax(Hex("94 0B D5")).Should().BeTrue();
+ IsHimax(Hex("00 1C F8")).Should().BeFalse();
+ IsHimax(Hex("94 0B")).Should().BeFalse();
+ }
+
+ [Fact]
+ public void Аргументы_по_раскладке_ctl_aux_access_args_t()
+ {
+ var a = Args(RegisterRead(RegRomVersion)[2]);
+
+ a.Should().HaveCount(0xB0);
+ a[0x00..0x04].Should().Equal(Hex("B0 00 00 00")); // Size
+ a[0x08..0x0C].Should().Equal(Hex("02 00 00 00")); // OpType: запись
+ a[0x0C..0x10].Should().Equal(Hex("04 00 00 00")); // Flags: I2C MOT
+ a[0x10..0x14].Should().Equal(Hex("F2 00 00 00")); // Address
+ a[0x24..0x28].Should().Equal(Hex("0A 00 00 00")); // DataSize
+ a[0x28..0x32].Should().Equal(Hex("00 10 02 80 E7 00 00 00 00 00"));
+ a[0x32..].Should().OnlyContain(b => b == 0);
+ }
+
+ [Fact]
+ public void Ответ_берётся_по_DataSize()
+ {
+ var a = Args(new AuxOp(OpRead, FlagI2c, HimaxI2c, [], 4));
+ Hex("06 0B 57 00").CopyTo(a, 0x28);
+
+ Result(a).Should().Equal(Hex("06 0B 57 00"));
+ a[0x24] = 0;
+ Result(a).Should().BeEmpty();
+ a[0x24] = 200; // больше буфера — мусор, а не данные
+ Result(a).Should().BeEmpty();
+ }
+
+ [Theory]
+ [InlineData("06 0B 57 00", "570B06")] // TM2424 на момент XIC-103
+ [InlineData("08 0B 74 00", "740B08")] // в формате каталога
+ [InlineData("00 00 00 00", null)]
+ [InlineData("06 0B", null)]
+ public void ROM_Ver_в_формате_каталога(string bytes, string? expected) =>
+ RomVersion(Hex(bytes)).Should().Be(expected);
+}
diff --git a/tests/XiControl.Tests/TouchpadHapticsProtocolTests.cs b/tests/XiControl.Tests/TouchpadHapticsProtocolTests.cs
index c1c882d..22c0d98 100644
--- a/tests/XiControl.Tests/TouchpadHapticsProtocolTests.cs
+++ b/tests/XiControl.Tests/TouchpadHapticsProtocolTests.cs
@@ -85,6 +85,28 @@ public void Parses_live_responses()
ParseResponse(Padded("0D 06 3F 00 2C 19 0B 00"), 2).Should().Equal(0x192C, 0x000B);
}
+ // версия прошивки (XIC-104): живой ответ на 0x10 — тот же 44.25.11.0, что в логе PC Manager
+ [Fact]
+ public void Firmware_version_from_live_response()
+ {
+ ReadRequest(CmdFirmware, 2).Should().Equal(Padded("0D 07 15 01 00 01 00 04 10"));
+ FirmwareVersion(ParseResponse(Padded("0D 06 3F 00 2C 19 0B 00"), 2)!).Should().Be("44.25.11.0");
+ FirmwareVersion([0x192C, 0x000D]).Should().Be("44.25.13.0"); // каталожная — для сверки формата
+ }
+
+ // версия контроллера касаний через COL04 (XIC-104): кадр OEM как есть — из FirmwareTouchPadTool.exe
+ [Fact]
+ public void Controller_version_request_is_OEM_frame() =>
+ ControllerVersionRequest().Should().Equal(Padded("0E 07 B4 00 00 01 00 04 B6"));
+
+ [Theory]
+ [InlineData("0E 06 B6 00 0D 01 05 03", "3.5.13.1")] // [7].[6].[4].[5], как печатает OEM
+ [InlineData("0E 06 B5 00 0D 01 05 03", null)] // не эхо нашей команды
+ [InlineData("0E 05 B6 00 0D 01 05 03", null)] // не тот размер
+ [InlineData("0D 06 B6 00 0D 01 05 03", null)] // ответ COL05, не COL04
+ public void Controller_version_parsed_as_OEM(string response, string? expected) =>
+ ParseControllerVersion(Padded(response)).Should().Be(expected);
+
[Fact]
public void Empty_response_means_not_ready()
{