Runtime-интроспекция закрытого Klipper-модуля

Вендорские форки Klipper возят extras скомпилированными .so (Cython) — исходников нет, но их объекты живут в обычном реестре (printer.lookup_object) и полностью прозрачны для рефлексии Python. Самый быстрый способ узнать реальный API — не декомпиляция, а временный extra с dir()-дампом, загруженный на сам принтер.

Рецепт

  1. Написать одноразовый extra (у меня — cs1237_probe_debug.py), который регистрирует команду дампа: обойти объект dir()-ом рекурсивно (глубина ~3), для callable — inspect.signature (у Cython часто падает — ловить и писать (?)), для примитивов — значения, всё в файл на принтере. Обязательно: стоп-лист атрибутов (printer, reactor, gcode, mcu, toolhead) — иначе обход утянет весь граф объектов Klipper; и set посещённых id против циклов.
  2. Положить в klippy/extras/, добавить секцию в конфиг, рестарт сервиса (plain RESTART extras не перечитывает), выполнить команду, забрать файл, удалить секцию.
  3. Отдельная команда-поисковик: рекурсивно найти атрибут по имени класса (у меня 'cs1237' in type(v).__module__) — отдаёт точный путь до нужного объекта. Мой результат: probe_air.sensor_helper -> cs1237.CS1237 плюс полный список методов с сигнатурами (read_origin_data(), setup_home(print_time, trsync_oid, hit_reason, err_reason, rest_time), атрибуты oid=5, bulk_queue, командные обёртки MCU).

Дамп 210 строк дал мне за один запуск то, что реверс-инжиниринг по strings собирал неделями — включая значения конфига, имена command-обёрток MCU-протокола и attribute-пути, которых в символах .so нет вовсе.

Декомпиляция и шаблоны ВРУТ — проверять экспериментом

Три случая из одного дня, где «знание» из реверса/шаблона опроверглось живьём:

  1. add_client оказался мёртвым кодом. Модуль — переименованный мейнлайновый probe_eddy_current, по шаблону подписка add_client(cb) должна автозапускать поток сэмплов. Живьём: подписка есть, колбэков ноль — даже во время НАСТОЯЩЕГО промера G28 (0 сообщений при активном клиенте). Вендор вырезал раздачу глубже, чем видно по классам.
  2. Имена команд из декомпиляции устарели. Реверс-док давал CS1237_WEIGHT_BEGIN/WEIGHTING_CONFIG_READ; живой GET /printer/gcode/helpCS_WEIGHT_BEGIN/CS_WEIGHT_READ_CONFIG. Вендор переименовал между версиями (видимо, наступив на цифры в именах — см. ниже). Истина — только реестр живой прошивки.
  3. MCU-протокол известен, но не воспроизводим снаружи. Команды query_cs1237_begin(config) / query_cs1237 rest_ticks=N отправляются, чип отвечает ACK ({'oid': 5, 'config': 60}), но bulk-очередь остаётся пустой — старт потока завязан на внутренний путь, который снаружи не повторить малой кровью. Гипотеза «протокол виден в strings → поток мой» не пережила проверку.

Вывод-правило: каждая гипотеза о закрытом коде получает дешёвый живой эксперимент до того, как на неё что-то ставится. Подписка? — подписаться и посчитать колбэки за 5 с. Команда? — вызвать на простое и прочитать gcode_store.

Гочи процесса

  • Цифры в имени G-code команды: парсер Klipper режет имя по первой цифре — моя CS1237_FIND_SENSOR регистрировалась (видна в help), но диспатчилась как несуществующая CS1237 и молча терялась. Имена временных команд — только [A-Z_] (CSDBG_FIND).
  • getattr исполняет код: обход свойств Cython-объектов может дёргать MCU. Мой первый дамп повис на 60 с — из-за паразитного совпадения с Moonraker-лагом я час искал не там; ground truth дала трассировка первой строкой хендлера в файл (/tmp/...trace.txt с fsync) — «вызвался ли вообще».
  • Прямые каналы мимо Moonraker: G-code можно вбросить в pty ~/printer_data/comms/klippy.serial (printf "CMD\n" > /dev/pts/N, ответы — cat из того же pty) или JSON в unix-сокет klippy.sock ({"id":1,"method":"gcode/script","params":{"script":"CMD"}} + байт 0x03). Незаменимо, когда Moonraker после рестарта сервиса 30–60 с молча теряет запросы.
  • Временный extra после разведки — убрать секцию из конфига (сам файл инертен: Klipper грузит extras только по секциям).

Живое применение — вскрытие датчика Q2: QIDI Q2 — закрытый API тензодатчика; общий контекст форка — Klipper — приёмы работы с вендорским форком QIDI.