Поддержка MacOS (проверено в Tahoe)

This commit is contained in:
2026-09-06 17:13:34 +08:00
parent 57c4e80f0d
commit 4515b39090
24 changed files with 590 additions and 65 deletions
+7 -7
View File
@@ -15,15 +15,15 @@
README.md — обзор проекта: возможности, карта документов, быстрый старт, лицензия, предостережение.
Тематические документы лежат в docs/:
* INSTALL.md - установка
* COLOR-MODES.md - режимы
* install.md - установка
* color-modes.md - режимы
* cli/ - команды (обзор и карта файлов — в cli/README.md)
* EXTENDING.md - расширение
* PROTOCOL.md - протокол
* SCRIPTS.md - скрипты
* RAW-DATA.md - дампы
* extending.md - расширение
* protocol.md - протокол
* scripts.md - скрипты
* raw-data.md - дампы
Пользовательские вопросы — в тематических документах, реверс-инжиниринг — в PROTOCOL.md.
Пользовательские вопросы — в тематических документах, реверс-инжиниринг — в protocol.md.
Ссылки между документами — относительные, без якорей на заголовки с кириллицей.
+15 -12
View File
@@ -13,6 +13,8 @@
Подсветка и клавиши управляются HID feature-репортами через штатный драйвер `usbhid` — **печатать можно прямо во время настройки**, драйвер не отцепляется, root не нужен (после разовой установки udev-правила).
macOS поддерживается экспериментально (транспорт IOKit, см. [docs/install.md](docs/install.md) → «Установка на macOS»).
| Характеристика | Значение |
| -------------- | ------------------------------------------------------ |
| Устройство | GANSS ARDOR_Katana |
@@ -22,11 +24,11 @@
## Быстрый старт
```
# 1. установка (подробности — docs/INSTALL.md)
# 1. установка (подробности — docs/install.md)
python3 -m venv .venv && .venv/bin/pip install pyusb
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 paint --black --wasd dodgerblue # чёрная база, WASD голубым
./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 # макросы + переназначение + подсветка
```
Если что-то не работает — загляните в [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/COLOR-MODES.md](docs/COLOR-MODES.md) | 19 режимов подсветки: таблица, примеры `mode`, аргументы, направление анимации. |
| [docs/install.md](docs/install.md) | Установка: зависимости, venv, udev-правило, проверка устройства, решение проблем. |
| [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/remap.md](docs/cli/remap.md) | Переназначение клавиш: мышь, редактор, горячие клавиши, мультимедиа, макросы. |
| [docs/cli/macro.md](docs/cli/macro.md) | Макросы: запись, режимы повтора, XML вендора. |
| [docs/cli/apply.md](docs/cli/apply.md) | `apply` — YAML-конфиг: все настройки одним файлом. |
| [docs/EXTENDING.md](docs/EXTENDING.md) | Свои скрипты и анимации: `katana.py` как библиотека, API `Katana`, ограничения скорости и флеша, готовые приёмы. |
| [docs/PROTOCOL.md](docs/PROTOCOL.md) | Протокол для реверсеров: транспорт, транзакции, форматы payload'ов, карта клавиш, статус реверса, грабли, планы. |
| [docs/SCRIPTS.md](docs/SCRIPTS.md) | Состав репозитория и вспомогательные скрипты: `keymap.py`, `analyze_pcap.py`, `probe.py`. |
| [docs/RAW-DATA.md](docs/RAW-DATA.md) | Дампы USB-трафика вендорской утилиты: как снимались, как конвертировать, состав `raw-data/katana-v1/`. |
| [docs/extending.md](docs/extending.md) | Свои скрипты и анимации: `katana.py` как библиотека, API `Katana`, ограничения скорости и флеша, готовые приёмы. |
| [docs/protocol.md](docs/protocol.md) | Протокол для реверсеров: транспорт, транзакции, форматы payload'ов, карта клавиш, статус реверса, грабли, планы. |
| [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/`. |
## Лицензия
@@ -101,6 +103,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
* AI-модель `koda-pro` через [KodaCode](https://kodacode.ru)
* python 3.10 + pyusb + pyyaml
* Linux (hidraw-ioctl) и macOS (IOKit через ctypes, экспериментально)
* VirtualBox + Windows 11:
* штатная утилита конфигурации клавиатуры
* WireShark с установленным `usbpcap`
@@ -108,7 +111,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
## TODO
* переназначение на другие типы действий (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) → «Макросы»)
* пресеты/анимации поверх per-key API — методика описана в [docs/EXTENDING.md](docs/EXTENDING.md).
* макросы: чтение содержимого из прошивки, коллизия кодов громкости и букв B/C, стрелки Up/Down, остальные медиа-коды ([docs/protocol.md](docs/protocol.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
View File
@@ -2,7 +2,7 @@
19 встроенных режимов прошивки плюс выключение.
Номер режима = байт [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` | моно/спектр | + | + | | бегущие строки по чётным/нечётным рядам в обе стороны |
| 20 | Выключена | `off` | | | | | все клавиши погашены |
¹ У breathing цвет из payload игнорируется прошивкой: цикл цвета зашит и гоняет зелёно-жёлто-красную гамму при любом `--color` (проверено на железе, payload совпадает с вендорским — подробности в [PROTOCOL.md](PROTOCOL.md)).
¹ У breathing цвет из payload игнорируется прошивкой: цикл цвета зашит и гоняет зелёно-жёлто-красную гамму при любом `--color` (проверено на железе, payload совпадает с вендорским — подробности в [protocol.md](protocol.md)).
Для моноцветного дыхания аналога нет; если нужен именно красный постоянный свет — используйте `static`.
## Примеры
@@ -99,4 +99,4 @@
Задаётся `--direction north-south|south-north|east-west|west-east` (байт [11] payload).
Значения сняты для `waterfall` (02/03) и `marquee` (01/00).
Для `flow`, `rotate`, `wind` применяйте те же опции — значения байта уточняются.
Подробности — [PROTOCOL.md](PROTOCOL.md) (раздел «Байт [11] — подрежим»).
Подробности — [protocol.md](protocol.md) (раздел «Байт [11] — подрежим»).
+2 -2
View File
@@ -4,7 +4,7 @@
Модуль спроектирован как библиотека: класс `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
@@ -225,4 +225,4 @@ if __name__ == "__main__":
- визуализация музыки через любой FFT-вход: спектр по рядам клавиатуры;
- световой «миникарт»: подсветка зоны WASD в играх, где это уместно.
Список открытых направлений самого протокола (переназначение клавиш, байты направлений) — в [PROTOCOL.md](PROTOCOL.md) → «Следующие шаги».
Список открытых направлений самого протокола (переназначение клавиш, байты направлений) — в [protocol.md](protocol.md) → «Следующие шаги».
+25 -1
View File
@@ -7,7 +7,7 @@ description: Установка, udev-правило и решение проб
## Зависимости
Нужен 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
@@ -54,3 +54,27 @@ ID `0c45:8006` принадлежит чипу Sonix SN32, который про
Переподключите клавиатуру.
Если пользователь не в группе `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
View File
@@ -18,6 +18,30 @@ Feature-репорты через hidraw ioctl (`HIDIOCSFEATURE`/`HIDIOCGFEATURE
**usbhid не отцепляется** — печать никогда не прерывается.
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`.
- Режим 0x80 отображает таблицу per-key (см. ниже) — это и есть «кастом».
### Таблица режимов (описания вендорской утилиты, docs/COLOR-MODES.md)
### Таблица режимов (описания вендорской утилиты, docs/color-modes.md)
Номер = байт [0] payload.
«моно/спектр» — режим рисует одиночный цвет или полный спектр независимо от него.
@@ -535,7 +559,7 @@ ACTION: мышь (`lmb`/`mouse1`, `rmb`/`mouse2`, `mmb`/`mouse3`, `back`/`mouseb
Шорткаты редактора (тип `02`) тоже сняты и реализованы (дамп keybind-editor).
Горячие клавиши (тип `02`, маска модификаторов в байте [1]) сняты по дампу keybind-hotkeys и реализованы (`HOTKEY_KEYS`/`HOTKEY_MODIFIERS`, `remap --key KEY=MOD+KEY`).
Осталось: другие типы действий (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`, см. «Макросы»).
Осталось: чтение содержимого, коллизия кодов 05/06, стрелки Up/Down, остальные медиа-коды.
+4 -4
View File
@@ -1,6 +1,6 @@
# Дампы GANSS ARDOR_Katana (katana-v1)
Сырые данные, на которых построен реверс протокола (см. [PROTOCOL.md](PROTOCOL.md)).
Сырые данные, на которых построен реверс протокола (см. [protocol.md](protocol.md)).
Устройство: клавиатура GANSS ARDOR_Katana, USB `0C45:8006` (Sonix SN32).
## Состав
@@ -56,7 +56,7 @@
На 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).
В `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).
- Расшифровка протокола по этим дампам — в [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
View File
@@ -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
├── 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/ Офлайн-тесты (без железа)
├── docs/ Документация (см. README.md)
└── raw-data/
@@ -29,7 +31,7 @@
```
Печатает подтверждённую карту, список слепых слотов (39 шт — резерв прошивки, ни к какому LED не подключены), непроидентифицированные индексы и таблицу алиасов клавиш (`KEY_SYMBOL_ALIASES`).
Методика картирования и открытые вопросы — в [PROTOCOL.md](PROTOCOL.md) (раздел «Карта клавиш»).
Методика картирования и открытые вопросы — в [protocol.md](protocol.md) (раздел «Карта клавиш»).
## analyze_pcap.py
@@ -41,19 +43,34 @@
```
Без второго аргумента пишет `/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, состояние клавиатуры не меняет).
Полезен как минимальный пример hidraw-транспорта:
```
./probe.py [hidrawN]
./misc/probe.py [hidrawN]
```
Без аргумента находит все 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/
Офлайн-тесты (железо не нужно):
@@ -63,6 +80,7 @@ python3 tests/test_macro.py # макросы, remap-действия, св
python3 tests/test_apply.py # apply: YAML-конфиг → argv команд
python3 tests/test_parsers.py # парсеры, payload, ACK, keymap
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.
+3 -3
View File
@@ -2,10 +2,10 @@
Справочник команд и аргументов.
Все команды идут через транзакцию `begin → data → commit → save` и безопасны: печать не прерывается, прошивка остаётся живой.
Исключение — `macro set`/`macro clear`: вендор после записи макросов save не делает (подробности в [PROTOCOL.md](../PROTOCOL.md)).
Низкоуровневое описание транзакций — в [PROTOCOL.md](../PROTOCOL.md).
Исключение — `macro set`/`macro clear`: вендор после записи макросов save не делает (подробности в [protocol.md](../protocol.md)).
Низкоуровневое описание транзакций — в [protocol.md](../protocol.md).
Режимы подсветки и их аргументы вынесены в отдельный документ: [COLOR-MODES.md](../COLOR-MODES.md).
Режимы подсветки и их аргументы вынесены в отдельный документ: [color-modes.md](../color-modes.md).
## Состав справочника
+2 -2
View File
@@ -8,7 +8,7 @@
./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
```
### `lighting` — режим подсветки (см. [COLOR-MODES.md](../COLOR-MODES.md))
### `lighting` — режим подсветки (см. [color-modes.md](../color-modes.md))
`mode` — имя или номер 1..19, а также `off` и `default` (применяются как команды `off`/`default`).
+1 -1
View File
@@ -1,7 +1,7 @@
# macro — макросы
Записывает содержимое макросов в прошивку (канал 04 19) и работает с вендорским XML.
Формат протокола — в [PROTOCOL.md](../PROTOCOL.md).
Формат протокола — в [protocol.md](../protocol.md).
Макрос нужно привязать к клавише через `remap --key КЛЮЧ=macro<N>` (индексация 0-based: «Макрос 1» вендорской утилиты = 0).
+1 -1
View File
@@ -91,7 +91,7 @@ Shorthand для `--key`: покрасить горизонтальный ряд
### `--no-save`
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
По умолчанию save выполняется.
Без `--all` и `--key` команда завершается ошибкой.
+2 -2
View File
@@ -10,7 +10,7 @@ Payload до 64 байт (пробелы допустимы).
### `--no-save`
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
По умолчанию save выполняется.
```
@@ -20,4 +20,4 @@ Payload до 64 байт (пробелы допустимы).
Payload отправляется внутри штатной транзакции `begin → data → commit → save`.
Для экспериментов с неописанными байтами.
Формат payload'ов и назначение байтов — в [PROTOCOL.md](../PROTOCOL.md).
Формат payload'ов и назначение байтов — в [protocol.md](../protocol.md).
+2 -2
View File
@@ -1,6 +1,6 @@
# remap — переназначение клавиш
Переназначает клавиши на кнопки мыши, функции текстового редактора, мультимедиа/веб-действия, эмуляцию горячих клавиш и макросы (канал 04 11, формат — в [PROTOCOL.md](../PROTOCOL.md)).
Переназначает клавиши на кнопки мыши, функции текстового редактора, мультимедиа/веб-действия, эмуляцию горячих клавиш и макросы (канал 04 11, формат — в [protocol.md](../protocol.md)).
```
./katana.py remap --key caps=lmb # Caps → левая кнопка мыши
@@ -121,7 +121,7 @@
### `--no-save`
Не писать во флеш (см. `mode --no-save` в [COLOR-MODES.md](../COLOR-MODES.md)).
Не писать во флеш (см. `mode --no-save` в [color-modes.md](../color-modes.md)).
**Важно:** каждый вызов перезаписывает ВСЮ таблицу переназначений (как вендорская утилита).
Переназначения, не указанные в текущем вызове `--key`, сбрасываются в default.
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -3,4 +3,4 @@
- Команда отклоняется со статусом `00`/`ff` — сессия залипла.
Сначала повторить команду, затем `katana.py reset` (переподключение порта ~2–12 с, клавиатура не отваливается от системы).
- `reset` не помог — выключить/включить клавиатуру физически.
- Подробности и все известные грабли — в [PROTOCOL.md](../PROTOCOL.md) (раздел «Важные грабли»).
- Подробности и все известные грабли — в [protocol.md](../protocol.md) (раздел «Важные грабли»).
+171 -7
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Управление RGB-подсветкой GANSS ARDOR_Katana (0c45:8006) из Linux.
Полная документация протокола — docs/PROTOCOL.md, карта «LED-индекс → клавиша» —
Полная документация протокола — docs/protocol.md, карта «LED-индекс → клавиша» —
keymap.py, справочник команд — docs/CLI.md.
Краткая справка (подробности в PROTOCOL.md):
Краткая справка (подробности в protocol.md):
- Транспорт: HID feature-репорты rid=0 через ioctl к /dev/hidrawN
(драйвер usbhid не отцепляется, печать не прерывается).
НИКОГДА не использовать pyusb с detach_kernel_driver.
@@ -45,6 +45,8 @@ keymap.py, справочник команд — docs/CLI.md.
./katana.py reset # разблокировать прошивку без переподключения
"""
import argparse
import ctypes
import ctypes.util
import fcntl
import glob
import os
@@ -96,7 +98,7 @@ MOUSE_ACTIONS = {
# шорткаты Ctrl+<буква>, код в байте [2] — HID usage ID буквы, порядок
# транзакций в дампе совпадает с порядком списка в UI.
# Байт [1] = 01 — модификатор (наблюдался только Ctrl); другие модификаторы
# и произвольные шорткаты не исследованы (см. PROTOCOL.md).
# и произвольные шорткаты не исследованы (см. protocol.md).
EDITOR_ACTIONS = {
"open": 0x12, # Открыть (Ctrl+O)
"new": 0x11, # Создать (Ctrl+N)
@@ -214,7 +216,7 @@ VK_TO_MOUSE = {1: 0x01, 2: 0x04, 3: 0x02}
# Обратные таблицы: код → имя / код → VK (для показа и экспорта XML).
# Коды 0x05/0x06 двусмысленны: это VolUp/VolDown прошивки и одновременно
# HID-коды букв B/C (коллизия кодового пространства, см. PROTOCOL.md).
# HID-коды букв B/C (коллизия кодового пространства, см. protocol.md).
# При показе и экспорте предпочитаем медиа-имена, как в вендорском XML.
MACRO_CODE_NAMES = {0x05: "volup", 0x06: "voldown",
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 = {
1: ("static", "Постоянный свет всех клавиш (моно/спектр)"),
@@ -467,7 +469,7 @@ def resolve_key(name: str):
return None
if _KEY_ALIASES is None:
try:
from keymap import KEYMAP, KEY_SYMBOL_ALIASES
from misc.keymap import KEYMAP, KEY_SYMBOL_ALIASES
except ImportError:
return None
_KEY_ALIASES = {}
@@ -782,6 +784,168 @@ class Katana:
"(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 -------------------------------------------------
def build_macro_blob(macros):
@@ -1130,7 +1294,7 @@ def dispatch(args, keyboard: Katana):
def main():
args = build_parser().parse_args()
keyboard = Katana()
keyboard = open_katana()
try:
dispatch(args, keyboard)
finally:
+1 -1
View File
@@ -61,7 +61,7 @@ lighting:
# direction: north-south # направление: north-south|south-north|east-west|west-east
# flag: null # переопределение байта flag (обычно не нужно)
# ВНИМАНИЕ: у breathing (режим 7) цвет игнорируется прошивкой — цикл зашит
# (см. docs/COLOR-MODES.md, сноска ¹). Для моноцвета берите static/fade.
# (см. docs/color-modes.md, сноска ¹). Для моноцвета берите static/fade.
# --- сырой payload (команда raw, для экспериментов) ----------------------------
# raw: "80 00 00 00 00 00 00 00 00 0f 00 00 00 00 aa 55"
+71
View File
@@ -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()
+1 -1
View File
@@ -3,7 +3,7 @@
Построена экспериментально: группы индексов красились контрастными цветами,
пользователь сообщал, какие клавиши загорелись (методика и открытые вопросы —
в docs/PROTOCOL.md).
в docs/protocol.md).
Имена клавиш — позиции US-раскладки.
"""
+1 -1
View File
@@ -5,7 +5,7 @@ HIDIOCGFEATURE(len) = _IOWR('H', 0x07, len) — размер кодируетс
Для состояния клавиатуры зонд безопасен: только GET_FEATURE-запросы,
ничего не записывается.
Использование: probe.py [hidrawN]
Использование: misc/probe.py [hidrawN]
(по умолчанию — автоопределение всех hidraw-узлов клавиатуры)
"""
import array
+1 -1
View File
@@ -15,7 +15,7 @@ import sys
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,
MODES, NUMPAD_KEYS, PUNCT_KEYS, ROW_KEYS, Katana, _hid_ioc,
_hid_iocgfeature, _hid_iocsfeature, build_payload,
+221
View File
@@ -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()