mandrock-files / tools

debloat-api-desk

Периферія й домашня інфра як програмований шар: OpenRazer D-Bus, QMK raw HID, Syncthing REST+events, OpenMediaVault JSON-RPC. По кожному — транспорт, автентифікація, робочий приклад, сценарій і граблі. Фінал серії про локальні API.

Локальні API замість вендорської хмари: робочий стіл

Третя, фінальна частина серії за каталогом debloat.dev. Тема — залізо на столі та вдома: клавіатура, миша, підсвітка, синхронізація файлів, домашній NAS. Не «фірмовий комбайн у треї, який стартує з системою і живе фоновим процесом на 300 МБ», а периферія, якою керує скрипт: підсвітка реагує на подію збірки, клавіатура шле команду в прошивку, синхронізація рапортує про кожен долитий файл, NAS налаштовується однією командою в терміналі.

Спільний знаменник із двома попередніми частинами той самий: софт уже вміє все локально, питання лише в тому, чи є до цього програмний вхід повз GUI. Тут він є всюди — але транспорт у кожного інший: D-Bus, сирий USB HID, HTTP REST і JSON-RPC. Чотири різні світи на одному столі.


Рамка: що обрати під задачу

Що є Що взяти Транспорт
Razer-периферія на Linux, треба підсвітка з коду OpenRazer D-Bus org.razer
QMK/VIA-клавіатура, треба двосторонній канал хост↔прошивка QMK raw HID сирий USB HID
Синхронізація файлів між машинами, треба реагувати на зміни Syncthing REST + events HTTP REST
Домашній NAS, треба конфігурувати його зі скрипта OpenMediaVault JSON-RPC / omv-rpc

OpenRazer (D-Bus)

Що замінює. Razer Synapse — хмарний конфігуратор, який на Linux не працює взагалі, а на Windows тягне акаунт і фоновий сервіс. OpenRazer — це драйвери ядра + демон (openrazer-daemon), який виставляє всю периферію на сесійній шині D-Bus під сервісом org.razer. Підсвітка, DPI, профілі, частота опитування — усе доступне як D-Bus-методи, а поверх них є чиста Python-обгортка. GUI Polychromatic — це теж лише клієнт до цього самого демона, не окремий стек; усе, що вміє Polychromatic, доступно й зі скрипта.

Транспорт і авт. D-Bus, сесійна шина (не системна), сервіс org.razer. Окремого токена нема — доступ дає належність до сесії користувача: демон крутиться від імені юзера, і будь-який його ж процес до нього ходить. Умова одна — користувач у групі plugdev, інакше драйвер не віддасть пристрій. Демон має бути запущений (systemctl --user status openrazer-daemon або автостарт при першому зверненні бібліотеки).

Робочий приклад. pip install openrazer (бібліотека openrazer, поверх драйверів із репозиторію дистрибутива):

from openrazer.client import DeviceManager

dm = DeviceManager()
for device in dm.devices:
    print(device.name, device.type)          # 'Razer BlackWidow V3', 'keyboard'

kbd = dm.devices[0]

# готові апаратні ефекти — рахуються на самому девайсі
kbd.fx.static(0, 255, 0)                      # суцільний зелений, канали 0..255
kbd.fx.spectrum()                             # циклічний перелив
kbd.fx.wave(1)                                # хвиля; напрямок WAVE_LEFT/WAVE_RIGHT
kbd.fx.reactive(255, 0, 0, 2)                 # підсвічує натиснуту клавішу; time 1..3

# власний кадр: покласти колір у кожну клавішу матриці й відмалювати
matrix = kbd.fx.advanced.matrix
matrix.reset()
for col in range(kbd.fx.advanced.cols):
    matrix[0, col] = (255, 0, 255)            # matrix[row, col] = (r, g, b)
kbd.fx.advanced.draw()                        # шле кадр по D-Bus (setKeyRow + setCustom)

fx.* — це апаратні ефекти, які девайс рахує сам. fx.advanced.matrix[row, col] = (r,g,b) + draw() — покадрове малювання, де кожен піксель задає хост: draw() серіалізує матрицю в setKeyRow і застосовує setCustom через D-Bus. Це і є точка, де пишуться власні анімації.

Сценарій. Демон-скрипт слухає власну подію (успіх/провал CI, вхідний пуш, стан батареї) і кидає fx.static() відповідного кольору на клавіатуру: зелений — білд зелений, червоний — впав. Для біжучих ефектів — цикл, що раз на N мілісекунд оновлює advanced.matrix і викликає draw(). Хмари й акаунта в ланцюгу нема — лише локальна D-Bus-петля.

Граблі. - Шина сесійна, не системна. Скрипт із-під cron/systemd-system не побачить org.razer, бо в нього нема сесійного D-Bus. Запускати як systemd --user-юніт або з правильним DBUS_SESSION_BUS_ADDRESS. - fx.advanced є не в кожного пристрою — лише в тих, що мають адресовану матрицю (per-key RGB). На мишці чи килимку з однією зоною advanced.matrix не працює; перевіряти device.has('lighting_led_matrix') перед draw(). - Немає користувача в plugdev → девайс просто не зʼявиться в dm.devices, без явної помилки. Це грабля номер один при першому запуску.

Джерела: openrazer.github.io, github.com/openrazer/openrazer (pylib), polychromatic.app.


QMK / VIA (raw HID)

Що замінює. Пропрієтарний конфігуратор клавіатури (Razer Synapse, Logitech G Hub, Corsair iCUE) — але глибше, ніж решта добірки. Тут заміняється не хмара, а сама межа між хостом і залізом: QMK-прошивка може відкрити довільний двонапрямний канал RAW_ENABLE, і хост шле в мікроконтролер клавіатури власні команди, а прошивка відповідає. Це не ремап. Ремап (у тому числі через VIA) міняє те, який скан-код віддає клавіша — таблицю в прошивці. Raw HID — це окремий канал даних поверх USB, яким їздить що завгодно: «покажи на дисплеї клавіатури поточний трек», «підсвіти шар червоним, бо в докері помилка», «віддай хосту стан енкодера». Ремап відповідає на питання «що робить клавіша», raw HID — «що прошивка й застосунок кажуть одне одному».

Транспорт і авт. Сирий USB HID, окремий інтерфейс. Автентифікації нема взагалі — це локальна USB-шина, фізичний доступ і є доступ. Канал визначається двома константами (config.h): RAW_USAGE_PAGE = 0xFF60, RAW_USAGE_ID = 0x61. Хост знаходить потрібний інтерфейс саме за цією парою, а не лише за VID/PID (клавіатура виставляє кілька HID-інтерфейсів). Усі репорти в обидва боки — рівно 32 байти (RAW_EPSIZE).

Робочий приклад. Бік прошивки — в rules.mk:

RAW_ENABLE = yes

І колбек у keymap.c (викликається на кожен вхідний репорт; length завжди 32):

#include "raw_hid.h"

void raw_hid_receive(uint8_t *data, uint8_t length) {
    // data[0] — власний код команди; ехо назад на хост
    switch (data[0]) {
        case 0x01:
            // ... зробити щось: перемкнути шар, оновити екран ...
            break;
    }
    raw_hid_send(data, length);   // відповідь мусить бути теж 32 байти
}

Бік хоста — Python, pip install hid (обгортка hidapi):

import hid

VID, PID = 0x4653, 0x0001     # з info.json клавіатури
# фільтрувати САМЕ за usage_page/usage, не лише за VID/PID:
path = next(d["path"] for d in hid.enumerate(VID, PID)
            if d["usage_page"] == 0xFF60 and d["usage"] == 0x61)

h = hid.Device(path=path)
report = bytes([0x01] + [0] * 31)      # 32 байти корисних даних
h.write(b"\x00" + report)              # провідний 0x00 — Report ID (див. граблі)
print(h.read(32, timeout=1000))        # відповідь від raw_hid_send

Сценарій. Демон на хості раз на секунду шле в клавіатуру 32-байтовий пакет зі станом системи (навантаження CPU, чи зелений CI, скільки непрочитаних) — прошивка малює це на OLED або підсвіткою шару. У зворотний бік енкодер шле хосту raw_hid_send, а слухач на хості мапить це на гучність. Один USB-кабель, нуль мережі.

Граблі. - Провідний Report ID. На більшості ОС write() очікує першим байтом Report ID (0x00), тобто фізично йде 33 байти (1 + 32), хоча прошивка бачить рівно 32. Забути \x00 — репорт не піде або поїде зсунутим. - Фільтрувати за usage_page/usage, а не за VID/PID. Клавіатура виставляє кілька інтерфейсів (keyboard, mouse, raw); відкриєш не той — write мовчки нікуди. - VIA займає той самий канал. З VIA_ENABLE = yes функцію raw_hid_receive уже використовує сам VIA-протокол. Щоб додати свої команди, не переписуючи VIA, — хук raw_hid_receive_kb або власний діапазон у VIA custom-командах, інакше свій колбек перебʼє ремап через застосунок VIA. - Усе строго по 32 байти в обидва боки; більший payload — самому різати на кілька репортів.

Джерела: docs.qmk.fm/features/rawhid, docs.qmk.fm — VIA.


Syncthing (REST + events)

Що замінює. Dropbox, Google Drive, OneDrive — будь-яку хмару синхронізації файлів. Syncthing тримає P2P-синхронізацію без жодного центрального сервера, і поверх неї віддає повний HTTP REST API — той самий, яким користується його ж веб-GUI. Два режими під дві задачі: разові запити (GET /rest/db/status — скільки долити, чи все синхронізовано) і потік подій (GET /rest/events — реагувати на кожен долитий файл без опитування).

Транспорт і авт. HTTP(S), порт 8384 (той самий, що GUI). Ключ — заголовок X-API-Key: <ключ> (або Authorization: Bearer <ключ>). Звідки ключ: GUI → Actions → Settings → поле API Key (кнопка генерує новий), або напряму в config.xml, елемент <gui><apikey>…</apikey></gui>. Ключ дає повний адмінський доступ до інстансу.

Робочий приклад.

KEY=abc123DEFyourapikeyhere
BASE=http://localhost:8384

# стан конкретної папки: скільки байт лишилось долити, скільки файлів
curl -H "X-API-Key: $KEY" "$BASE/rest/db/status?folder=default"
# -> {"globalBytes":..., "needBytes":..., "state":"idle", ...}

# уся конфігурація (сучасний ендпоінт, з 1.12.0; старий /rest/system/config задепрекейчено)
curl -H "X-API-Key: $KEY" "$BASE/rest/config"

# окрема папка / девайс — редагується точково
curl -H "X-API-Key: $KEY" "$BASE/rest/config/folders/default"

Потік подій — довгий полінг: запит блокується, поки не зʼявляться нові події або не спливе timeout (дефолт 60 с), і віддає масив, де кожна подія має id, type, time, data. Наступний запит іде з since=<останній id>:

import requests

BASE = "http://localhost:8384"
H = {"X-API-Key": "abc123DEFyourapikeyhere"}
last = 0
while True:
    r = requests.get(f"{BASE}/rest/events",
                     params={"since": last, "events": "ItemFinished"},
                     headers=H, timeout=70)
    for ev in r.json():
        last = ev["id"]
        d = ev["data"]
        print(d["folder"], d["item"])        # яка папка, який файл долито

Параметри /rest/events: since=<id> (віддати все після цього id), limit=, events=<через кому> (фільтр типів), timeout=. Корисні типи: ItemStarted/ItemFinished (файл), FolderCompletion/FolderSummary (прогрес папки), StateChanged, DownloadProgress, LocalIndexUpdated.

Сценарій. Слухач тримає один запит на /rest/events?events=ItemFinished і на кожен долитий файл запускає обробку: прийшло нове фото в синк-папку — прогнати через тег-скрипт; прийшов новий .md — перебілдити сайт. Замість cron, що раз на хвилину сканує папку, — миттєва реакція на факт долиття, з нульовим полінгом файлової системи.

Граблі. - GUI/REST за замовчуванням слухає лише 127.0.0.1:8384. Для доступу з іншої машини — або підняти адресу в налаштуваннях GUI, або (краще) реверс-проксі; голий порт у LAN/інтернет = адмінський API назовні. - API-ключ = повний адмін. Ним можна переписати конфіг і додати чужий девайс у синхронізацію. Не в git, не в публічний порт. - /rest/events без since віддасть історію від старту й може одразу вивалити пачку старих подій; для «тільки нове» передати since= останнього відомого id (або спершу зробити запит із limit=1, щоб дізнатись поточний id). - Старий /rest/system/config ще відповідає, але задепрекейчений на користь /rest/config — нове писати на новий.

Джерела: docs.syncthing.net/dev/rest, docs.syncthing.net/rest/config, docs.syncthing.net/rest/events.


OpenMediaVault (JSON-RPC)

Що замінює. Пропрієтарні NAS-прошивки (Synology DSM, QNAP QTS) з їхніми закритими API. OpenMediaVault — Debian-базований NAS, і весь його веб-інтерфейс говорить із бекендом через один JSON-RPC-ендпоінт /rpc.php. Тобто будь-яка дія в UI — це виклик service/method/params, і той самий виклик доступний ззовні: створити спільну папку, підняти SMB-шару, зібрати RAID, глянути змонтовані ФС — усе програмно.

Транспорт і авт. HTTP, веб-порт OMV. Тіло POST /rpc.php — JSON {"service","method","params","options"}; відповідь — {"response": …, "error": …}. Авт сесійна, на кукі: спершу POST /rpc.php з service:"session", method:"login" віддає кукі сесії, і всі подальші виклики йдуть із цим кукі. Це головна незручність сирих викликів — треба вести cookie-jar, а в новіших версіях ще й пробрасувати CSRF-токен сесії, і назва сервісу логіну між версіями плавала (session/Session).

Робочий приклад — сирий JSON-RPC (як зразок, чому так робити не треба):

BASE=http://nas.local

# 1) логін — зберегти кукі сесії
curl -c cookies.txt -X POST "$BASE/rpc.php" \
     -H "Content-Type: application/json" \
     -d '{"service":"session","method":"login","params":{"username":"admin","password":"<PASS>"}}'
# -> {"response":{"authenticated":true,"username":"admin"},"error":null}

# 2) виклик методу з тим самим кукі
curl -b cookies.txt -X POST "$BASE/rpc.php" \
     -H "Content-Type: application/json" \
     -d '{"service":"FileSystemMgmt","method":"enumerateMountedFilesystems","params":{"includeroot":true},"options":null}'

Той самий результат через omv-rpc (локально на NAS):

# ані логіну, ані кукі, ані CSRF — авт бере з системного користувача
omv-rpc -u admin 'FileSystemMgmt' 'enumerateMountedFilesystems' '{"includeroot": true}'
omv-rpc -u admin 'RaidMgmt' 'getCandidates' | jq

omv-rpc викликає ті самі service/method/params, що й веб-GUI, але обходить весь HTTP-шар авт: логін, кукі, токен. Для скрипта, що крутиться на самому NAS (cron, деплой, бекап-хук), це єдиний осудний шлях — сирий curl виправданий лише коли керувати треба з іншої машини.

Сценарій. Скрипт розгортання нового шара: omv-rpc … 'ShareMgmt' 'set' '{…}' створює спільну папку, далі 'SMB' 'setShare' піднімає над нею SMB, і 'Config' 'applyChanges' застосовує зміни — увесь провіжининг NAS у version-controlled bash-файлі замість двадцяти кліків у вебі. Перелік доступних service/method — у теці engined/rpc вихідників OMV.

Граблі. - Сесія на кукі + CSRF. Сирий curl мусить вести cookie-jar (-c/-b); один запит без кукі після логіну → session not authenticated. Саме тому локально — omv-rpc, не curl. - Назва сервісу логіну й дрібниці протоколу різнилися між мажорами OMV — приклад із чужої версії може не завестись; канон завжди — engined/rpc конкретної інсталяції. - Історично rpc.php мав RCE-вразливості (напр. CVE-2013-3632) — веб-морда OMV не для публічного інтернету; тримати за VPN/реверс-проксі й свіжою версією. - applyChanges — окремий крок: багато set-методів лише пишуть у конфіг, зміни не діють, поки не застосовано.

Джерела: docs.openmediavault.org — omv-rpc, docs.openmediavault.org — RPC, github.com/openmediavault/openmediavault.


Спільні граблі всіх чотирьох


Порівняльна таблиця

Проєкт Транспорт Авт Події (push) Стабільність між мажорами
OpenRazer D-Bus (сесійна шина) членство в сесії + plugdev сигнали D-Bus помірна; матрична частина залежить від моделі
QMK raw HID сирий USB HID нема (фізичний доступ) так — двонапрямний канал контракт визначає твоя ж прошивка
Syncthing HTTP REST X-API-Key (повний адмін) так — /rest/events long-poll висока; старий config-ендпоінт депрекейтнуто плавно
OpenMediaVault JSON-RPC /rpc.php сесійний кукі (+CSRF); або omv-rpc ні — полінг назви сервісів плавали між мажорами

Фінал серії: як відрізнити реальний API від «просто локального»

Три частини серії пройшли по чотирнадцяти проєктах, і закономірність вилазить одна: «локальний» і «програмований» — не синоніми. Купа софту працює без хмари, але керується виключно пальцем по GUI; а частина віддає інтерфейс, на який справді можна покласти автоматизацію. Ознака других — не маркетинг «open & local», а конкретні технічні риси. Чекліст, за яким проєкт перевіряється до того, як вкладати в нього вечір:

Проєкт, що бере 5–6 з 6, — це фундамент, на який можна писати роками. 2–3 з 6 — «локальний», але автоматизувати доведеться реверс-інжинірингом і з ризиком, що наступний реліз усе перепише. 0–1 — це GUI, який просто не ходить у хмару; програмованим його називати не можна.


Серія

P.S. — про «фонову службу»

Вендорський конфігуратор периферії любить назватися «фоновою службою», ніби це скромний помічник, що тихо сидить у треї. По факту це процес, який стартує з системою, тримає з'єднання з акаунтом, і без якого не міняється колір підсвітки на клавіатурі, за яку вже заплачено повністю.

D-Bus-виклик, 32-байтовий HID-репорт чи omv-rpc у cron — це те саме керування залізом, тільки без посередника, який має власну думку щодо того, чи варто йому сьогодні запускатись. Клавіатура світиться зеленим, бо скрипт сказав їй світитися зеленим, а не бо хмара дозволила. Ціна питання — розуміння, що шина D-Bus сесійна, а перший байт HID-репорту — це Report ID, який прошивка навіть не побачить.

MANDROCK.DEV · TELEGRAMЩе більше про код, агентів і те, що ламається красиво.Зазирнути в канал →