Runtime-интроспекция закрытого Klipper-модуля
Вендорские форки Klipper возят extras скомпилированными .so (Cython) — исходников нет, но их объекты живут в обычном реестре (printer.lookup_object) и полностью прозрачны для рефлексии Python. Самый быстрый способ узнать реальный API — не декомпиляция, а временный extra с dir()-дампом, загруженный на сам принтер.
Рецепт
- Написать одноразовый extra (у меня —
cs1237_probe_debug.py), который регистрирует команду дампа: обойти объектdir()-ом рекурсивно (глубина ~3), для callable —inspect.signature(у Cython часто падает — ловить и писать(?)), для примитивов — значения, всё в файл на принтере. Обязательно: стоп-лист атрибутов (printer,reactor,gcode,mcu,toolhead) — иначе обход утянет весь граф объектов Klipper; и set посещённых id против циклов. - Положить в
klippy/extras/, добавить секцию в конфиг, рестарт сервиса (plain RESTART extras не перечитывает), выполнить команду, забрать файл, удалить секцию. - Отдельная команда-поисковик: рекурсивно найти атрибут по имени класса (у меня
'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 нет вовсе.
Декомпиляция и шаблоны ВРУТ — проверять экспериментом
Три случая из одного дня, где «знание» из реверса/шаблона опроверглось живьём:
add_clientоказался мёртвым кодом. Модуль — переименованный мейнлайновыйprobe_eddy_current, по шаблону подпискаadd_client(cb)должна автозапускать поток сэмплов. Живьём: подписка есть, колбэков ноль — даже во время НАСТОЯЩЕГО промера G28 (0 сообщений при активном клиенте). Вендор вырезал раздачу глубже, чем видно по классам.- Имена команд из декомпиляции устарели. Реверс-док давал
CS1237_WEIGHT_BEGIN/WEIGHTING_CONFIG_READ; живойGET /printer/gcode/help—CS_WEIGHT_BEGIN/CS_WEIGHT_READ_CONFIG. Вендор переименовал между версиями (видимо, наступив на цифры в именах — см. ниже). Истина — только реестр живой прошивки. - 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.