Initial commit
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# Расширение функционала: свои скрипты и анимации
|
||||
|
||||
Встроенных команд `katana.py` хватает для настройки подсветки, но настоящий интерес начинается, когда вы пишете собственные скрипты.
|
||||
Модуль спроектирован как библиотека: класс `Katana` и вспомогательные функции импортируются в любой ваш скрипт одной строкой.
|
||||
|
||||
Документ описывает программный интерфейс, его ограничения и готовые приёмы.
|
||||
Справка по CLI-командам — в [cli/README.md](cli/README.md), низкоуровневые детали протокола — в [PROTOCOL.md](PROTOCOL.md).
|
||||
|
||||
## Что даёт per-key API
|
||||
|
||||
Прошивка хранит таблицу из 143 слотов `[индекс, R, G, B]` и умеет показывать её как отдельный режим `0x80`.
|
||||
Ключевое свойство, проверенное на железе: **таблица обновляется живьём**.
|
||||
Если подсветка уже в режиме `0x80`, каждая новая запись таблицы сразу меняет свечение клавиш — переключать режим повторно не нужно.
|
||||
Именно это делает возможными анимации: цикл «изменил таблицу → записал → пауза» превращается в эффект.
|
||||
|
||||
Второе свойство — чтение живого кадра (`04 f5`): скрипт видит, что сейчас светится, с учётом яркости и активных анимаций.
|
||||
На этом строятся скрипты, которые дорисовывают поверх текущей картинки, не затирая её.
|
||||
|
||||
## Подключение библиотеки
|
||||
|
||||
Все примеры предполагают, что скрипт лежит рядом с `katana.py` (или путь добавлен в `sys.path`).
|
||||
|
||||
```python
|
||||
from light import Katana, KEYS_COUNT, resolve_key, build_payload
|
||||
|
||||
keyboard = Katana() # находит /dev/hidrawN и открывает его
|
||||
try:
|
||||
... # работа с клавиатурой
|
||||
finally:
|
||||
keyboard.close() # обязательно закрывать дескриптор
|
||||
```
|
||||
|
||||
Импортируется именно `light`, а не дублируется транспорт: все проверки ACK, контрольных сумм и таймингов уже внутри.
|
||||
|
||||
## Справочник API
|
||||
|
||||
| Объект | Сигнатура | Что делает |
|
||||
| ------------------------------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `Katana()` | конструктор | Находит hidraw-узел интерфейса 0 и открывает его. |
|
||||
| `keyboard.close()` | — | Закрывает дескриптор. Вызывайте в `finally`. |
|
||||
| `keyboard.lighting(payload, save=True)` | payload — 64 байта | Полная транзакция: begin → data-begin → payload → commit → save. Переключает режим подсветки. |
|
||||
| `keyboard.write_key_table(colors, save=True)` | colors — 143 кортежей `(r, g, b)` | Записывает per-key таблицу (9 пакетов). При активном режиме `0x80` свечение меняется сразу. |
|
||||
| `keyboard.show_custom(brightness=15, save=True)` | brightness 1–15 | Переключает подсветку в режим `0x80` (показ таблицы). |
|
||||
| `keyboard.read_frame()` | — | Читает живой кадр, возвращает список из 143 кортежей `(r, g, b)`. |
|
||||
| `keyboard.reset()` | — | USB port reset: снимает клин прошивки без переподключения. |
|
||||
| `resolve_key(name)` | `'tab'`, `'tilde'`, `'0x25'` | Имя или символ клавиши → LED-индекс (`None`, если не найдено). |
|
||||
| `build_payload(mode, rgb, ...)` | см. докстринг | Собирает 64-байтный payload для `lighting()`. |
|
||||
| `KEYS_COUNT` | константа | Размер таблицы: 143 слота (`0x00`–`0x8e`). |
|
||||
|
||||
Имена слотов: индекс — это позиция в таблице, а не номер клавиши.
|
||||
Карта «индекс → физическая клавиша» — в `keymap.py` (`KEYMAP`, `BLIND`), человекочитаемо: `python3 keymap.py`.
|
||||
|
||||
## Ограничения, которые надо знать заранее
|
||||
|
||||
### Скорость: ~1 кадр в секунду
|
||||
|
||||
Между любыми двумя USB-запросами выдерживается пауза `DELAY = 0.04` с (вендорский тайминг, ускорять не стоит — прошивка теряет пакеты).
|
||||
Один кадр записи таблицы — это 17 запросов, что даёт измеренные на железе цифры:
|
||||
|
||||
| Операция | Время |
|
||||
| ----------------------------- | ------ |
|
||||
| `write_key_table(save=False)` | ~0,6 с |
|
||||
| `write_key_table(save=True)` | ~0,7 с |
|
||||
| `read_frame()` | ~0,6 с |
|
||||
| `lighting()` (смена режима) | ~0,4 с |
|
||||
|
||||
Практический потолок плавной анимации — **1–1,5 кадра в секунду**.
|
||||
Эффекты с быстрым движением (бегущая строка) будут дёргаными; хорошо смотрятся медленные волны, дыхание, плавные переливы.
|
||||
|
||||
### Флеш-память: не пишите в неё каждый кадр
|
||||
|
||||
`save=True` после каждой записи коммитит настройки во флеш-память, у которой ограниченный ресурс перезаписи.
|
||||
Для анимаций это недопустимо расточительно, поэтому:
|
||||
|
||||
- в цикле анимации всегда используйте `save=False`;
|
||||
- настройка, сохранённая последним `save`, остаётся во флеше и восстанавливается при включении клавиатуры;
|
||||
- правило проекта: вызовы `./katana.py paint` из шелла — только с `--no-save` (см. AGENTS.md).
|
||||
|
||||
Несохранённая сессия не вредит: следующая команда откроет её заново (`begin` ответит `ff` = «сессия уже открыта», это штатно).
|
||||
Но не оставляйте сессию открытой на часы — при зависании прошивки помогает `katana.py reset`.
|
||||
|
||||
### Слепые слоты
|
||||
|
||||
39 слотов из 143 ни к какому LED не подключены (список `BLIND` в `keymap.py`).
|
||||
Прошивка молча принимает в них любой цвет, но светиться ничего не будет — не ищите баг у себя.
|
||||
|
||||
## Приём 1. Разовая раскраска скриптом
|
||||
|
||||
Скрипт-«пресет»: заливка по вашему алгоритму вместо перечисления `--key` в шелле.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Двухцветная раскраска: буквенный блок — один цвет, остальное — другой."""
|
||||
import time
|
||||
|
||||
from light import Katana, KEYS_COUNT, resolve_key
|
||||
from keymap import KEYMAP
|
||||
|
||||
LETTERS = {resolve_key(l) for l in "qwertyuiopasdfghjklzxcvbnm"}
|
||||
|
||||
def make_table(base, accent):
|
||||
"""143 кортежей (r, g, b): буквам — accent, остальным — base."""
|
||||
return [accent if i in LETTERS else base for i in range(KEYS_COUNT)]
|
||||
|
||||
keyboard = Katana()
|
||||
try:
|
||||
keyboard.write_key_table(make_table((30, 0, 40), (255, 120, 0)), save=False)
|
||||
keyboard.show_custom(brightness=15, save=True) # режим 0x80 + один save
|
||||
finally:
|
||||
keyboard.close()
|
||||
```
|
||||
|
||||
Обратите внимание: `save` выполняется один раз — в `show_custom`.
|
||||
Таблица при этом остаётся несохранённой во флеше, но горит до выключения клавиатуры.
|
||||
|
||||
## Приём 2. Анимация: бегущая волна
|
||||
|
||||
Каркас любой анимации: подготовить режим `0x80` один раз, затем в цикле менять таблицу.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Синяя волна пробегает по цифровому ряду слева направо."""
|
||||
import math
|
||||
import time
|
||||
|
||||
from light import Katana, KEYS_COUNT, resolve_key
|
||||
|
||||
ROW = [resolve_key(k) for k in "`1234567890-="] # индексы 0x13..0x1f
|
||||
FRAME_PAUSE = 0.2 # пауза между кадрами
|
||||
|
||||
keyboard = Katana()
|
||||
try:
|
||||
# один раз включаем режим 0x80 и гасим таблицу
|
||||
keyboard.write_key_table([(0, 0, 0)] * KEYS_COUNT, save=False)
|
||||
keyboard.show_custom(brightness=15, save=True)
|
||||
|
||||
phase = 0
|
||||
while True:
|
||||
table = [(0, 0, 0)] * KEYS_COUNT
|
||||
for pos, idx in enumerate(ROW):
|
||||
# волна: яркость синего канала зависит от расстояния до фронта
|
||||
wave = max(0.0, math.cos((pos - phase) * math.pi / 6))
|
||||
table[idx] = (0, 0, int(255 * wave))
|
||||
keyboard.write_key_table(table, save=False)
|
||||
phase = (phase + 1) % (len(ROW) + 6)
|
||||
time.sleep(FRAME_PAUSE)
|
||||
finally:
|
||||
keyboard.close()
|
||||
```
|
||||
|
||||
Цикл бесконечный — останавливайте `Ctrl+C`, блок `finally` закроет дескриптор.
|
||||
Полный кадр занимает ~0,8 с (запись + пауза), волна движется заметно плавно, потому что форма синусоиды «размазана» по 13 клавишам.
|
||||
|
||||
Советы по анимациям:
|
||||
|
||||
- меняйте за кадр всю таблицу целиком — частичные обновления всё равно требуют полной транзакции;
|
||||
- не пишите `save` внутри цикла никогда;
|
||||
- гасите таблицу в начале (`[(0, 0, 0)] * KEYS_COUNT`), иначе волна наложится на старую раскраску;
|
||||
- для «дыхания» масштабируйте цвет всех клавиш одним коэффициентом — это дёшево и выглядит хорошо при 1 FPS.
|
||||
|
||||
## Приём 3. Дорисовка поверх живого кадра
|
||||
|
||||
`read_frame()` возвращает текущую картинку, поэтому скрипт может добавлять акценты, не затирая остальное.
|
||||
Это тот же механизм, что у команды `paint --keep`.
|
||||
|
||||
```python
|
||||
frame = keyboard.read_frame() # что светится сейчас
|
||||
table = list(frame) # копия, чтобы не портить оригинал
|
||||
table[resolve_key("esc")] = (255, 0, 0)
|
||||
keyboard.write_key_table(table, save=False)
|
||||
```
|
||||
|
||||
Так работают индикаторы: капс-лок красным, заряд беспроводного режима зелёным, напоминание о перерыве — подсветить ряд цифр после часа работы.
|
||||
Данные для условия скрипт берёт откуда угодно: файл, сокет, D-Bus, `os.stat`.
|
||||
|
||||
Ограничение: в режиме `0x80` кадр — это сама таблица, поэтому «дорисовка поверх анимации прошивки» (например, поверх `spectrum`) даст статичную замену, а не наложение.
|
||||
Смешивать своё и встроенные эффекты в одном кадре прошивка не умеет — это ограничение железа, а не API.
|
||||
|
||||
## Приём 4. Переключение сценариев из шелла
|
||||
|
||||
Скриптам не нужен свой CLI, если хочется вызывать их как команды `katana.py`.
|
||||
Оформите каждый сценарий функцией и выбирайте аргументом:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""./my_fx.py wave|breath|off — выбор сценария первым аргументом."""
|
||||
import sys
|
||||
|
||||
def wave(kb): ...
|
||||
def breath(kb): ...
|
||||
|
||||
SCENARIOS = {"wave": wave, "breath": breath}
|
||||
|
||||
if __name__ == "__main__":
|
||||
from light import Katana
|
||||
name = sys.argv[1] if len(sys.argv) > 1 else "wave"
|
||||
kb = Katana()
|
||||
try:
|
||||
SCENARIOS[name](kb)
|
||||
finally:
|
||||
kb.close()
|
||||
```
|
||||
|
||||
Привычные цвета принимайте через `parse_color` из `light` — так ваши скрипты поймут и `ff8800`, и `darkorange`.
|
||||
|
||||
## Грабли
|
||||
|
||||
- **`pyusb` с `detach_kernel_driver` ломает печать.**
|
||||
Транспорт — только hidraw-ioctl, как в `katana.py`.
|
||||
- **Сессию всегда доводите до `save` или закрывайте программой.**
|
||||
Долгая висящая сессия клинит прошивку; лечение — `katana.py reset`.
|
||||
- **Не укорачивайте `DELAY`.**
|
||||
Пауза 0,04 с снята с вендорских захватов; при ускорении прошивка начинает терять пакеты и ACK-и.
|
||||
- **`read_frame()` возвращает масштабированные яркостью цвета.**
|
||||
При яркости 15/15 максимум `ee`, а не `ff` — не сравнивайте кадр с записанным цветом «в лоб».
|
||||
- **Слот `0x6d` всегда читается как `000000`.**
|
||||
Это null-слот прошивки, даже у вендорской утилиты.
|
||||
|
||||
## Идеи для скриптов
|
||||
|
||||
- индикатор загрузки CPU или памяти — градиент по функциональному ряду;
|
||||
- подсветка уведомлений: вспышка волной при новом сообщении;
|
||||
- «помидор»: 25 минут работы — спокойный цвет, перерыв — зелёная заливка;
|
||||
- плавный рассвет: медленное нарастание яркости и цвета по утрам;
|
||||
- визуализация музыки через любой FFT-вход: спектр по рядам клавиатуры;
|
||||
- световой «миникарт»: подсветка зоны WASD в играх, где это уместно.
|
||||
|
||||
Список открытых направлений самого протокола (переназначение клавиш, байты направлений) — в [PROTOCOL.md](PROTOCOL.md) → «Следующие шаги».
|
||||
Reference in New Issue
Block a user