Files

229 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Расширение функционала: свои скрипты и анимации
Встроенных команд `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) → «Следующие шаги».