Files

16 KiB
Raw Permalink Blame History

Расширение функционала: свои скрипты и анимации

Встроенных команд katana.py хватает для настройки подсветки, но настоящий интерес начинается, когда вы пишете собственные скрипты. Модуль спроектирован как библиотека: класс Katana и вспомогательные функции импортируются в любой ваш скрипт одной строкой.

Документ описывает программный интерфейс, его ограничения и готовые приёмы. Справка по CLI-командам — в cli/README.md, низкоуровневые детали протокола — в protocol.md.

Что даёт per-key API

Прошивка хранит таблицу из 143 слотов [индекс, R, G, B] и умеет показывать её как отдельный режим 0x80. Ключевое свойство, проверенное на железе: таблица обновляется живьём. Если подсветка уже в режиме 0x80, каждая новая запись таблицы сразу меняет свечение клавиш — переключать режим повторно не нужно. Именно это делает возможными анимации: цикл «изменил таблицу → записал → пауза» превращается в эффект.

Второе свойство — чтение живого кадра (04 f5): скрипт видит, что сейчас светится, с учётом яркости и активных анимаций. На этом строятся скрипты, которые дорисовывают поверх текущей картинки, не затирая её.

Подключение библиотеки

Все примеры предполагают, что скрипт лежит рядом с katana.py (или путь добавлен в sys.path).

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 в шелле.

#!/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 один раз, затем в цикле менять таблицу.

#!/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.

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. Оформите каждый сценарий функцией и выбирайте аргументом:

#!/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 → «Следующие шаги».