Klipper — приёмы работы с вендорским форком QIDI

Вендоры (QIDI и др.) поставляют свой Klipper с закрытыми модулями и урезанным Moonraker без [update_manager]. Приёмы отладки такого форка через Moonraker API и SSH.

Гоча: после FIRMWARE_RESTART состояние startup — это НЕ ошибка

FIRMWARE_RESTART перезапускает Klipper + все MCU. Сразу после printer/info отдаёт:

{"state":"startup","state_message":"Printer is not ready ... The klippy host software is attempting to connect."}

Это штатная фаза переподключения MCU (на боксе с боксом-MCU ~12 с). Реальный промах: проверил состояние ОДИН раз через 16 с, увидел startup, счёл сбоем и откатил свои макросы. Перезапустил с поллингом — реальные тайминги: t=3s startup, t=6s startup, t=9s startup, t=12s ready. Правильно — опрашивать в цикле до ready, отличая startup (ждать) от error/shutdown (реальный сбой, читать state_message):

for i in $(seq 1 20); do sleep 3
  S=$(curl -s http://IP:7125/printer/info | python3 -c "import sys,json;print(json.load(sys.stdin)['result']['state'])")
  [ "$S" = ready ] && break
  case "$S" in error|shutdown) echo FAIL; break;; esac
done

Проверка регистрации макроса после: curl .../printer/gcode/help | ... 'MY_MACRO' in help.

Что QIDI вырезала/сломала в Moonraker (и гоча 404 vs 405)

  • /machine/system_info → 500 (падает на отсутствующем /dev_info.txt);
  • /machine/update/status → 404 (нет [update_manager] — Klipper/Moonraker штатно не обновить, только перепрошивкой);
  • НЕ вырезано, хоть и кажется: /machine/services/restart, /machine/reboot, /machine/shutdown — POST-only. GET по ним даёт 405 (не 404!), что легко прочитать как «функции нет». Различать 404 (нет маршрута) и 405 (не тот метод), пробовать целевым методом. Рестарт Moonraker: POST /machine/services/restart с {"service":"moonraker"}.
  • Гоча загрузки: POST /server/files/upload трактует поле path как каталог, имя берётся из файла; path=foo.cfg создаст foo.cfg/foo.cfg.

Правка конфига и порядок include

Новые секции — отдельным файлом ([include extras.cfg]), строку include ставить до автосейв-блока #*# (SAVE_CONFIG в конце printer.cfg), иначе include попадёт внутрь автосейва и сломает его. Реальный пример вставки — [include klipperscreen-extras.cfg] после [include box.cfg] через sed -i '/^\[include box.cfg\]/a ...'. Владельца/права — под klipper-юзера (chown qidi:netdev, chmod 644). Применение — FIRMWARE_RESTART. Держать бэкап и авто-откат при error.

Бинарные (скомпилированные) модули

QIDI часть extras отгружает как .so (напр. multi_color_controller.so в klippy/extras/) — исходника нет. Что делать:

  • список команд — curl http://IP:7125/printer/gcode/help (в т.ч. вендорские MULTI_COLOR_*);
  • имена параметров — strings module.so | grep -iE 'TEMP|SLOT|time|state|cmd_': выдаёт токены gcode-параметров (SLOT, TEMP, minutes, state) и имена cmd-функций (cmd_multi_color_dry);
  • безопасный пробинг — вызвать команду без аргументов и прочитать ответ: если параметр обязателен, Klipper вернёт ошибку с его именем, НЕ выполнив действие. НО если параметр опционален, команда сработает с дефолтом → держать сеть безопасности (для нагрева — *_DISABLE_HEATER) и проверять эффект по объекту (printer/objects/query?<module>). Пример: SET_FILAMENT_DRY/MULTI_COLOR_DRY без аргументов → {"result":"ok"}, нагрев НЕ запущен (drying.box1.dry_state:0).

Запуск shell из макроса

gcode_shell_command (сторонний extra) есть не во всех форках — проверить ls klippy/extras/gcode_shell_command.py. Если есть — из макроса можно дёргать shell (напр. переключать сервисы, если у klipper-юзера беспарольный sudo):

[gcode_shell_command my_cmd]
command: bash -c "..."
timeout: 30.
verbose: True
[gcode_macro MY_MACRO]
gcode:
  RUN_SHELL_COMMAND CMD=my_cmd

gcode_shell_command исполняется от klipper-юзера — для sudo внутри нужен его NOPASSWD.

Чтение консоли (что видит экран)

Вывод команд (respond_info, //-строки) идёт в консоль, не попапом. Забрать: curl -s "http://IP:7125/server/gcode_store?count=30" (JSON, result.gcode_store[].message). ⚠️ парсить питоном через heredoc, а не python3 -c "...['result']..." внутри ssh '...' — вложенные одинарные кавычки ломают ключ, получаешь NameError: name 'result' is not defined (это про кавычки, не про данные). Родственное — KlipperScreen на слабом SBC.

Переопределение вендорских макросов (rename_existing и слияние секций)

Чтобы обернуть штатный макрос (например T0, добавив своё действие), в Klipper есть
rename_existing. С ним две ловушки:

  1. Тип имени. Цель переименования обязана быть того же класса, что исходное имя. T0 —
    «традиционный» g-code (буква+цифра), поэтому и цель — буква+цифра. rename_existing: BOX_T0
    падает: G-Code macro rename of different types ('T0' vs 'BOX_T0'). Валидно только вроде T90.
  2. Слияние секций. Klipper сливает одноимённые [gcode_macro T0] из разных include
    (опции более позднего файла побеждают). Если и вендорский box.cfg, и твой файл объявляют
    [gcode_macro T0], отдельного «оригинала» для переименования не остаётся → на connect падает
    Existing command 'T0' not found in gcode_macro rename.

Вывод: переопределять вендорский макрос из своего include надо полным переопределением, а
не rename_existing: в своём файле (подключённом ПОСЛЕ вендорского) объявить тот же
[gcode_macro T0], скопировав тело вендора и добавив своё — более поздняя опция gcode: побеждает
при слиянии. Чьё тело победило — видно по description в printer/gcode/help. Обе ошибки валят
Klipper в error, чинится следующим FIRMWARE_RESTART.

Три дополнения к этому паттерну из следующего захода (обёртка штатной чистки сопла):

Merge-smoke до боевого деплоя. Прежде чем вешать override на макрос, от которого зависит каждый старт печати, я проверил премис «поздняя секция побеждает» на безобидной паре: две одноимённые [gcode_macro _AQ_MERGE_TEST] в одном файле, первая делает M118 AQ_MERGE first, вторая — M118 AQ_MERGE second; FIRMWARE_RESTART; запуск макроса. Klippy поднялся молча (слияние без предупреждений), в консоли — AQ_MERGE second. Пара удаляется, рестарт. Стоит две минуты, доказывает механику именно НА ЭТОЙ прошивке.

Доказательство победы — смердженный configfile, не глазами. description в gcode/help — косвенный признак; прямой — объект configfile Moonraker, который отдаёт конфиг ПОСЛЕ всех слияний: objects/query?configfile → config["gcode_macro CLEAR_NOZZLE"].gcode содержит мою добавку → override победил. Это же — честная smoke-проверка установщика (в отличие от objects/query по имени, который эхоит и несуществующие объекты — Moonraker — проверка существования объекта).

Копию вендорского тела не перепечатывать, а извлекать установщиком. Тело стокового макроса (у меня 93 строки) вытаскивается регэкспом из вендорского файла в момент установки и кладётся в [gcode_macro _CLEAR_NOZZLE_STOCK] рядом с override; блок обрамляется маркерами с sha-суммой содержимого — установщик отличает свой блок (пересоздать можно) от правленного руками (остановиться и спросить). Обновление прошивки перезаписывает вендорский файл → переустановка сама переизвлекает свежее тело, копия не протухает. И критично проверять ПОРЯДОК include: override живёт только пока его файл подключён после вендорского — перестановка строк в printer.cfg после обновления прошивки молча возвращает сток, поэтому установщик сверяет индексы обеих include-строк и падает при неверном порядке.

Урезанный Moonraker: чего ещё может не быть

Кроме [update_manager], на старом форке нередко отсутствует компонент [analysis] (нет
analysis.py, среди компонентов не грузится) — тогда серверная точная оценка времени недоступна,
остаётся только слайсерный post-process (см.
klipper_estimator — точная оценка времени печати).
Веб-морда Fluidd при этом может быть стоковой, но с вендорскими правками настроек и скрытыми
карточками — см. Fluidd — настройки в БД Moonraker.

Цифра в имени G-code команды = команда молча теряется

Парсер G-code Klipper режет имя команды по первой цифре: моя CS1237_FIND_SENSOR регистрировалась (видна в printer/gcode/help с моим description), но диспатчилась как несуществующая CS1237 — без ошибки, просто ничего не происходило. Поэтому вендорские команды называются CS_WEIGHT_*, а не CS1237_* (и между версиями прошивки их переименовывали — декомпилированные имена сверять с живым gcode/help). Свои команды и макросы — только [A-Z_] в имени.

Команда без help-строки НЕ видна в gcode/help — отсутствие в списке ничего не доказывает

printer/gcode/help перечисляет только команды, зарегистрированные с описанием. Всё, что зарегистрировано без desc=, в ответе отсутствует, хотя прекрасно исполняется. У вендорского форка так живёт целый пласт команд: ENABLE_BOX_DRY, DISABLE_BOX_DRY и, что особенно неприятно, стандартный M118 из модуля [respond].

Меня это чуть не увело в сторону: макрос, который я писал, начинался с M118 <сообщение>, а M118 в выдаче gcode/help отсутствовал. Вывод «команды нет, макрос упадёт на первой же строке» напрашивался сам, но оказался неверным — секция [respond] в printer.cfg есть, RESPOND в списке присутствует, а штатный PRINT_START самой QIDI использует M118 в своём теле.

Проверять надо исполнением, а не наличием в списке:

curl -s -X POST "http://<ip>:7125/printer/gcode/script?script=M118%20test"
# {"result": "ok"}  -> команда есть

Это тот же класс ошибки, что 404 против 405 на маршрутах Moonraker выше: код ответа и содержимое справочника — свойства инструмента наблюдения, а не факта о системе. Прежде чем объявлять функцию вырезанной, надо ткнуть в неё тем способом, которым ей пользуются на самом деле.

Extras перечитываются ТОЛЬКО рестартом сервиса (и Moonraker после него глохнет)

RESTART/FIRMWARE_RESTART не перечитывают новые/изменённые файлы в klippy/extras/ — только systemctl restart klipper (удалённо: POST /machine/services/restart с {"service":"klipper"}). Две гочи следом:

  • Moonraker после рестарта сервиса 30–60 с молча теряет G-code: /printer/info уже ready, а POST gcode/script висит или возвращается пустым. Лечение — пинговать M115 в цикле до первого {"result": "ok"}, только потом слать команды.
  • Обходные каналы мимо Moonraker, когда он глохнет: pty ~/printer_data/comms/klippy.serial (printf "CMD\n" > <pty>, ответы cat оттуда же) и unix-сокет klippy.sock (JSON {"id":1,"method":"gcode/script","params":{"script":"CMD"}} + терминатор 0x03). Работают мгновенно и сразу после старта klippy.

Жёсткая остановка движения = THR MCU в shutdown

Рестарт сервиса klipper посреди активного движения роняет MCU тулхеда: klippy при переподключении падает в error с Can not update MCU 'THR' config as it is shutdown. Это штатно чинится FIRMWARE_RESTART (~15 с) — не паниковать и не искать поломку железа. Хоуминг после этого потерян — G28 перед следующим движением. Runtime-вскрытие закрытых модулей — Runtime-интроспекция закрытого Klipper-модуля.

Статус-автомат форка — не источник истины для safety-гвардов

У форка есть собственный статус-автомат печати (print_stats_manager с полем main_status) — соблазнительный источник для гвардов вида «не двигаться, если на столе деталь». Три причины ему не доверять, найденные ревью моего гварда, который выглядел правильным:

  1. В словаре может не быть нужного значения. Полный легальный словарь зашит в модуле (set_main_status кидает command_error на всё вне списка), и в нём НЕТ paused — только pausing, которое ни один стоковый макрос не ставит. Мой гвард if status in ('printing', 'paused') был наполовину мёртвым кодом: ветка paused недостижима по построению. Словарь надо ЧИТАТЬ из модуля форка, а не угадывать по аналогии с mainline.
  2. Ошибки сбрасывают статус посреди печати. Форк регистрирует обработчик gcode:command_error → сброс main_status в ready. Любая ошибка команды во время печати (а пауза с деталью на столе чаще всего и наступает после ошибки) делает статус-автомат врущим: печать идёт, статус говорит «свободен» — гвард пропускает движение в деталь.
  3. Fail-open при недоступности. Вендорский объект может быть переименован следующей прошивкой; except Exception: return 'ready' превращает ЛЮБУЮ поломку чтения в «принтер свободен» — тихий отказ ровно того гварда, который стоит между движением и деталью.

Правильная конструкция: аллоулист доверенных значений вендорского автомата для узких разрешений (у меня print_start и leveling_calibration — состояния, в которых стол гарантированно пуст) ПЛЮС авторитетный стандартный объект print_stats.state (есть в любом Klipper) как запрещающий: printing/paused → стоп независимо от вендорского статуса. Недоступность вендорского объекта — громкая деградация с сообщением в консоль и пропуском в печатных контекстах, никогда молчаливое «ready»: потерять один запуск функции дешевле, чем провести горячее сопло сквозь деталь.

M109 на форке не ждёт остывания

M109 S220 при сопле 233 °C вернулся за ~11 с — команда ждёт только НАГРЕВ. В любой процедуре со ступенями температуры вниз (температурный свип, остывание перед промером) следующий шаг молча выполняется на недоостывшем сопле. Я поймал это только потому, что контрольный шаг логировал фактическую температуру рядом с таргетом: замер «на 220» реально стартовал бы на 233. Лечение — после M109 явное ожидание с полосой в обе стороны:

TEMPERATURE_WAIT SENSOR=extruder MINIMUM=219 MAXIMUM=221

Полоса ±1 ждёт и нагрев, и остывание. Правило: в свипах по температуре фактическую температуру (extruder.temperature из objects/query) писать в данные каждой ступени рядом с таргетом — иначе ошибку не увидеть в принципе.

Метаданные слотов мультиматериального бокса — вендорский дефолт, а не факт

objects/query?multi_color_controller отдаёт slots.materials с типом филамента и min/max_temp на каждый слот — выглядит как источник истины о заряженной катушке. Это ловушка: пока владелец не назначил материал руками в вендорском UI, там лежит один и тот же фирменный филамент вендора на ВСЕХ слотах. У меня все четыре слота значились «QIDI PLA Rapido 190–240», хотя физически стояли катушки трёх разных производителей — из-за этого катушка в замерах даже успела пожить под чужим именем. API не отличает «назначено владельцем» от «дефолт», поэтому материал и температурные лимиты из бокса читать нельзя: единственный источник истины — этикетка катушки. В своих инструментах диапазон температур я передаю параметрами команды (TMIN=/TMAX=), а не читаю из бокса. Занятость слота при этом читать МОЖНО — она из механического концевика (runout_button), не из метаданных.

Источники

  • Moonraker API: printer/info, printer/gcode/help, printer/gcode/script, server/gcode_store, printer/objects/query, machine/services, server/files/upload
  • github.com/qidi-community/q2-wiki