Поддержка MacOS (проверено в Tahoe)
This commit is contained in:
@@ -15,15 +15,15 @@
|
|||||||
README.md — обзор проекта: возможности, карта документов, быстрый старт, лицензия, предостережение.
|
README.md — обзор проекта: возможности, карта документов, быстрый старт, лицензия, предостережение.
|
||||||
|
|
||||||
Тематические документы лежат в docs/:
|
Тематические документы лежат в docs/:
|
||||||
* INSTALL.md - установка
|
* install.md - установка
|
||||||
* COLOR-MODES.md - режимы
|
* color-modes.md - режимы
|
||||||
* cli/ - команды (обзор и карта файлов — в cli/README.md)
|
* cli/ - команды (обзор и карта файлов — в cli/README.md)
|
||||||
* EXTENDING.md - расширение
|
* extending.md - расширение
|
||||||
* PROTOCOL.md - протокол
|
* protocol.md - протокол
|
||||||
* SCRIPTS.md - скрипты
|
* scripts.md - скрипты
|
||||||
* RAW-DATA.md - дампы
|
* raw-data.md - дампы
|
||||||
|
|
||||||
Пользовательские вопросы — в тематических документах, реверс-инжиниринг — в PROTOCOL.md.
|
Пользовательские вопросы — в тематических документах, реверс-инжиниринг — в protocol.md.
|
||||||
|
|
||||||
Ссылки между документами — относительные, без якорей на заголовки с кириллицей.
|
Ссылки между документами — относительные, без якорей на заголовки с кириллицей.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
Подсветка и клавиши управляются HID feature-репортами через штатный драйвер `usbhid` — **печатать можно прямо во время настройки**, драйвер не отцепляется, root не нужен (после разовой установки udev-правила).
|
Подсветка и клавиши управляются HID feature-репортами через штатный драйвер `usbhid` — **печатать можно прямо во время настройки**, драйвер не отцепляется, root не нужен (после разовой установки udev-правила).
|
||||||
|
|
||||||
|
macOS поддерживается экспериментально (транспорт IOKit, см. [docs/install.md](docs/install.md) → «Установка на macOS»).
|
||||||
|
|
||||||
| Характеристика | Значение |
|
| Характеристика | Значение |
|
||||||
| -------------- | ------------------------------------------------------ |
|
| -------------- | ------------------------------------------------------ |
|
||||||
| Устройство | GANSS ARDOR_Katana |
|
| Устройство | GANSS ARDOR_Katana |
|
||||||
@@ -22,11 +24,11 @@
|
|||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
```
|
```
|
||||||
# 1. установка (подробности — docs/INSTALL.md)
|
# 1. установка (подробности — docs/install.md)
|
||||||
python3 -m venv .venv && .venv/bin/pip install pyusb
|
python3 -m venv .venv && .venv/bin/pip install pyusb
|
||||||
sudo cp 70-ganss-katana.rules /etc/udev/rules.d/ && sudo udevadm trigger
|
sudo cp 70-ganss-katana.rules /etc/udev/rules.d/ && sudo udevadm trigger
|
||||||
|
|
||||||
# 2. подсветка (подробности — docs/COLOR-MODES.md)
|
# 2. подсветка (подробности — docs/color-modes.md)
|
||||||
./katana.py mode breathing --color red # дыхание красным
|
./katana.py mode breathing --color red # дыхание красным
|
||||||
./katana.py paint --black --wasd dodgerblue # чёрная база, WASD голубым
|
./katana.py paint --black --wasd dodgerblue # чёрная база, WASD голубым
|
||||||
./katana.py keys # посмотреть, что сейчас светится
|
./katana.py keys # посмотреть, что сейчас светится
|
||||||
@@ -43,7 +45,7 @@ sudo cp 70-ganss-katana.rules /etc/udev/rules.d/ && sudo udevadm trigger
|
|||||||
./katana.py apply ~/my-katana.yaml # макросы + переназначение + подсветка
|
./katana.py apply ~/my-katana.yaml # макросы + переназначение + подсветка
|
||||||
```
|
```
|
||||||
|
|
||||||
Если что-то не работает — загляните в [docs/INSTALL.md](docs/INSTALL.md) (разделы «Проверка устройства» и решение проблем) и в справочник команд [docs/cli/README.md](docs/cli/README.md).
|
Если что-то не работает — загляните в [docs/install.md](docs/install.md) (разделы «Проверка устройства» и решение проблем) и в справочник команд [docs/cli/README.md](docs/cli/README.md).
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
@@ -51,16 +53,16 @@ sudo cp 70-ganss-katana.rules /etc/udev/rules.d/ && sudo udevadm trigger
|
|||||||
|
|
||||||
| Документ | Тема |
|
| Документ | Тема |
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| [docs/INSTALL.md](docs/INSTALL.md) | Установка: зависимости, venv, udev-правило, проверка устройства, решение проблем. |
|
| [docs/install.md](docs/install.md) | Установка: зависимости, venv, udev-правило, проверка устройства, решение проблем. |
|
||||||
| [docs/COLOR-MODES.md](docs/COLOR-MODES.md) | 19 режимов подсветки: таблица, примеры `mode`, аргументы, направление анимации. |
|
| [docs/color-modes.md](docs/color-modes.md) | 19 режимов подсветки: таблица, примеры `mode`, аргументы, направление анимации. |
|
||||||
| [docs/cli/README.md](docs/cli/README.md) | Справочник `katana.py`: именованные цвета, имена клавиш, `paint`/`scan`/`raw`/`remap`/`macro`/`keys`/`reset`, решение проблем. |
|
| [docs/cli/README.md](docs/cli/README.md) | Справочник `katana.py`: именованные цвета, имена клавиш, `paint`/`scan`/`raw`/`remap`/`macro`/`keys`/`reset`, решение проблем. |
|
||||||
| [docs/cli/remap.md](docs/cli/remap.md) | Переназначение клавиш: мышь, редактор, горячие клавиши, мультимедиа, макросы. |
|
| [docs/cli/remap.md](docs/cli/remap.md) | Переназначение клавиш: мышь, редактор, горячие клавиши, мультимедиа, макросы. |
|
||||||
| [docs/cli/macro.md](docs/cli/macro.md) | Макросы: запись, режимы повтора, XML вендора. |
|
| [docs/cli/macro.md](docs/cli/macro.md) | Макросы: запись, режимы повтора, XML вендора. |
|
||||||
| [docs/cli/apply.md](docs/cli/apply.md) | `apply` — YAML-конфиг: все настройки одним файлом. |
|
| [docs/cli/apply.md](docs/cli/apply.md) | `apply` — YAML-конфиг: все настройки одним файлом. |
|
||||||
| [docs/EXTENDING.md](docs/EXTENDING.md) | Свои скрипты и анимации: `katana.py` как библиотека, API `Katana`, ограничения скорости и флеша, готовые приёмы. |
|
| [docs/extending.md](docs/extending.md) | Свои скрипты и анимации: `katana.py` как библиотека, API `Katana`, ограничения скорости и флеша, готовые приёмы. |
|
||||||
| [docs/PROTOCOL.md](docs/PROTOCOL.md) | Протокол для реверсеров: транспорт, транзакции, форматы payload'ов, карта клавиш, статус реверса, грабли, планы. |
|
| [docs/protocol.md](docs/protocol.md) | Протокол для реверсеров: транспорт, транзакции, форматы payload'ов, карта клавиш, статус реверса, грабли, планы. |
|
||||||
| [docs/SCRIPTS.md](docs/SCRIPTS.md) | Состав репозитория и вспомогательные скрипты: `keymap.py`, `analyze_pcap.py`, `probe.py`. |
|
| [docs/scripts.md](docs/scripts.md) | Состав репозитория и вспомогательные скрипты: `keymap.py`, `analyze_pcap.py`, `misc/probe.py`. |
|
||||||
| [docs/RAW-DATA.md](docs/RAW-DATA.md) | Дампы USB-трафика вендорской утилиты: как снимались, как конвертировать, состав `raw-data/katana-v1/`. |
|
| [docs/raw-data.md](docs/raw-data.md) | Дампы USB-трафика вендорской утилиты: как снимались, как конвертировать, состав `raw-data/katana-v1/`. |
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
* AI-модель `koda-pro` через [KodaCode](https://kodacode.ru)
|
* AI-модель `koda-pro` через [KodaCode](https://kodacode.ru)
|
||||||
* python 3.10 + pyusb + pyyaml
|
* python 3.10 + pyusb + pyyaml
|
||||||
|
* Linux (hidraw-ioctl) и macOS (IOKit через ctypes, экспериментально)
|
||||||
* VirtualBox + Windows 11:
|
* VirtualBox + Windows 11:
|
||||||
* штатная утилита конфигурации клавиатуры
|
* штатная утилита конфигурации клавиатуры
|
||||||
* WireShark с установленным `usbpcap`
|
* WireShark с установленным `usbpcap`
|
||||||
@@ -108,7 +111,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* переназначение на другие типы действий (Fn-слой) и чтение таблицы переназначений — мышиные действия, шорткаты редактора, горячие клавиши, мультимедиа и макросы реализованы ([docs/cli/remap.md](docs/cli/remap.md), [docs/cli/macro.md](docs/cli/macro.md))
|
* переназначение на другие типы действий (Fn-слой) и чтение таблицы переназначений — мышиные действия, шорткаты редактора, горячие клавиши, мультимедиа и макросы реализованы ([docs/cli/remap.md](docs/cli/remap.md), [docs/cli/macro.md](docs/cli/macro.md))
|
||||||
* макросы: чтение содержимого из прошивки, коллизия кодов громкости и букв B/C, стрелки Up/Down, остальные медиа-коды ([docs/PROTOCOL.md](docs/PROTOCOL.md) → «Макросы»)
|
* макросы: чтение содержимого из прошивки, коллизия кодов громкости и букв B/C, стрелки Up/Down, остальные медиа-коды ([docs/protocol.md](docs/protocol.md) → «Макросы»)
|
||||||
* пресеты/анимации поверх per-key API — методика описана в [docs/EXTENDING.md](docs/EXTENDING.md).
|
* пресеты/анимации поверх per-key API — методика описана в [docs/extending.md](docs/extending.md).
|
||||||
|
|
||||||
Cм. [docs/PROTOCOL.md](docs/PROTOCOL.md) → «Следующие шаги»
|
Cм. [docs/protocol.md](docs/protocol.md) → «Следующие шаги»
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
19 встроенных режимов прошивки плюс выключение.
|
19 встроенных режимов прошивки плюс выключение.
|
||||||
Номер режима = байт [0] lighting-payload, алиасы заданы в `katana.py` (`MODES`).
|
Номер режима = байт [0] lighting-payload, алиасы заданы в `katana.py` (`MODES`).
|
||||||
Полная таблица с байтами payload — в [PROTOCOL.md](PROTOCOL.md) (раздел «Lighting payload»).
|
Полная таблица с байтами payload — в [protocol.md](protocol.md) (раздел «Lighting payload»).
|
||||||
|
|
||||||
## Таблица режимов
|
## Таблица режимов
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
| 19 | Шаттл | `shuttle` | моно/спектр | + | + | | бегущие строки по чётным/нечётным рядам в обе стороны |
|
| 19 | Шаттл | `shuttle` | моно/спектр | + | + | | бегущие строки по чётным/нечётным рядам в обе стороны |
|
||||||
| 20 | Выключена | `off` | | | | | все клавиши погашены |
|
| 20 | Выключена | `off` | | | | | все клавиши погашены |
|
||||||
|
|
||||||
¹ У breathing цвет из payload игнорируется прошивкой: цикл цвета зашит и гоняет зелёно-жёлто-красную гамму при любом `--color` (проверено на железе, payload совпадает с вендорским — подробности в [PROTOCOL.md](PROTOCOL.md)).
|
¹ У breathing цвет из payload игнорируется прошивкой: цикл цвета зашит и гоняет зелёно-жёлто-красную гамму при любом `--color` (проверено на железе, payload совпадает с вендорским — подробности в [protocol.md](protocol.md)).
|
||||||
Для моноцветного дыхания аналога нет; если нужен именно красный постоянный свет — используйте `static`.
|
Для моноцветного дыхания аналога нет; если нужен именно красный постоянный свет — используйте `static`.
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
@@ -99,4 +99,4 @@
|
|||||||
Задаётся `--direction north-south|south-north|east-west|west-east` (байт [11] payload).
|
Задаётся `--direction north-south|south-north|east-west|west-east` (байт [11] payload).
|
||||||
Значения сняты для `waterfall` (02/03) и `marquee` (01/00).
|
Значения сняты для `waterfall` (02/03) и `marquee` (01/00).
|
||||||
Для `flow`, `rotate`, `wind` применяйте те же опции — значения байта уточняются.
|
Для `flow`, `rotate`, `wind` применяйте те же опции — значения байта уточняются.
|
||||||
Подробности — [PROTOCOL.md](PROTOCOL.md) (раздел «Байт [11] — подрежим»).
|
Подробности — [protocol.md](protocol.md) (раздел «Байт [11] — подрежим»).
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
Модуль спроектирован как библиотека: класс `Katana` и вспомогательные функции импортируются в любой ваш скрипт одной строкой.
|
Модуль спроектирован как библиотека: класс `Katana` и вспомогательные функции импортируются в любой ваш скрипт одной строкой.
|
||||||
|
|
||||||
Документ описывает программный интерфейс, его ограничения и готовые приёмы.
|
Документ описывает программный интерфейс, его ограничения и готовые приёмы.
|
||||||
Справка по CLI-командам — в [cli/README.md](cli/README.md), низкоуровневые детали протокола — в [PROTOCOL.md](PROTOCOL.md).
|
Справка по CLI-командам — в [cli/README.md](cli/README.md), низкоуровневые детали протокола — в [protocol.md](protocol.md).
|
||||||
|
|
||||||
## Что даёт per-key API
|
## Что даёт per-key API
|
||||||
|
|
||||||
@@ -225,4 +225,4 @@ if __name__ == "__main__":
|
|||||||
- визуализация музыки через любой FFT-вход: спектр по рядам клавиатуры;
|
- визуализация музыки через любой FFT-вход: спектр по рядам клавиатуры;
|
||||||
- световой «миникарт»: подсветка зоны WASD в играх, где это уместно.
|
- световой «миникарт»: подсветка зоны WASD в играх, где это уместно.
|
||||||
|
|
||||||
Список открытых направлений самого протокола (переназначение клавиш, байты направлений) — в [PROTOCOL.md](PROTOCOL.md) → «Следующие шаги».
|
Список открытых направлений самого протокола (переназначение клавиш, байты направлений) — в [protocol.md](protocol.md) → «Следующие шаги».
|
||||||
|
|||||||
+25
-1
@@ -7,7 +7,7 @@ description: Установка, udev-правило и решение проб
|
|||||||
## Зависимости
|
## Зависимости
|
||||||
|
|
||||||
Нужен Python 3.10+.
|
Нужен Python 3.10+.
|
||||||
Зависимости: `pyusb` (для `reset`) и `pyyaml` (для `apply`); сам транспорт работает на голых `fcntl`-ioctl'ах к `/dev/hidraw*`:
|
Зависимости: `pyusb` (для `reset`) и `pyyaml` (для `apply`); сам транспорт работает на голых `fcntl`-ioctl'ах к `/dev/hidraw*` (на Linux) или через hidapi (на macOS, см. ниже):
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
@@ -54,3 +54,27 @@ ID `0c45:8006` принадлежит чипу Sonix SN32, который про
|
|||||||
|
|
||||||
Переподключите клавиатуру.
|
Переподключите клавиатуру.
|
||||||
Если пользователь не в группе `plugdev` — `sudo usermod -aG plugdev $USER` и перелогин.
|
Если пользователь не в группе `plugdev` — `sudo usermod -aG plugdev $USER` и перелогин.
|
||||||
|
|
||||||
|
## Установка на macOS
|
||||||
|
|
||||||
|
Экспериментальная поддержка: транспорт работает через IOKit напрямую (ctypes),
|
||||||
|
сторонние HID-библиотеки не нужны, udev-правило не нужно.
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install pyusb pyyaml
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS выдаёт доступ к HID-клавиатурам вручную:
|
||||||
|
**Системные настройки → Конфиденциальность и безопасность → «Input Monitoring»** —
|
||||||
|
добавьте приложение, из которого запускается `katana.py` (Terminal, iTerm2, VS Code),
|
||||||
|
включите его и **перезапустите приложение** (разрешение подхватывается только новым процессом).
|
||||||
|
Без разрешения `katana.py` выведет подсказку об этом.
|
||||||
|
|
||||||
|
Важно: утилиты, перехватывающие клавиатуры (Karabiner-Elements и подобные),
|
||||||
|
открывают их в монопольном режиме — пока Karabiner работает, открыть устройство нельзя
|
||||||
|
(ошибка `kIOReturnExclusiveAccess`). Выйдите из Karabiner на время настройки
|
||||||
|
или исключите ARDOR_Katana в его настройках (вкладка Devices).
|
||||||
|
Проверить, кто мешает, можно диагностикой `misc/iokit_probe.py` (см. [scripts.md](scripts.md)).
|
||||||
|
|
||||||
|
Транспорт выбирается автоматически: на macOS — IOKit, на Linux — hidraw-ioctl.
|
||||||
|
|||||||
+26
-2
@@ -18,6 +18,30 @@ Feature-репорты через hidraw ioctl (`HIDIOCSFEATURE`/`HIDIOCGFEATURE
|
|||||||
**usbhid не отцепляется** — печать никогда не прерывается.
|
**usbhid не отцепляется** — печать никогда не прерывается.
|
||||||
pyusb-вариант НЕ использовать (отцеплял драйвер).
|
pyusb-вариант НЕ использовать (отцеплял драйвер).
|
||||||
|
|
||||||
|
### macOS (экспериментально, класс `KatanaMacOS`)
|
||||||
|
|
||||||
|
Тот же протокол поверх IOKit напрямую (ctypes, без сторонних библиотек):
|
||||||
|
`IOHIDDeviceSetReport` / `IOHIDDeviceGetReport` с `kIOHIDReportTypeFeature` —
|
||||||
|
семантика 1:1 с hidraw, feature-репорты rid=0, 64 байта данных.
|
||||||
|
Устройство ищется через `IOServiceGetMatchingServices("IOHIDDevice")` + `IOHIDDeviceGetProperty`:
|
||||||
|
выбирается клавиатура 0C45:8006 с PrimaryUsagePage=1, PrimaryUsage=6 (boot keyboard = интерфейс 0).
|
||||||
|
Выбор транспорта автоматический по `sys.platform` (`open_katana()`): darwin → IOKit, остальное → hidraw.
|
||||||
|
|
||||||
|
Грабли macOS (все проверены на железе):
|
||||||
|
- у IOKit своя нумерация типов репортов: Input=0, Output=1, **Feature=2**;
|
||||||
|
с Output прошивка отвечает мусором (1 байт `04`);
|
||||||
|
- `IOServiceMatching` возвращает CFDictionaryRef — без явного `restype = c_void_p`
|
||||||
|
ctypes обрезает указатель и процесс падает segfault'ом;
|
||||||
|
- `IOHIDDeviceGetProperty` принимает IOHIDDeviceRef (от `IOHIDDeviceCreate`),
|
||||||
|
а не registry entry — с entry процесс падает segfault'ом;
|
||||||
|
плоских `idVendor` в свойствах реестра нет — только через GetProperty;
|
||||||
|
- доступ к HID-клавиатурам выдаётся вручную: Системные настройки → Конфиденциальность и безопасность → «Input Monitoring» для приложения-терминала, затем перезапуск терминала;
|
||||||
|
- Karabiner-Elements (и подобные утилиты) захватывает клавиатуры монопольно — пока он работает, `IOHIDDeviceOpen` возвращает `kIOReturnExclusiveAccess` (0xE00002C5); проверено диагностикой `misc/iokit_probe.py`;
|
||||||
|
- `reset` (pyusb) на macOS работает (проверено: USB port reset проходит);
|
||||||
|
- udev-правило не нужно.
|
||||||
|
|
||||||
|
Проверка на железе (macOS, Karabiner выключен): mode/paint/keys/remap/macro/reset — работают.
|
||||||
|
|
||||||
## Общая транзакция (все команды)
|
## Общая транзакция (все команды)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -65,7 +89,7 @@ CLI: `--rainbow`, `--direction north-south|south-north|east-west|west-east`.
|
|||||||
- Яркость масштабирует per-key цвета: 15/15 → `ee` (238), 1/15 → `0f`.
|
- Яркость масштабирует per-key цвета: 15/15 → `ee` (238), 1/15 → `0f`.
|
||||||
- Режим 0x80 отображает таблицу per-key (см. ниже) — это и есть «кастом».
|
- Режим 0x80 отображает таблицу per-key (см. ниже) — это и есть «кастом».
|
||||||
|
|
||||||
### Таблица режимов (описания вендорской утилиты, docs/COLOR-MODES.md)
|
### Таблица режимов (описания вендорской утилиты, docs/color-modes.md)
|
||||||
|
|
||||||
Номер = байт [0] payload.
|
Номер = байт [0] payload.
|
||||||
«моно/спектр» — режим рисует одиночный цвет или полный спектр независимо от него.
|
«моно/спектр» — режим рисует одиночный цвет или полный спектр независимо от него.
|
||||||
@@ -535,7 +559,7 @@ ACTION: мышь (`lmb`/`mouse1`, `rmb`/`mouse2`, `mmb`/`mouse3`, `back`/`mouseb
|
|||||||
Шорткаты редактора (тип `02`) тоже сняты и реализованы (дамп keybind-editor).
|
Шорткаты редактора (тип `02`) тоже сняты и реализованы (дамп keybind-editor).
|
||||||
Горячие клавиши (тип `02`, маска модификаторов в байте [1]) сняты по дампу keybind-hotkeys и реализованы (`HOTKEY_KEYS`/`HOTKEY_MODIFIERS`, `remap --key KEY=MOD+KEY`).
|
Горячие клавиши (тип `02`, маска модификаторов в байте [1]) сняты по дампу keybind-hotkeys и реализованы (`HOTKEY_KEYS`/`HOTKEY_MODIFIERS`, `remap --key KEY=MOD+KEY`).
|
||||||
Осталось: другие типы действий (Fn-слой) и чтение таблицы.
|
Осталось: другие типы действий (Fn-слой) и чтение таблицы.
|
||||||
5. ~~Демо-скрипты (радуга, волна) поверх per-key API + живое чтение f5~~ — ЗАВЕРШЕНО, методика описана и проверена ([EXTENDING.md](EXTENDING.md)): таблица обновляется живьём в режиме 0x80, ~1 кадр/с.
|
5. ~~Демо-скрипты (радуга, волна) поверх per-key API + живое чтение f5~~ — ЗАВЕРШЕНО, методика описана и проверена ([extending.md](extending.md)): таблица обновляется живьём в режиме 0x80, ~1 кадр/с.
|
||||||
6. ~~Макросы: запись содержимого и привязка~~ — ЗАВЕРШЕНО (канал 04 19 + записи `[06, ...]` в 04 11, команда `macro`, см. «Макросы»).
|
6. ~~Макросы: запись содержимого и привязка~~ — ЗАВЕРШЕНО (канал 04 19 + записи `[06, ...]` в 04 11, команда `macro`, см. «Макросы»).
|
||||||
Осталось: чтение содержимого, коллизия кодов 05/06, стрелки Up/Down, остальные медиа-коды.
|
Осталось: чтение содержимого, коллизия кодов 05/06, стрелки Up/Down, остальные медиа-коды.
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# Дампы GANSS ARDOR_Katana (katana-v1)
|
# Дампы GANSS ARDOR_Katana (katana-v1)
|
||||||
|
|
||||||
Сырые данные, на которых построен реверс протокола (см. [PROTOCOL.md](PROTOCOL.md)).
|
Сырые данные, на которых построен реверс протокола (см. [protocol.md](protocol.md)).
|
||||||
Устройство: клавиатура GANSS ARDOR_Katana, USB `0C45:8006` (Sonix SN32).
|
Устройство: клавиатура GANSS ARDOR_Katana, USB `0C45:8006` (Sonix SN32).
|
||||||
|
|
||||||
## Состав
|
## Состав
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
На Linux, из корня репозитория:
|
На Linux, из корня репозитория:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 analyze_pcap.py raw-data/katana-v1/color-modes.pcapng raw-data/katana-v1/color-modes.plain.txt
|
./misc/analyze_pcap.py raw-data/katana-v1/color-modes.pcapng raw-data/katana-v1/color-modes.plain.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Формат строки лога:
|
Формат строки лога:
|
||||||
@@ -82,9 +82,9 @@ usb-devices | grep -B 2 -A 8 'Vendor=0c45' > usb-devices-output.txt
|
|||||||
Конфигурационный канал — feature-репорты интерфейса 0 (эндпоинты управления, не interrupt).
|
Конфигурационный канал — feature-репорты интерфейса 0 (эндпоинты управления, не interrupt).
|
||||||
|
|
||||||
В `lsusb` устройство подписывается «Microdia Dual Mode Camera (8006 VGA)» — это норма: ID `0c45:8006` чипа Sonix SN32 используется и вебкамерами, и клавиатурами, а база `usb.ids` знает за ним только камеру.
|
В `lsusb` устройство подписывается «Microdia Dual Mode Camera (8006 VGA)» — это норма: ID `0c45:8006` чипа Sonix SN32 используется и вебкамерами, и клавиатурами, а база `usb.ids` знает за ним только камеру.
|
||||||
Подробнее — в [INSTALL.md](INSTALL.md) → «Проверка устройства».
|
Подробнее — в [install.md](install.md) → «Проверка устройства».
|
||||||
|
|
||||||
## Замечания
|
## Замечания
|
||||||
|
|
||||||
- Имена файлов оригинальные, включая опечатку `color-pesets.pcapng` (содержимое соответствует color-presets).
|
- Имена файлов оригинальные, включая опечатку `color-pesets.pcapng` (содержимое соответствует color-presets).
|
||||||
- Расшифровка протокола по этим дампам — в [PROTOCOL.md](PROTOCOL.md), парсер — `analyze_pcap.py`, рабочая утилита — `katana.py` (обзор — в [SCRIPTS.md](SCRIPTS.md)).
|
- Расшифровка протокола по этим дампам — в [protocol.md](protocol.md), парсер — `analyze_pcap.py`, рабочая утилита — `katana.py` (обзор — в [scripts.md](scripts.md)).
|
||||||
|
|||||||
+28
-10
@@ -1,18 +1,20 @@
|
|||||||
# Скрипты репозитория
|
# Скрипты репозитория
|
||||||
|
|
||||||
Состав репозитория и назначение каждого скрипта.
|
Состав репозитория и назначение каждого скрипта.
|
||||||
Использование `katana.py` как библиотеки (свои скрипты и анимации) — в [EXTENDING.md](EXTENDING.md).
|
Использование `katana.py` как библиотеки (свои скрипты и анимации) — в [extending.md](extending.md).
|
||||||
|
|
||||||
## Структура репозитория
|
## Структура репозитория
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── katana.py Основная CLI-утилита управления подсветкой
|
|
||||||
├── keymap.py Карта «LED-индекс → физическая клавиша» (104 клавиши)
|
|
||||||
├── katana.yaml.example Пример YAML-конфига для команды apply
|
|
||||||
├── analyze_pcap.py Парсер USBPcap-захватов → лог control-трансферов
|
|
||||||
├── probe.py Исследовательский зонд: чтение feature-репортов
|
|
||||||
├── 70-ganss-katana.rules udev-правило: доступ к устройству без root
|
├── 70-ganss-katana.rules udev-правило: доступ к устройству без root
|
||||||
|
├── katana.yaml.example Пример YAML-конфига для команды apply
|
||||||
|
├── katana.py Основная CLI-утилита управления подсветкой
|
||||||
|
├── misc/
|
||||||
|
│ ├── keymap.py Карта «LED-индекс → физическая клавиша» (104 клавиши)
|
||||||
|
│ ├── analyze_pcap.py Парсер USBPcap-захватов → лог control-трансферов
|
||||||
|
│ ├── probe.py Исследовательский зонд: чтение feature-репортов
|
||||||
|
│ └── iokit_probe.py Диагностика открытия HID на macOS (код IOReturn)
|
||||||
├── tests/ Офлайн-тесты (без железа)
|
├── tests/ Офлайн-тесты (без железа)
|
||||||
├── docs/ Документация (см. README.md)
|
├── docs/ Документация (см. README.md)
|
||||||
└── raw-data/
|
└── raw-data/
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
Печатает подтверждённую карту, список слепых слотов (39 шт — резерв прошивки, ни к какому LED не подключены), непроидентифицированные индексы и таблицу алиасов клавиш (`KEY_SYMBOL_ALIASES`).
|
Печатает подтверждённую карту, список слепых слотов (39 шт — резерв прошивки, ни к какому LED не подключены), непроидентифицированные индексы и таблицу алиасов клавиш (`KEY_SYMBOL_ALIASES`).
|
||||||
Методика картирования и открытые вопросы — в [PROTOCOL.md](PROTOCOL.md) (раздел «Карта клавиш»).
|
Методика картирования и открытые вопросы — в [protocol.md](protocol.md) (раздел «Карта клавиш»).
|
||||||
|
|
||||||
## analyze_pcap.py
|
## analyze_pcap.py
|
||||||
|
|
||||||
@@ -41,19 +43,34 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
Без второго аргумента пишет `/tmp/tx_log.txt`.
|
Без второго аргумента пишет `/tmp/tx_log.txt`.
|
||||||
Формат строк и методика снятия захватов — в [RAW-DATA.md](RAW-DATA.md).
|
Формат строк и методика снятия захватов — в [raw-data.md](raw-data.md).
|
||||||
|
|
||||||
## probe.py
|
## misc/probe.py
|
||||||
|
|
||||||
Исследовательский зонд времён начала реверса: перебирает report id и читает feature-репорты (только GET, состояние клавиатуры не меняет).
|
Исследовательский зонд времён начала реверса: перебирает report id и читает feature-репорты (только GET, состояние клавиатуры не меняет).
|
||||||
Полезен как минимальный пример hidraw-транспорта:
|
Полезен как минимальный пример hidraw-транспорта:
|
||||||
|
|
||||||
```
|
```
|
||||||
./probe.py [hidrawN]
|
./misc/probe.py [hidrawN]
|
||||||
```
|
```
|
||||||
|
|
||||||
Без аргумента находит все hidraw-узлы клавиатуры сам.
|
Без аргумента находит все hidraw-узлы клавиатуры сам.
|
||||||
|
|
||||||
|
## misc/iokit_probe.py
|
||||||
|
|
||||||
|
Диагностика macOS-only: открывает HID-сервис клавиатуры через IOKit напрямую
|
||||||
|
(ctypes, сторонних библиотек не нужно) и печатает точный код `IOReturn` от
|
||||||
|
`IOHIDDeviceOpen`. Полезен, когда katana.py сообщает только «open failed»:
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv/bin/python misc/iokit_probe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Интерпретация результата: `0x00000000` — права есть (проблема в софте),
|
||||||
|
`0xE00002C1` — нет разрешения «Input Monitoring»,
|
||||||
|
`0xE00002C5` (`kIOReturnExclusiveAccess`) — устройство захвачено монопольно
|
||||||
|
(типичный случай — Karabiner-Elements).
|
||||||
|
|
||||||
## tests/
|
## tests/
|
||||||
|
|
||||||
Офлайн-тесты (железо не нужно):
|
Офлайн-тесты (железо не нужно):
|
||||||
@@ -63,6 +80,7 @@ python3 tests/test_macro.py # макросы, remap-действия, св
|
|||||||
python3 tests/test_apply.py # apply: YAML-конфиг → argv команд
|
python3 tests/test_apply.py # apply: YAML-конфиг → argv команд
|
||||||
python3 tests/test_parsers.py # парсеры, payload, ACK, keymap
|
python3 tests/test_parsers.py # парсеры, payload, ACK, keymap
|
||||||
python3 tests/test_cli.py # команды CLI на подменном устройстве
|
python3 tests/test_cli.py # команды CLI на подменном устройстве
|
||||||
|
python3 tests/test_transport.py # выбор транспорта Linux/macOS, IOKit-обмен
|
||||||
```
|
```
|
||||||
|
|
||||||
`test_macro.py` — блоб, собранный из вендорских XML, сверяется побайтово с дампом macros-create, плюс разбор токенов, round-trip XML, hotkey/remap-действия и сверка с дампами keybind-editor/keybind-hotkeys/keybind-multimedia.
|
`test_macro.py` — блоб, собранный из вендорских XML, сверяется побайтово с дампом macros-create, плюс разбор токенов, round-trip XML, hotkey/remap-действия и сверка с дампами keybind-editor/keybind-hotkeys/keybind-multimedia.
|
||||||
|
|||||||
+3
-3
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
Справочник команд и аргументов.
|
Справочник команд и аргументов.
|
||||||
Все команды идут через транзакцию `begin → data → commit → save` и безопасны: печать не прерывается, прошивка остаётся живой.
|
Все команды идут через транзакцию `begin → data → commit → save` и безопасны: печать не прерывается, прошивка остаётся живой.
|
||||||
Исключение — `macro set`/`macro clear`: вендор после записи макросов save не делает (подробности в [PROTOCOL.md](../PROTOCOL.md)).
|
Исключение — `macro set`/`macro clear`: вендор после записи макросов save не делает (подробности в [protocol.md](../protocol.md)).
|
||||||
Низкоуровневое описание транзакций — в [PROTOCOL.md](../PROTOCOL.md).
|
Низкоуровневое описание транзакций — в [protocol.md](../protocol.md).
|
||||||
|
|
||||||
Режимы подсветки и их аргументы вынесены в отдельный документ: [COLOR-MODES.md](../COLOR-MODES.md).
|
Режимы подсветки и их аргументы вынесены в отдельный документ: [color-modes.md](../color-modes.md).
|
||||||
|
|
||||||
## Состав справочника
|
## Состав справочника
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
./katana.py apply katana.yaml.example # применить пример из репозитория
|
./katana.py apply katana.yaml.example # применить пример из репозитория
|
||||||
```
|
```
|
||||||
|
|
||||||
Зависимость: PyYAML (`pip install pyyaml`, см. [INSTALL.md](../INSTALL.md)).
|
Зависимость: PyYAML (`pip install pyyaml`, см. [install.md](../install.md)).
|
||||||
|
|
||||||
## Порядок применения и сохранение
|
## Порядок применения и сохранение
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ paint:
|
|||||||
brightness: 15
|
brightness: 15
|
||||||
```
|
```
|
||||||
|
|
||||||
### `lighting` — режим подсветки (см. [COLOR-MODES.md](../COLOR-MODES.md))
|
### `lighting` — режим подсветки (см. [color-modes.md](../color-modes.md))
|
||||||
|
|
||||||
`mode` — имя или номер 1..19, а также `off` и `default` (применяются как команды `off`/`default`).
|
`mode` — имя или номер 1..19, а также `off` и `default` (применяются как команды `off`/`default`).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# macro — макросы
|
# macro — макросы
|
||||||
|
|
||||||
Записывает содержимое макросов в прошивку (канал 04 19) и работает с вендорским XML.
|
Записывает содержимое макросов в прошивку (канал 04 19) и работает с вендорским XML.
|
||||||
Формат протокола — в [PROTOCOL.md](../PROTOCOL.md).
|
Формат протокола — в [protocol.md](../protocol.md).
|
||||||
|
|
||||||
Макрос нужно привязать к клавише через `remap --key КЛЮЧ=macro<N>` (индексация 0-based: «Макрос 1» вендорской утилиты = 0).
|
Макрос нужно привязать к клавише через `remap --key КЛЮЧ=macro<N>` (индексация 0-based: «Макрос 1» вендорской утилиты = 0).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ Shorthand для `--key`: покрасить горизонтальный ряд
|
|||||||
|
|
||||||
### `--no-save`
|
### `--no-save`
|
||||||
|
|
||||||
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
|
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
|
||||||
По умолчанию save выполняется.
|
По умолчанию save выполняется.
|
||||||
|
|
||||||
Без `--all` и `--key` команда завершается ошибкой.
|
Без `--all` и `--key` команда завершается ошибкой.
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ Payload до 64 байт (пробелы допустимы).
|
|||||||
|
|
||||||
### `--no-save`
|
### `--no-save`
|
||||||
|
|
||||||
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
|
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
|
||||||
По умолчанию save выполняется.
|
По умолчанию save выполняется.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -20,4 +20,4 @@ Payload до 64 байт (пробелы допустимы).
|
|||||||
Payload отправляется внутри штатной транзакции `begin → data → commit → save`.
|
Payload отправляется внутри штатной транзакции `begin → data → commit → save`.
|
||||||
Для экспериментов с неописанными байтами.
|
Для экспериментов с неописанными байтами.
|
||||||
|
|
||||||
Формат payload'ов и назначение байтов — в [PROTOCOL.md](../PROTOCOL.md).
|
Формат payload'ов и назначение байтов — в [protocol.md](../protocol.md).
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# remap — переназначение клавиш
|
# remap — переназначение клавиш
|
||||||
|
|
||||||
Переназначает клавиши на кнопки мыши, функции текстового редактора, мультимедиа/веб-действия, эмуляцию горячих клавиш и макросы (канал 04 11, формат — в [PROTOCOL.md](../PROTOCOL.md)).
|
Переназначает клавиши на кнопки мыши, функции текстового редактора, мультимедиа/веб-действия, эмуляцию горячих клавиш и макросы (канал 04 11, формат — в [protocol.md](../protocol.md)).
|
||||||
|
|
||||||
```
|
```
|
||||||
./katana.py remap --key caps=lmb # Caps → левая кнопка мыши
|
./katana.py remap --key caps=lmb # Caps → левая кнопка мыши
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
### `--no-save`
|
### `--no-save`
|
||||||
|
|
||||||
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
|
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
|
||||||
|
|
||||||
**Важно:** каждый вызов перезаписывает ВСЮ таблицу переназначений (как вендорская утилита).
|
**Важно:** каждый вызов перезаписывает ВСЮ таблицу переназначений (как вендорская утилита).
|
||||||
Переназначения, не указанные в текущем вызове `--key`, сбрасываются в default.
|
Переназначения, не указанные в текущем вызове `--key`, сбрасываются в default.
|
||||||
|
|||||||
+1
-1
@@ -19,4 +19,4 @@ katana.py scan --from 1 --to 19 --delay 2
|
|||||||
В конце печатается список принятых и отклонённых прошивкой режимов.
|
В конце печатается список принятых и отклонённых прошивкой режимов.
|
||||||
Полезно при реверсе: смотреть на клавиатуру и записывать, какой номер какой эффект показывает.
|
Полезно при реверсе: смотреть на клавиатуру и записывать, какой номер какой эффект показывает.
|
||||||
|
|
||||||
Сами режимы и их названия — в [COLOR-MODES.md](../COLOR-MODES.md).
|
Сами режимы и их названия — в [color-modes.md](../color-modes.md).
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
- Команда отклоняется со статусом `00`/`ff` — сессия залипла.
|
- Команда отклоняется со статусом `00`/`ff` — сессия залипла.
|
||||||
Сначала повторить команду, затем `katana.py reset` (переподключение порта ~2–12 с, клавиатура не отваливается от системы).
|
Сначала повторить команду, затем `katana.py reset` (переподключение порта ~2–12 с, клавиатура не отваливается от системы).
|
||||||
- `reset` не помог — выключить/включить клавиатуру физически.
|
- `reset` не помог — выключить/включить клавиатуру физически.
|
||||||
- Подробности и все известные грабли — в [PROTOCOL.md](../PROTOCOL.md) (раздел «Важные грабли»).
|
- Подробности и все известные грабли — в [protocol.md](../protocol.md) (раздел «Важные грабли»).
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Управление RGB-подсветкой GANSS ARDOR_Katana (0c45:8006) из Linux.
|
"""Управление RGB-подсветкой GANSS ARDOR_Katana (0c45:8006) из Linux.
|
||||||
|
|
||||||
Полная документация протокола — docs/PROTOCOL.md, карта «LED-индекс → клавиша» —
|
Полная документация протокола — docs/protocol.md, карта «LED-индекс → клавиша» —
|
||||||
keymap.py, справочник команд — docs/CLI.md.
|
keymap.py, справочник команд — docs/CLI.md.
|
||||||
|
|
||||||
Краткая справка (подробности в PROTOCOL.md):
|
Краткая справка (подробности в protocol.md):
|
||||||
- Транспорт: HID feature-репорты rid=0 через ioctl к /dev/hidrawN
|
- Транспорт: HID feature-репорты rid=0 через ioctl к /dev/hidrawN
|
||||||
(драйвер usbhid не отцепляется, печать не прерывается).
|
(драйвер usbhid не отцепляется, печать не прерывается).
|
||||||
НИКОГДА не использовать pyusb с detach_kernel_driver.
|
НИКОГДА не использовать pyusb с detach_kernel_driver.
|
||||||
@@ -45,6 +45,8 @@ keymap.py, справочник команд — docs/CLI.md.
|
|||||||
./katana.py reset # разблокировать прошивку без переподключения
|
./katana.py reset # разблокировать прошивку без переподключения
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
import fcntl
|
import fcntl
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -96,7 +98,7 @@ MOUSE_ACTIONS = {
|
|||||||
# шорткаты Ctrl+<буква>, код в байте [2] — HID usage ID буквы, порядок
|
# шорткаты Ctrl+<буква>, код в байте [2] — HID usage ID буквы, порядок
|
||||||
# транзакций в дампе совпадает с порядком списка в UI.
|
# транзакций в дампе совпадает с порядком списка в UI.
|
||||||
# Байт [1] = 01 — модификатор (наблюдался только Ctrl); другие модификаторы
|
# Байт [1] = 01 — модификатор (наблюдался только Ctrl); другие модификаторы
|
||||||
# и произвольные шорткаты не исследованы (см. PROTOCOL.md).
|
# и произвольные шорткаты не исследованы (см. protocol.md).
|
||||||
EDITOR_ACTIONS = {
|
EDITOR_ACTIONS = {
|
||||||
"open": 0x12, # Открыть (Ctrl+O)
|
"open": 0x12, # Открыть (Ctrl+O)
|
||||||
"new": 0x11, # Создать (Ctrl+N)
|
"new": 0x11, # Создать (Ctrl+N)
|
||||||
@@ -214,7 +216,7 @@ VK_TO_MOUSE = {1: 0x01, 2: 0x04, 3: 0x02}
|
|||||||
|
|
||||||
# Обратные таблицы: код → имя / код → VK (для показа и экспорта XML).
|
# Обратные таблицы: код → имя / код → VK (для показа и экспорта XML).
|
||||||
# Коды 0x05/0x06 двусмысленны: это VolUp/VolDown прошивки и одновременно
|
# Коды 0x05/0x06 двусмысленны: это VolUp/VolDown прошивки и одновременно
|
||||||
# HID-коды букв B/C (коллизия кодового пространства, см. PROTOCOL.md).
|
# HID-коды букв B/C (коллизия кодового пространства, см. protocol.md).
|
||||||
# При показе и экспорте предпочитаем медиа-имена, как в вендорском XML.
|
# При показе и экспорте предпочитаем медиа-имена, как в вендорском XML.
|
||||||
MACRO_CODE_NAMES = {0x05: "volup", 0x06: "voldown",
|
MACRO_CODE_NAMES = {0x05: "volup", 0x06: "voldown",
|
||||||
0x5C: "left", 0x5D: "up", 0x5E: "right", 0x5F: "down"}
|
0x5C: "left", 0x5D: "up", 0x5E: "right", 0x5F: "down"}
|
||||||
@@ -237,7 +239,7 @@ FLAG_DEFAULTS = {1: 0x00, 4: 0x00} # для остальных режимов
|
|||||||
# ---- таблица режимов и алиасы ----------------------------------------------
|
# ---- таблица режимов и алиасы ----------------------------------------------
|
||||||
|
|
||||||
# Номер режима → (алиас, описание). Порядок совпадает со списком вендорской
|
# Номер режима → (алиас, описание). Порядок совпадает со списком вендорской
|
||||||
# утилиты (docs/COLOR-MODES.md). «моно/спектр» — режим рисует одиночный --color
|
# утилиты (docs/color-modes.md). «моно/спектр» — режим рисует одиночный --color
|
||||||
# или полный спектр независимо от него.
|
# или полный спектр независимо от него.
|
||||||
MODES = {
|
MODES = {
|
||||||
1: ("static", "Постоянный свет всех клавиш (моно/спектр)"),
|
1: ("static", "Постоянный свет всех клавиш (моно/спектр)"),
|
||||||
@@ -467,7 +469,7 @@ def resolve_key(name: str):
|
|||||||
return None
|
return None
|
||||||
if _KEY_ALIASES is None:
|
if _KEY_ALIASES is None:
|
||||||
try:
|
try:
|
||||||
from keymap import KEYMAP, KEY_SYMBOL_ALIASES
|
from misc.keymap import KEYMAP, KEY_SYMBOL_ALIASES
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
_KEY_ALIASES = {}
|
_KEY_ALIASES = {}
|
||||||
@@ -782,6 +784,168 @@ class Katana:
|
|||||||
"(try 'reset')")
|
"(try 'reset')")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- IOKit-транспорт (macOS) -----------------------------------------------------
|
||||||
|
|
||||||
|
def _mac_load_iokit():
|
||||||
|
"""Загрузить IOKit/CoreFoundation и настроить прототипы функций (один раз)."""
|
||||||
|
iokit = ctypes.CDLL(ctypes.util.find_library("IOKit"))
|
||||||
|
cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
|
||||||
|
# перечисление и создание устройства
|
||||||
|
iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOServiceGetMatchingServices.restype = ctypes.c_int
|
||||||
|
# ВАЖНО: IOServiceMatching возвращает CFDictionary (указатель) — без
|
||||||
|
# явного restype ctypes обрежет его до int и упадёт segfault'ом.
|
||||||
|
iokit.IOServiceMatching.restype = ctypes.c_void_p
|
||||||
|
iokit.IOServiceMatching.argtypes = [ctypes.c_char_p]
|
||||||
|
iokit.IOIteratorNext.argtypes = [ctypes.c_uint]
|
||||||
|
iokit.IOIteratorNext.restype = ctypes.c_uint
|
||||||
|
iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
|
||||||
|
iokit.IORegistryEntryCreateCFProperties.argtypes = [
|
||||||
|
ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
|
||||||
|
iokit.IORegistryEntryCreateCFProperties.restype = ctypes.c_int
|
||||||
|
iokit.IOHIDDeviceCreate.argtypes = [ctypes.c_void_p, ctypes.c_uint]
|
||||||
|
iokit.IOHIDDeviceCreate.restype = ctypes.c_void_p
|
||||||
|
iokit.IOHIDDeviceOpen.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
iokit.IOHIDDeviceOpen.restype = ctypes.c_int
|
||||||
|
iokit.IOHIDDeviceClose.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
iokit.IOHIDDeviceClose.restype = ctypes.c_int
|
||||||
|
# feature-репорты: (device, reportType, reportID, buffer, bufferLen)
|
||||||
|
iokit.IOHIDDeviceSetReport.argtypes = [ctypes.c_void_p, ctypes.c_int,
|
||||||
|
ctypes.c_int, ctypes.c_void_p, ctypes.c_long]
|
||||||
|
iokit.IOHIDDeviceSetReport.restype = ctypes.c_int
|
||||||
|
iokit.IOHIDDeviceGetReport.argtypes = [ctypes.c_void_p, ctypes.c_int,
|
||||||
|
ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOHIDDeviceGetReport.restype = ctypes.c_int
|
||||||
|
# свойства устройства: IOHIDDeviceGetProperty(dev, CFStringRef) -> CFTypeRef
|
||||||
|
iokit.IOHIDDeviceGetProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOHIDDeviceGetProperty.restype = ctypes.c_void_p
|
||||||
|
# CoreFoundation: чтение свойств реестра
|
||||||
|
cf.CFDictionaryGetValue.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
cf.CFDictionaryGetValue.restype = ctypes.c_void_p
|
||||||
|
cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_long, ctypes.c_void_p]
|
||||||
|
cf.CFNumberGetValue.restype = ctypes.c_bool
|
||||||
|
cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long]
|
||||||
|
cf.CFStringCreateWithCString.restype = ctypes.c_void_p
|
||||||
|
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||||
|
return iokit, cf
|
||||||
|
|
||||||
|
|
||||||
|
def _mac_find_service(iokit, cf):
|
||||||
|
"""IOKit-сервис интерфейса 0 клавиатуры (конфиг-канал).
|
||||||
|
|
||||||
|
Устройства перечисляет IOServiceGetMatchingServices("IOHIDDevice");
|
||||||
|
свойства читает IOHIDDeviceGetProperty (в реестре idVendor нет — это
|
||||||
|
свойство устройства, а не записи). Интерфейс различаем по usage:
|
||||||
|
конфиг-канал — boot keyboard (page 1, usage 6), тот же, что и :1.0
|
||||||
|
в Linux (см. find_hidraw).
|
||||||
|
"""
|
||||||
|
kCFStringEncodingUTF8, kCFNumberSInt32Type = 0x08000100, 3
|
||||||
|
|
||||||
|
def get_int(dev, key):
|
||||||
|
v = iokit.IOHIDDeviceGetProperty(
|
||||||
|
dev, cf.CFStringCreateWithCString(None, key.encode(),
|
||||||
|
kCFStringEncodingUTF8))
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
|
out = ctypes.c_int32()
|
||||||
|
cf.CFNumberGetValue(v, kCFNumberSInt32Type, ctypes.byref(out))
|
||||||
|
return out.value
|
||||||
|
|
||||||
|
it = ctypes.c_uint()
|
||||||
|
kr = iokit.IOServiceGetMatchingServices(
|
||||||
|
0, iokit.IOServiceMatching(b"IOHIDDevice"), ctypes.byref(it))
|
||||||
|
if kr != 0:
|
||||||
|
raise SystemExit(f"macOS: IOServiceGetMatchingServices failed {kr:#x}")
|
||||||
|
while True:
|
||||||
|
entry = iokit.IOIteratorNext(it)
|
||||||
|
if not entry:
|
||||||
|
break
|
||||||
|
# IOHIDDeviceGetProperty принимает IOHIDDeviceRef, а не registry
|
||||||
|
# entry — создаём хэндл через IOHIDDeviceCreate и освобождаем его.
|
||||||
|
dev = iokit.IOHIDDeviceCreate(None, entry)
|
||||||
|
if dev:
|
||||||
|
match = (get_int(dev, "VendorID") == VID
|
||||||
|
and get_int(dev, "ProductID") == PID
|
||||||
|
and get_int(dev, "PrimaryUsagePage") == 1
|
||||||
|
and get_int(dev, "PrimaryUsage") == 6)
|
||||||
|
cf.CFRelease(dev)
|
||||||
|
if match:
|
||||||
|
return entry
|
||||||
|
iokit.IOObjectRelease(entry)
|
||||||
|
raise SystemExit(
|
||||||
|
f"GANSS ARDOR_Katana ({VID:04x}:{PID:04x}) IOKit iface0 not found")
|
||||||
|
|
||||||
|
|
||||||
|
class KatanaMacOS(Katana):
|
||||||
|
"""Тот же протокол, но обмен через IOKit напрямую (macOS, без hidapi).
|
||||||
|
|
||||||
|
Унаследованная логика команд (_cmd, lighting, write_key_table, ...)
|
||||||
|
вызывает только _set/_get/close — их и переопределяем.
|
||||||
|
IOHIDDeviceSetReport/GetReport с kIOHIDReportTypeFeature=2 соответствуют
|
||||||
|
HIDIOCSFEATURE/HIDIOCGFEATURE из Linux-транспорта: те же feature-репорты
|
||||||
|
rid=0, 64 байта данных. ВНИМАНИЕ: у IOKit своя нумерация типов репортов
|
||||||
|
(Input=0, Output=1, Feature=2) — с Output=1 прошивка отвечает мусором.
|
||||||
|
"""
|
||||||
|
|
||||||
|
REPORT_FEATURE = 2 # kIOHIDReportTypeFeature (не путать с Output=1!)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.iokit, self.cf = _mac_load_iokit()
|
||||||
|
service = _mac_find_service(self.iokit, self.cf)
|
||||||
|
self.dev = self.iokit.IOHIDDeviceCreate(None, service)
|
||||||
|
self.iokit.IOObjectRelease(service)
|
||||||
|
if not self.dev:
|
||||||
|
raise SystemExit("macOS: IOHIDDeviceCreate failed")
|
||||||
|
r = self.iokit.IOHIDDeviceOpen(self.dev, 0)
|
||||||
|
if r != 0:
|
||||||
|
self.cf.CFRelease(self.dev)
|
||||||
|
self.dev = None
|
||||||
|
# 0xE00002C1 = NotPrivileged, 0xE00002C5 = ExclusiveAccess
|
||||||
|
hint = ("1. Grant 'Input Monitoring' permission to your terminal "
|
||||||
|
"app (System Settings → Privacy & Security → Input "
|
||||||
|
"Monitoring), then restart the terminal.\n"
|
||||||
|
"2. If it still fails, another app holds the keyboard "
|
||||||
|
"exclusively — quit Karabiner-Elements (or exclude this "
|
||||||
|
"device in its Devices settings) and retry.")
|
||||||
|
extra = ""
|
||||||
|
if r == 0xE00002C5:
|
||||||
|
extra = (" (kIOReturnExclusiveAccess — клавиатуру захватил "
|
||||||
|
"другой процесс, скорее всего Karabiner-Elements)")
|
||||||
|
raise SystemExit(f"macOS: IOHIDDeviceOpen failed {r:#010x}{extra}\n{hint}")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if getattr(self, "dev", None) is not None:
|
||||||
|
self.iokit.IOHIDDeviceClose(self.dev, 0)
|
||||||
|
self.cf.CFRelease(self.dev)
|
||||||
|
self.dev = None
|
||||||
|
|
||||||
|
def _set(self, payload: bytes):
|
||||||
|
"""Отправить feature-репорт (SET_REPORT), rid=0."""
|
||||||
|
r = self.iokit.IOHIDDeviceSetReport(
|
||||||
|
self.dev, self.REPORT_FEATURE, 0, bytes(payload), len(payload))
|
||||||
|
time.sleep(DELAY)
|
||||||
|
if r != 0:
|
||||||
|
raise SystemExit(f"macOS: IOHIDDeviceSetReport failed {r:#010x}")
|
||||||
|
|
||||||
|
def _get(self) -> bytes:
|
||||||
|
"""Прочитать feature-репорт (GET_REPORT), rid=0 → 64 байта данных."""
|
||||||
|
buf = (ctypes.c_ubyte * 64)()
|
||||||
|
n = ctypes.c_long(64)
|
||||||
|
r = self.iokit.IOHIDDeviceGetReport(
|
||||||
|
self.dev, self.REPORT_FEATURE, 0, buf, ctypes.byref(n))
|
||||||
|
time.sleep(DELAY)
|
||||||
|
if r != 0:
|
||||||
|
raise SystemExit(f"macOS: IOHIDDeviceGetReport failed {r:#010x}")
|
||||||
|
return bytes(buf[:n.value])
|
||||||
|
|
||||||
|
|
||||||
|
def open_katana():
|
||||||
|
"""Фабрика транспорта: IOKit на macOS, hidraw-ioctl на остальном."""
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return KatanaMacOS()
|
||||||
|
return Katana()
|
||||||
|
|
||||||
|
|
||||||
# ---- макросы: блоб, токены, XML -------------------------------------------------
|
# ---- макросы: блоб, токены, XML -------------------------------------------------
|
||||||
|
|
||||||
def build_macro_blob(macros):
|
def build_macro_blob(macros):
|
||||||
@@ -1130,7 +1294,7 @@ def dispatch(args, keyboard: Katana):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = build_parser().parse_args()
|
args = build_parser().parse_args()
|
||||||
keyboard = Katana()
|
keyboard = open_katana()
|
||||||
try:
|
try:
|
||||||
dispatch(args, keyboard)
|
dispatch(args, keyboard)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ lighting:
|
|||||||
# direction: north-south # направление: north-south|south-north|east-west|west-east
|
# direction: north-south # направление: north-south|south-north|east-west|west-east
|
||||||
# flag: null # переопределение байта flag (обычно не нужно)
|
# flag: null # переопределение байта flag (обычно не нужно)
|
||||||
# ВНИМАНИЕ: у breathing (режим 7) цвет игнорируется прошивкой — цикл зашит
|
# ВНИМАНИЕ: у breathing (режим 7) цвет игнорируется прошивкой — цикл зашит
|
||||||
# (см. docs/COLOR-MODES.md, сноска ¹). Для моноцвета берите static/fade.
|
# (см. docs/color-modes.md, сноска ¹). Для моноцвета берите static/fade.
|
||||||
|
|
||||||
# --- сырой payload (команда raw, для экспериментов) ----------------------------
|
# --- сырой payload (команда raw, для экспериментов) ----------------------------
|
||||||
# raw: "80 00 00 00 00 00 00 00 00 0f 00 00 00 00 aa 55"
|
# raw: "80 00 00 00 00 00 00 00 00 0f 00 00 00 00 aa 55"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Диагностика открытия HID-устройства через IOKit напрямую (ctypes).
|
||||||
|
|
||||||
|
Берёт путь устройства из hidapi (DevSrvsID:<entry_id> = registry entry ID),
|
||||||
|
открывает сервис через IORegistryEntryIDMatching + IOHIDDeviceCreate (как
|
||||||
|
делает сам hidapi) и печатает точный код IOReturn от IOHIDDeviceOpen:
|
||||||
|
- 0x00000000 — успех (права есть, проблема в hidapi)
|
||||||
|
- 0xE00002C1 — kIOReturnNotPrivileged (нет разрешения Input Monitoring)
|
||||||
|
- другой код — иная причина (см. коды IOKit, iokit_common_err).
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import hid
|
||||||
|
|
||||||
|
iokit = ctypes.CDLL(ctypes.util.find_library("IOKit"))
|
||||||
|
cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
|
||||||
|
|
||||||
|
# прототипы функций
|
||||||
|
iokit.IOServiceGetMatchingService.argtypes = [ctypes.c_int, ctypes.c_void_p]
|
||||||
|
iokit.IOServiceGetMatchingService.restype = ctypes.c_uint
|
||||||
|
iokit.IORegistryEntryIDMatching.argtypes = [ctypes.c_longlong]
|
||||||
|
iokit.IORegistryEntryIDMatching.restype = ctypes.c_void_p
|
||||||
|
iokit.IOObjectRelease.argtypes = [ctypes.c_uint]
|
||||||
|
iokit.IOObjectRelease.restype = ctypes.c_int
|
||||||
|
iokit.IOHIDDeviceCreate.argtypes = [ctypes.c_void_p, ctypes.c_uint]
|
||||||
|
iokit.IOHIDDeviceCreate.restype = ctypes.c_void_p
|
||||||
|
iokit.IOHIDDeviceOpen.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
iokit.IOHIDDeviceOpen.restype = ctypes.c_int
|
||||||
|
iokit.IOHIDDeviceClose.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
iokit.IOHIDDeviceClose.restype = ctypes.c_int
|
||||||
|
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Найти iface0 клавиатуры через hidapi и открыть её сервис в IOKit."""
|
||||||
|
paths = [i["path"].decode() for i in hid.enumerate(0x0C45, 0x8006)
|
||||||
|
if i["interface_number"] == 0]
|
||||||
|
if not paths:
|
||||||
|
raise SystemExit("устройство 0C45:8006 не найдено через hidapi")
|
||||||
|
m = re.fullmatch(r"DevSrvsID:(\d+)", paths[0])
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"неожиданный формат пути: {paths[0]!r}")
|
||||||
|
entry_id = int(m.group(1))
|
||||||
|
|
||||||
|
service = iokit.IOServiceGetMatchingService(
|
||||||
|
0, iokit.IORegistryEntryIDMatching(entry_id))
|
||||||
|
if not service:
|
||||||
|
raise SystemExit(f"сервис с entry_id {entry_id} не найден в реестре")
|
||||||
|
print(f"сервис найден: {service:#x}")
|
||||||
|
|
||||||
|
dev = iokit.IOHIDDeviceCreate(None, service)
|
||||||
|
iokit.IOObjectRelease(service)
|
||||||
|
if not dev:
|
||||||
|
raise SystemExit("IOHIDDeviceCreate вернул NULL")
|
||||||
|
print("IOHIDDeviceCreate: ok")
|
||||||
|
|
||||||
|
r = iokit.IOHIDDeviceOpen(dev, 0) # kIOHIDOptionsTypeNone
|
||||||
|
meaning = {0x00000000: "OK (права есть)",
|
||||||
|
0xE00002C1: "NotPrivileged — нет разрешения Input Monitoring",
|
||||||
|
}.get(r, "см. коды IOKit (iokit_common_err)")
|
||||||
|
print(f"IOHIDDeviceOpen: {r:#010x} ({meaning})")
|
||||||
|
if r == 0:
|
||||||
|
iokit.IOHIDDeviceClose(dev, 0)
|
||||||
|
cf.CFRelease(dev)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Построена экспериментально: группы индексов красились контрастными цветами,
|
Построена экспериментально: группы индексов красились контрастными цветами,
|
||||||
пользователь сообщал, какие клавиши загорелись (методика и открытые вопросы —
|
пользователь сообщал, какие клавиши загорелись (методика и открытые вопросы —
|
||||||
в docs/PROTOCOL.md).
|
в docs/protocol.md).
|
||||||
|
|
||||||
Имена клавиш — позиции US-раскладки.
|
Имена клавиш — позиции US-раскладки.
|
||||||
"""
|
"""
|
||||||
@@ -5,7 +5,7 @@ HIDIOCGFEATURE(len) = _IOWR('H', 0x07, len) — размер кодируетс
|
|||||||
Для состояния клавиатуры зонд безопасен: только GET_FEATURE-запросы,
|
Для состояния клавиатуры зонд безопасен: только GET_FEATURE-запросы,
|
||||||
ничего не записывается.
|
ничего не записывается.
|
||||||
|
|
||||||
Использование: probe.py [hidrawN]
|
Использование: misc/probe.py [hidrawN]
|
||||||
(по умолчанию — автоопределение всех hidraw-узлов клавиатуры)
|
(по умолчанию — автоопределение всех hidraw-узлов клавиатуры)
|
||||||
"""
|
"""
|
||||||
import array
|
import array
|
||||||
@@ -15,7 +15,7 @@ import sys
|
|||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
import keymap
|
import misc.keymap as keymap
|
||||||
from katana import (ALPHA_KEYS, ARROW_KEYS, BIND_COUNT, DIGITS_KEYS, KEYS_COUNT,
|
from katana import (ALPHA_KEYS, ARROW_KEYS, BIND_COUNT, DIGITS_KEYS, KEYS_COUNT,
|
||||||
MODES, NUMPAD_KEYS, PUNCT_KEYS, ROW_KEYS, Katana, _hid_ioc,
|
MODES, NUMPAD_KEYS, PUNCT_KEYS, ROW_KEYS, Katana, _hid_ioc,
|
||||||
_hid_iocgfeature, _hid_iocsfeature, build_payload,
|
_hid_iocgfeature, _hid_iocsfeature, build_payload,
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Офлайн-тесты выбора транспорта katana.py (без железа).
|
||||||
|
|
||||||
|
Запуск: python3 tests/test_transport.py (или pytest tests/test_transport.py).
|
||||||
|
|
||||||
|
Покрывает: фабрику open_katana (платформенный выбор Katana/KatanaMacOS),
|
||||||
|
поиск устройства _mac_find_service (выбор boot keyboard) и формат обмена
|
||||||
|
KatanaMacOS._set/_get через IOKit (feature-репорты, 64 байта, rid=0).
|
||||||
|
IOKit подменяется фейком в katana._mac_load_iokit — реальный IOKit есть
|
||||||
|
только на macOS, поэтому на Linux тесты тоже проходят.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import katana
|
||||||
|
from katana import Katana, KatanaMacOS, open_katana
|
||||||
|
|
||||||
|
|
||||||
|
class FakeIOKit:
|
||||||
|
"""Подменный IOKit: журнал вызовов, зашитые свойства и ACK прошивки.
|
||||||
|
|
||||||
|
Имитирует только то, что использует KatanaMacOS:
|
||||||
|
перечисление (одна клавиатура: boot keyboard + consumer-интерфейс),
|
||||||
|
IOHIDDeviceSetReport/GetReport (feature-репорты rid=0).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.sent, self.get_count = [], 0
|
||||||
|
self.ack = bytes([0x04, 0x18, 0x00, 0x01]) + bytes(60)
|
||||||
|
# свойства двух интерфейсов клавиатуры (и одно чужое устройство)
|
||||||
|
self.props = {
|
||||||
|
0x101: {"VendorID": 0x0C45, "ProductID": 0x8006,
|
||||||
|
"PrimaryUsagePage": 1, "PrimaryUsage": 6}, # iface0
|
||||||
|
0x102: {"VendorID": 0x0C45, "ProductID": 0x8006,
|
||||||
|
"PrimaryUsagePage": 12, "PrimaryUsage": 1}, # iface1
|
||||||
|
0x103: {"VendorID": 0x1234, "ProductID": 0x5678,
|
||||||
|
"PrimaryUsagePage": 1, "PrimaryUsage": 6}, # чужое
|
||||||
|
}
|
||||||
|
self.released = []
|
||||||
|
|
||||||
|
# -- перечисление --
|
||||||
|
|
||||||
|
def IOServiceGetMatchingServices(self, port, matcher, it_ref):
|
||||||
|
# it_ref — ctypes.byref(c_uint); храним итератор отдельно
|
||||||
|
self._it = iter(list(self.props))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def IOServiceMatching(self, name):
|
||||||
|
return object() # фиктивный словарь соответствия
|
||||||
|
|
||||||
|
def IOIteratorNext(self, it):
|
||||||
|
return next(self._it, 0)
|
||||||
|
|
||||||
|
def IOObjectRelease(self, entry):
|
||||||
|
self.released.append(entry)
|
||||||
|
|
||||||
|
def IOHIDDeviceCreate(self, port, entry):
|
||||||
|
return f"dev{entry:#x}" if entry in self.props else None
|
||||||
|
|
||||||
|
def IOHIDDeviceGetProperty(self, dev, key):
|
||||||
|
entry = int(dev[3:], 16)
|
||||||
|
return self.props[entry].get(key.value.decode())
|
||||||
|
|
||||||
|
# -- feature-репорты --
|
||||||
|
|
||||||
|
def IOHIDDeviceSetReport(self, dev, rtype, rid, buf, length):
|
||||||
|
self.sent.append((rtype, rid, bytes(buf[:length])))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def IOHIDDeviceGetReport(self, dev, rtype, rid, buf, n_ref):
|
||||||
|
# n_ref приходит как ctypes.byref(n) — распаковываем через _obj
|
||||||
|
n = min(n_ref._obj.value, len(self.ack))
|
||||||
|
for i in range(n):
|
||||||
|
buf[i] = self.ack[i]
|
||||||
|
n_ref._obj.value = n
|
||||||
|
self.get_count += 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def IOHIDDeviceOpen(self, dev, options):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def IOHIDDeviceClose(self, dev, options):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCF:
|
||||||
|
"""Подменный CoreFoundation: строки-ключи как есть, CFNumber → int."""
|
||||||
|
|
||||||
|
def CFStringCreateWithCString(self, alloc, s, encoding):
|
||||||
|
return types.SimpleNamespace(value=s)
|
||||||
|
|
||||||
|
def CFNumberGetValue(self, num, ntype, out_ref):
|
||||||
|
out_ref._obj.value = int(num)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def CFRelease(self, obj):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def make_macos_katana():
|
||||||
|
"""KatanaMacOS с подмененными IOKit/CF; возвращает (k, fake_iokit, restore)."""
|
||||||
|
fake_iokit, fake_cf = FakeIOKit(), FakeCF()
|
||||||
|
saved = (katana._mac_load_iokit, katana._mac_find_service)
|
||||||
|
|
||||||
|
def fake_load():
|
||||||
|
return fake_iokit, fake_cf
|
||||||
|
|
||||||
|
def fake_find(iokit, cf):
|
||||||
|
return 0x101 # boot keyboard (iface0)
|
||||||
|
|
||||||
|
katana._mac_load_iokit = fake_load
|
||||||
|
katana._mac_find_service = fake_find
|
||||||
|
k = KatanaMacOS()
|
||||||
|
|
||||||
|
def restore():
|
||||||
|
katana._mac_load_iokit, katana._mac_find_service = saved
|
||||||
|
return k, fake_iokit, restore
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_katana_darwin():
|
||||||
|
"""open_katana на darwin возвращает KatanaMacOS (IOKit-транспорт)."""
|
||||||
|
orig = sys.platform
|
||||||
|
sys.platform = "darwin"
|
||||||
|
try:
|
||||||
|
k, fake, restore = make_macos_katana()
|
||||||
|
try:
|
||||||
|
got = open_katana()
|
||||||
|
assert isinstance(got, KatanaMacOS)
|
||||||
|
got.close()
|
||||||
|
finally:
|
||||||
|
restore()
|
||||||
|
finally:
|
||||||
|
sys.platform = orig
|
||||||
|
|
||||||
|
|
||||||
|
def test_mac_find_service_boot_keyboard():
|
||||||
|
"""_mac_find_service: выбирает boot keyboard (page 1, usage 6) клавиатуры."""
|
||||||
|
fake_iokit, fake_cf = FakeIOKit(), FakeCF()
|
||||||
|
assert katana._mac_find_service(fake_iokit, fake_cf) == 0x101
|
||||||
|
|
||||||
|
|
||||||
|
def test_mac_find_service_not_found():
|
||||||
|
"""_mac_find_service: без boot keyboard клавиатуры — SystemExit."""
|
||||||
|
fake_iokit, fake_cf = FakeIOKit(), FakeCF()
|
||||||
|
del fake_iokit.props[0x101] # убрать клавиатурный интерфейс
|
||||||
|
try:
|
||||||
|
katana._mac_find_service(fake_iokit, fake_cf)
|
||||||
|
except SystemExit as e:
|
||||||
|
assert "0c45:8006" in str(e), str(e)
|
||||||
|
else:
|
||||||
|
raise AssertionError("must fail without boot keyboard")
|
||||||
|
|
||||||
|
|
||||||
|
def test_macos_set_get_format():
|
||||||
|
"""KatanaMacOS._set/_get: feature-репорт rid=0, payload 64 байта целиком."""
|
||||||
|
k, fake, restore = make_macos_katana()
|
||||||
|
try:
|
||||||
|
payload = bytes([0x04, 0x18]) + bytes(62)
|
||||||
|
k._set(payload)
|
||||||
|
assert fake.sent == [(KatanaMacOS.REPORT_FEATURE, 0, payload)]
|
||||||
|
assert k._get() == fake.ack
|
||||||
|
assert fake.get_count == 1
|
||||||
|
k.close()
|
||||||
|
finally:
|
||||||
|
restore()
|
||||||
|
|
||||||
|
|
||||||
|
def test_macos_cmd_uses_inherited_logic():
|
||||||
|
"""KatanaMacOS._cmd: унаследованная логика команд работает поверх IOKit."""
|
||||||
|
k, fake, restore = make_macos_katana()
|
||||||
|
try:
|
||||||
|
ack = k._cmd(0x18)
|
||||||
|
assert ack[:4] == b"\x04\x18\x00\x01"
|
||||||
|
# _cmd шлёт 64-байтную команду 04 <opcode> как feature-репорт rid=0
|
||||||
|
rtype, rid, buf = fake.sent[0]
|
||||||
|
assert rtype == KatanaMacOS.REPORT_FEATURE and rid == 0
|
||||||
|
assert len(buf) == 64 and buf[0:3] == b"\x04\x18\x00"
|
||||||
|
k.close()
|
||||||
|
finally:
|
||||||
|
restore()
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_katana_linux_class():
|
||||||
|
"""На не-darwin платформе фабрика возвращает hidraw-класс Katana."""
|
||||||
|
orig = sys.platform
|
||||||
|
sys.platform = "linux"
|
||||||
|
try:
|
||||||
|
# Katana.__init__ открывает hidraw-узел — на macOS его нет,
|
||||||
|
# поэтому проверяем только тип: SystemExit от find_hidraw = ветка верная
|
||||||
|
try:
|
||||||
|
open_katana()
|
||||||
|
except SystemExit as e:
|
||||||
|
assert "hidraw" in str(e), str(e)
|
||||||
|
except OSError:
|
||||||
|
pass # /dev/hidraw* существует, но клавиатуры нет — тоже ок
|
||||||
|
else:
|
||||||
|
raise AssertionError("linux branch must try hidraw, not IOKit")
|
||||||
|
finally:
|
||||||
|
sys.platform = orig
|
||||||
|
|
||||||
|
|
||||||
|
def run_all():
|
||||||
|
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||||
|
failed = 0
|
||||||
|
for t in tests:
|
||||||
|
try:
|
||||||
|
t()
|
||||||
|
print(f"ok {t.__name__}")
|
||||||
|
except AssertionError as e:
|
||||||
|
failed += 1
|
||||||
|
print(f"FAIL {t.__name__}: {e}")
|
||||||
|
if failed:
|
||||||
|
raise SystemExit(f"{failed} test(s) failed")
|
||||||
|
print(f"all {len(tests)} transport tests passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_all()
|
||||||
Reference in New Issue
Block a user