Перейти к содержанию

Свои скрипты и анимации

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

Что даёт per-key API

Прошивка хранит таблицу из 143 слотов [индекс, R, G, B] и показывает её режимом 0x80. Таблица обновляется живьём: если подсветка уже в этом режиме, каждая новая запись сразу меняет свечение клавиш.

Второе свойство — чтение живого кадра: скрипт видит, что светится сейчас, с учётом яркости и активных анимаций.

Подключение

from katana import Katana, KEYS_COUNT, resolve_key, build_payload

keyboard = Katana()
try:
    ...
finally:
    keyboard.close()

Дескриптор обязательно закрывайте в finally. На macOS вместо Katana() используйте фабрику open_katana(): она сама выберет транспорт.

Справочник API

Объект Что делает
open_katana() Открывает устройство нужным транспортом: hidraw на Linux, IOKit на macOS.
Katana(), KatanaMacOS() Транспорты напрямую; обычно достаточно open_katana().
keyboard.close() Закрывает дескриптор.
keyboard.lighting(payload, save=True) Полная транзакция с payload: переключает режим подсветки.
keyboard.write_key_table(colors, save=True) Записывает таблицу раскраски из 143 кортежей (r, g, b).
keyboard.write_bind_table(records, save=True) Записывает таблицу переназначений, 143 записи по 4 байта.
keyboard.write_macro_table(macros, save=False) Записывает содержимое макросов, все слоты одним блобом.
keyboard.show_custom(brightness=15, save=True) Переключает подсветку в режим 0x80.
keyboard.read_frame() Читает живой кадр и возвращает 143 кортежа (r, g, b).
keyboard.reset() USB port reset без переподключения.
resolve_key(name) Имя или символ клавиши в LED-индекс; None, если клавиша не найдена.
build_payload(mode, rgb, ...) Собирает 64-байтный payload для lighting().
parse_color(text) Hex или имя цвета в кортеж (r, g, b).
KEYS_COUNT Размер таблицы: 143 слота.

Индекс — это позиция в таблице, а не номер клавиши. Карта «индекс → физическая клавиша» лежит в misc/keymap.py.

Для скриптов на Node.js есть та же библиотека — см. раздел TypeScript API.

Ограничения

Скорость

Между любыми двумя USB-запросами выдерживается пауза 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 идёт быстрее, около 15 кадров в секунду, но в katana.py такой метод пока не реализован: read_frame() всегда делает save. В приложении для этого есть readLiveFrame(), а перенос метода в Python-версию записан в открытые задачи.

Вендорская утилита анимирует отдельные клавиши через отдельный канал стриминга 04 20: без save, около 15 кадров в секунду. В katana.py этот канал пока не реализован. Эффекты с быстрым движением (бегущая строка) будут дёргаными; хорошо смотрятся медленные волны, дыхание, плавные переливы.

Флеш-память

save=True записывает настройку во флеш, у которого ограниченный ресурс перезаписи.

Правила:

  • в цикле анимации всегда используйте save=False;
  • последняя сохранённая настройка восстанавливается при включении клавиатуры;
  • для экспериментов из оболочки используйте --no-save.

Несохранённая сессия не вредит: следующая команда откроет её заново, а begin ответит ff.

Не пишите во флеш каждый кадр

Ресурс перезаписи флеш-памяти ограничен. Анимация с save=True в цикле быстро его израсходует.

Слепые слоты

39 слотов из 143 не подключены ни к одному светодиоду. Прошивка молча принимает в них цвет, но свечения не будет.

Приём: разовая раскраска

#!/usr/bin/env python3
"""Двухцветная раскраска: буквам один цвет, остальным другой."""
from katana import Katana, KEYS_COUNT, resolve_key

LETTERS = {resolve_key(letter) for letter in "qwertyuiopasdfghjklzxcvbnm"}


def make_table(base, accent):
    """143 кортежа (r, g, b): буквам accent, остальным base."""
    return [accent if index in LETTERS else base for index 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)
finally:
    keyboard.close()

save выполняется один раз, в show_custom. Таблица остаётся несохранённой, но горит до выключения клавиатуры.

Приём: анимация

#!/usr/bin/env python3
"""Синяя волна пробегает по цифровому ряду слева направо."""
import math
import time

from katana import Katana, KEYS_COUNT, resolve_key

ROW = [resolve_key(key) for key in "`1234567890-="]
FRAME_PAUSE = 0.2

keyboard = Katana()
try:
    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 position, index in enumerate(ROW):
            wave = max(0.0, math.cos((position - phase) * math.pi / 6))
            table[index] = (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()

Советы:

  • меняйте за кадр всю таблицу целиком;
  • не пишите save внутри цикла;
  • гасите таблицу в начале, иначе волна наложится на старую раскраску;
  • для дыхания масштабируйте цвет всех клавиш одним коэффициентом.

Приём: дорисовка поверх кадра

frame = keyboard.read_frame()
table = list(frame)
table[resolve_key("esc")] = (255, 0, 0)
keyboard.write_key_table(table, save=False)

Так работают индикаторы: Caps Lock красным, заряд беспроводного режима зелёным, напоминание о перерыве.

Ограничение: в режиме 0x80 кадр — это сама таблица, поэтому наложить своё поверх встроенного эффекта прошивки нельзя.

Приём: сценарии из оболочки

#!/usr/bin/env python3
"""./my_fx.py wave|breath — выбор сценария первым аргументом."""
import sys


def wave(keyboard):
    ...


def breath(keyboard):
    ...


SCENARIOS = {"wave": wave, "breath": breath}

if __name__ == "__main__":
    from katana import Katana

    name = sys.argv[1] if len(sys.argv) > 1 else "wave"
    keyboard = Katana()
    try:
        SCENARIOS[name](keyboard)
    finally:
        keyboard.close()

Цвета принимайте через parse_color из katana: так скрипт поймёт и ff8800, и darkorange.

Грабли

  • pyusb с detach_kernel_driver ломает печать: используйте hidraw.
  • Долгая висящая сессия клинит прошивку; лечение — reset.
  • Не сокращайте паузу 0,04 секунды.
  • read_frame() возвращает цвета, умноженные на яркость: максимум ee, а не ff.
  • Слот 0x6d всегда читается как 000000.

Идеи

  • индикатор загрузки процессора или памяти;
  • подсветка уведомлений;
  • таймер работы и перерыва;
  • плавный рассвет;
  • визуализация музыки;
  • подсветка зоны WASD в играх.

Открытые направления протокола перечислены в разделе Реверс-инжиниринг.