# Расширение функционала: свои скрипты и анимации Встроенных команд `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) → «Следующие шаги».