Пульт от телевизора LG как контроллер для мака и агентов Claude Code
Как я спарил LG Magic Remote MR25GA с маком по BLE, почему macOS отдаёт его кнопки только опросом и что получилось из пульта для агентов за полтора месяца.
- Опубликовано
- Проекты
- nik-node
- Теги
- nik-node, claude-code, ble, reverse-engineering
С агентами Claude Code я больше говорю, чем печатаю. Это удобно, но рядом всё равно нужна клавиатура: подтвердить, отменить, пролистать, переключить рабочий стол. Мне хотелось делать это пультом в руке, не тянясь к клавиатуре.
Пульт я долго выбирал. Искать начал с весны. В апреле искал маленький пульт в руку для диктовки и хоткеев и смотрел на 8BitDo Micro, 8BitDo Zero 2 и Satechi R2. В мае думал о голосовом пульте для дома: M5StickC Plus2 с кнопками, IMU и микрофоном или самодельная плата на ESP32-S3 или nRF с микрофоном и push-to-talk. 3 сентября я сформулировал требования: колесо с щелчками под большой палец, четыре направления, кнопки, желательно микрофон и возможность перепрошить. Перебрал Elecom Relacon, Joy-Con, TourBox, Surface Dial, air-mouse и кольца со скроллом с AliExpress. Один универсальный пульт я уже заказывал, но в нём не было колеса, а колесо для скролла я считаю очень удобным и важным элементом интерфейса. Вспоминал и свой старый пульт от телевизора лет пяти-семи назад: эргономичная форма, но очень тяжёлый, чтобы заказывать его отдельно. Оригинал такого на eBay стоит 40–80 долларов.
Выбор пал на новый LG Magic Remote MR25GA: прямоугольный и, что классно, достаточно лёгкий. В нём есть гироскоп, колесо и больше двадцати кнопок. Решило ещё то, что для старшей модели MR20 уже есть Linux-драйвер brainrom/lg-magic: пульт виден как обычное HID-устройство, а гироскоп отдаёт сырые данные. Главный риск при покупке — ИК-клоны без гироскопа и Bluetooth, поэтому брал оригинал. С доставкой за два дня он обошёлся в 20 евро.
Задачу я поставил 6 сентября: не управлять телевизором, а сделать из пульта устройство ввода для мака. Гироскоп двигает курсор, колесо скроллит, кнопка AI — микрофон.
Как устроен сам голосовой кластер и главная сессия, которая слушает мою речь, я рассказал в посте про домашний кластер. Здесь только про пульт.
Спаривание: мак пульт не видит
В окне Bluetooth пульта нет: в его рекламе нет ни HID-сервиса, ни appearance. Пришлось инициировать соединение скриптом на CoreBluetooth. Тогда macOS показывает диалог подтверждения, и нажать в нём Connect нужно примерно за 1,5 секунды, потому что пульт рвёт связь через 2,7 секунды. Получилось с третьей попытки. До этого пульт ещё надо было сбросить: Home и Back держать больше 5 секунд.
Подход к BLE я взял из своего кода для кольца Colmi. Спаренное BLE-HID-устройство macOS держит сам, рекламироваться оно перестаёт, и достать его можно только среди уже подключённых. Внутри пульта, кстати, чип Realtek RTL8762, а не что-то от LG.
Почему опрос, а не события
Первым делом я попробовал слушать события пульта обычным путём, через hidapi и через IOHIDManager на Swift. Ноль событий, хотя кнопки я нажимал. Заработал только опрос: раз за разом запрашивать у пульта input-репорт 0xFD. Получилось около 60 семплов в секунду.
Почему так, я понял через Android-телефон с nRF Connect и HCI snoop. Сам пульт шлёт 0xFD на 100 Гц, но macOS не включает для него уведомления: в дампе bluetoothd у всех трёх input-репортов подписка пустая. Интервал соединения 15 мс, на один запрос уходит один интервал, отсюда потолок около 66 опросов в секунду. Щелчок колеса живёт в репорте 10 мс, поэтому ловится примерно 2/3 щелчков. Полный поток даст только другой BLE-хост: Linux с BlueZ или своя плата на ESP32 или nRF.
На Linux это уже подтвердилось: BlueZ сам включает уведомления, и пульт шлёт 0xFD все 100 раз в секунду, без опроса. Для мини-ПК я уже купил Bluetooth-донгл. Возможно, проще будет сделать сервис пульта на мини-ПК в своей экосистеме, чем дальше городить костыли на маке.
С микрофоном в пульте так же. Голос идёт отдельным репортом 0xFE, и ни мак, ни Android его не включали. В тот же вечер я решил остановиться: голос с пульта мне не нужен, микрофон и так всегда в наушниках. Кнопка AI просто включает запись с них.
Разгадка нашлась только 25 октября, и не в эфире. Голос — не поток, который можно подслушать, а режим, который включает телевизор: хост шлёт пульту два байта F9 03, и через 70 мс идёт звук, 66 пакетов в секунду, кодек mSBC, как у Bluetooth-гарнитур. Протокол вытащил из прошивок телевизоров LG сторонний разработчик в PR #8 к brainrom/lg-magic. На моём MR25GA под Linux он подтвердился: полторы минуты без единой потери, речь записалась. Как это устроено, рассказал в следующем посте.
Кнопки под агентов
Раскладка выросла из того, что мне мешало. В первую же ночь выяснилось, что Enter нажать нечем: «я не понимаю, как мне нажимать Enter». Кнопка «123» стала Enter, то есть «принять». Back стал Escape. Потом понадобилось выделять текст: короткое нажатие колеса — клик, а если держать его неподвижно 0,4 секунды в терминале, сначала зажимается Alt, и можно выделять мышью.
Режимы поначалу раскладывались по кнопкам, и это было, как я тогда сказал, «слишком расточительно для пульта, там где мало кнопок». 15 сентября режимы стали независимыми флагами. Каждый флаг накладывает свои кнопки поверх базовых, а переключаются они на одном экране. В тот же день логика пульта переехала из отдельного демона в узел кластера. 3 октября меню на Hammerspoon я заменил панелью агентов. Пока она открыта, стрелки, OK, колесо, Back и Guide уходят в неё.
Сейчас карта такая:
| Кнопка | Что делает |
|---|---|
| Колесо, нажатие | клик; сдвинул курсор больше 8 px — перетаскивание; держишь неподвижно 0,4 с в терминале — выделение с Alt. Если курсор погашен, первое нажатие только будит его |
| Колесо, вращение | скролл на 3 строки; с флагом «слайды» — стрелки ← → |
| Стрелки | стрелки; в панели — переход по колонкам и строкам |
| Back | Escape; держать 1 с — вернуть микрофон главной сессии |
| 123 | Enter |
| Домик | показать или спрятать панель агентов |
| Домик с кружком | панель сразу на переключателях и состоянии |
| Guide | F1; в панели — переход к сессии, которая ждёт меня |
| ? | пауза речи агента, двойное нажатие — повтор фразы; держать 1 с — калибровка гироскопа |
| AI | с флагом live_mic — держи и говори; сейчас флаг выключен, и кнопка дёргает диктовку Handy на нажатии и на отпускании |
| prime video | включить или выключить курсор; держать 0,4 с — держи и говори |
| Шестерёнка | правый клик |
| Громкость + / − | громкость мака |
| Каналы ∧ / ∨ | Page Up / Page Down |
| NETFLIX | Backspace; при удержании после 6 символов стирает по словам |
| Disney+ / Rakuten TV | ⌘− / ⌘+ (масштаб в окне) |
| LG Channels / alexa | рабочий стол влево / вправо |
| Power | по BLE ничего не шлёт, только ИК |
Что ломалось
- Залипший Alt. Всю ночь 7 сентября меню вело себя так, будто зажат модификатор. Оказалось, флаги каждого синтетического события (Fn у стрелок, Alt у выделения) копятся в системном состоянии. Лечит пустое движение мыши с нулевыми флагами. В 02:57 я написал: «Заработало отлично».
- Рабочие столы. Mission Control игнорирует синтетический Ctrl+← из CGEvent во всех шести вариантах, которые мы перепробовали. Переключение работает через System Events.
- Меню отваливалось. «Опять все зависло… менюшка открытая, и тут ничего не работает». Мост в Hammerspoon жил в локальной переменной, сборщик мусора его собирал, и порт закрывался через минуту после загрузки. Теперь мост держится в глобальной.
- Автокалибровка. «Работал нормально, потом бах — он перекалибровался». Автоподстройку смещения гироскопа убрали, осталась ручная: положить пульт и подержать «?» секунду.
- Отваливается с зажатой AI. 13 октября я зажал AI, пульт пропал, а запись продолжилась. Выяснилось, что через 4–8 секунд удержания AI пульт сам рвёт BLE. Видимо, включает голосовой канал для телевизора. Теперь, если пульт пропал с зажатой кнопкой и не вернулся за 8 секунд, узел отпускает её сам, и запись уходит ходом. Для длинных фраз появилось долгое нажатие prime video.
- Батарейки. Уровень заряда пульт отдаёт мусором, −26 и −28 %. С новыми батарейками то же самое, так что сколько осталось заряда, я не вижу.
- Безопасность моста. Мост, через который пульт нажимает клавиши, слушает на локальном адресе. Без защиты любая страница в браузере могла бы нажимать клавиши за меня. Теперь на браузерные запросы он отвечает 403.
Агент забывал, как мы настраивали
Главная проблема оказалась не в железе. Пульт по очереди настраивали четыре сессии, и правки иногда шли из нескольких сразу. 14 сентября две сессии одновременно перезапустили демон, у меня подвисло меню и пропал клик. С тех пор пульт правит одна сессия.
Документы отстают от конфига. Когда я собирал этот пост, оказалось, что старая карта кнопок расходится с настоящей в семи строках. Флаг live_mic, без которого AI не говорит в узел, с 15 октября выключен, хотя по умолчанию включён, и по логу не понять, кто его выключил. А файла калибровки нет вовсе, смещение гироскопа нулевое. Курсор при этом ведёт себя нормально, так что, возможно, калибровку мы когда-то отключили специально, но записи об этом я не нашёл.
Что я на самом деле нажимаю
По логу старого демона с 6 по 15 сентября: колесо — 412 нажатий, стрелка вниз — 249, AI — 84. Enter сработал 109 раз, Escape — 37. Основные кнопки: колесо, AI, стрелки, рабочие столы, Enter, Back и prime video для курсора.
С середины октября я пульт почти не беру. Последнее нажатие в логе узла — примерно 20 октября.
Что не решено
- Колесо пропускает примерно треть щелчков. На маке это не лечится, на Linux поток полный.
- Микрофон пульта на маке недоступен. На Linux он работает, моста в мак пока нет.
- Удержание AI рвёт связь через 4–8 секунд. Раньше удержание держалось до 40 секунд, почему сейчас иначе — не выяснил.
- Заряда не видно.
- Фиксы колеса в браузере от 17 октября вживую не проверены.
Плюс пульт спаривается с одним хостом и работает только в радиусе BLE от мака. И он большой: держать его в руке постоянно неудобно.
Библиотека
Код пульта жил внутри узла: модуль на 816 строк. Я вынес его в отдельную библиотеку с версиями и документацией, чтобы им мог пользоваться любой владелец MR25GA с маком. Библиотека называется gyromote, в имени нет «LG», потому что это товарный знак. Первая версия — 0.1.0, узел уже работает на ней: в нём осталась обёртка на 174 строки, которая связывает пульт с панелью агентов, микрофоном и речью узла. Репозиторий: github.com/liknenS/gyromote.
Раскладку репорта и коды кнопок я снимал сам на живом пульте. Ближайший готовый проект — драйвер ядра Linux brainrom/lg-magic под GPL-2.0, проверенный на MR20. Код оттуда я не брал.
Что стоит знать о рисках:
- Пульт можно окирпичить. В пульте есть сервис обновления прошивки Realtek. Запись в его характеристики может перевести пульт в режим прошивки. Библиотека читает пульт только опросом репортов, а этот путь до сервиса прошивки не достаёт вообще. Мой исследовательский скрипт, который при спаривании обходит все сервисы, в библиотеку не попал.
- Права Accessibility. Чтобы нажимать клавиши, процессу нужно право Accessibility, а с ним он может нажать что угодно. Выдавать право стоит конкретному интерпретатору в отдельном окружении, а не терминалу.
- Конфиг выполняет команды. Кнопку можно повесить на
shell:с любой командой. Значит, конфиг — доверенный ввод: кто может его править, тот может запускать команды от вашего имени. Сетевого входа у библиотеки нет, сокет и мост панели остались в моём узле. Конфиг, который может править кто угодно, библиотека не загрузит.
Что дальше
Флаг «слайды» уже есть: колесо листает стрелками. Потенциально так можно будет показывать с пульта презентации.
Дальше я думаю о трёх вещах вместо пульта или рядом с ним:
- Без рук. Колонка с массивом микрофонов и жёсткие команды вроде «включи», «выключи», «приём», «дальше».
- Кольцо с физической кнопкой. Главный вопрос — батарейка: паять самому, заказывать или искать готовое.
- Маленький браслет или тонкая полоска сантиметров семь с парой кнопок.
Из всех кнопок главными для меня остались три: принять, Escape и микрофон. Их можно перенести на наушники: перехватывать медиаклавиши моих Sony или OnePlus, чтобы одиночное, двойное, тройное нажатие или зажатие включало и выключало запись. Это режим «без рук» вообще без пульта.