#!/usr/bin/env bash # Сборка и прошивка Klipper на всех MCU принтера leo-voron: # host — linux-процесс klipper_mcu на CB2 (/usr/local/bin/klipper_mcu, сервис klipper-mcu) # mcu — Octopus Pro 1.1 (stm32h723, USB-CAN bridge; прошивается по USB через katapult) # toolhead — EBB SB2209 (rp2040; прошивается по CAN через katapult) # # Использование: # update_fw.sh [-h] [--build-only] [--force] [host|mcu|toolhead ...] # без целей — все три # --build-only — только собрать прошивки в ~/firmware, ничего не прошивать # --force — не останавливаться, если Moonraker недоступен для проверки печати # # Исходники Klipper НЕ обновляются (это делает Moonraker Update Manager). # Скрипт интерактивный: пароль sudo спрашивается один раз в начале. # # Полезные команды: # make menuconfig KCONFIG_CONFIG=/home/biqu/printer_data/config/firmware/octopus-pro.config # make flash FLASH_DEVICE=0483:df11 # stm32 в DFU # python3 ~/katapult/scripts/flashtool.py -i can0 -q # список узлов на CAN # python3 ~/katapult/scripts/flashtool.py -i can0 -u -r # запрос бутлоадера # make flash FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_rp2040_45474E621A87A43A-if00 KCONFIG_CONFIG=.../hbb.cfg # # Старые тулхеды (UUID): # rp_sb2209 = 350f10596216 # sht36_v2 = 8ce295109ef0 set -euo pipefail # ---------- Константы ---------- MOONRAKER="http://127.0.0.1:7125" KLIPPER="$HOME/klipper" KATAPULT="$HOME/katapult/scripts/flashtool.py" CONFIG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUT_DIR="$HOME/firmware" KLIPPY_LOG="$HOME/printer_data/logs/klippy.log" CAN_IF="can0" JOBS=4 MCU_UUID="afcf80281237" MCU_CFG="octopus-pro.config" KATAPULT_SERIAL_GLOB='/dev/serial/by-id/usb-katapult_stm32h723xx_*' TOOLHEAD_UUID="d7e70bd38059" TOOLHEAD_CFG="ebb_rp2240.CAN" HOST_CFG="host_linux.config" HOST_BIN="/usr/local/bin/klipper_mcu" ALL_TARGETS=(host mcu toolhead) # ---------- Разбор аргументов ---------- BUILD_ONLY=0 FORCE=0 TARGETS=() for arg in "$@"; do case "$arg" in -h|--help) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; --build-only) BUILD_ONLY=1 ;; --force) FORCE=1 ;; host|mcu|toolhead) TARGETS+=("$arg") ;; *) echo "Неизвестный аргумент: $arg (см. -h)" >&2; exit 2 ;; esac done [ ${#TARGETS[@]} -eq 0 ] && TARGETS=("${ALL_TARGETS[@]}") want() { local t; for t in "${TARGETS[@]}"; do [ "$t" = "$1" ] && return 0; done; return 1; } # ---------- Лог и трапы ---------- mkdir -p "$OUT_DIR" LOG="$OUT_DIR/update_fw-$(date +%Y%m%d-%H%M).log" exec > >(tee -a "$LOG") 2>&1 START_TS=$(date +%s) STAGE="инициализация" KLIPPER_STOPPED=0 on_err() { echo echo "!!! ОШИБКА на этапе: $STAGE (лог: $LOG)" >&2 } on_exit() { local rc=$? rm -rf "${BUILD_TMP:-}" if [ "$KLIPPER_STOPPED" = 1 ]; then echo; echo "=== Запуск klipper ===" sudo systemctl start klipper || echo "!!! Не удалось запустить klipper — запустите вручную: sudo systemctl start klipper" >&2 fi [ $rc -ne 0 ] && echo "Завершено с ошибкой (код $rc)." exit $rc } trap on_err ERR trap on_exit EXIT section() { echo; echo "=== $* ==="; } die() { echo "!!! $*" >&2; exit 1; } # Ждать, пока команда-условие не вернёт 0 (таймаут в секундах) wait_for() { local timeout=$1 msg=$2; shift 2 local i for ((i = 0; i < timeout; i++)); do if "$@" >/dev/null 2>&1; then return 0; fi sleep 1 done echo "!!! Таймаут ($timeout с): $msg" >&2 return 1 } can_has_uuid() { python3 "$KATAPULT" -i "$CAN_IF" -q 2>/dev/null | grep -q "$1"; } katapult_serial() { local d; for d in $KATAPULT_SERIAL_GLOB; do [ -e "$d" ] && { echo "$d"; return 0; }; done; return 1; } klippy_state() { curl -s --max-time 5 "$MOONRAKER/printer/info" | jq -r '.result.state // empty' 2>/dev/null; } klippy_ready() { [ "$(klippy_state)" = "ready" ]; } mcu_versions() { curl -s --max-time 5 "$MOONRAKER/printer/objects/query?mcu&mcu%20EBBCan&mcu%20host" \ | jq -r '.result.status | to_entries[] | "\(.key)\t\(.value.mcu_version // "?")\t\(.value.mcu_constants.MCU // "?")"' 2>/dev/null \ || echo "недоступно" } # ---------- Preflight ---------- STAGE="предварительные проверки" section "Preflight" echo "Цели: ${TARGETS[*]} build-only=$BUILD_ONLY лог: $LOG" [ -d "$KLIPPER" ] || die "Нет исходников Klipper: $KLIPPER" [ -f "$KATAPULT" ] || die "Нет katapult flashtool: $KATAPULT" want host && { [ -f "$CONFIG_DIR/$HOST_CFG" ] || die "Нет конфига $CONFIG_DIR/$HOST_CFG"; } want mcu && { [ -f "$CONFIG_DIR/$MCU_CFG" ] || die "Нет конфига $CONFIG_DIR/$MCU_CFG"; } want toolhead && { [ -f "$CONFIG_DIR/$TOOLHEAD_CFG" ] || die "Нет конфига $CONFIG_DIR/$TOOLHEAD_CFG"; } KLIPPER_VER=$(git -C "$KLIPPER" describe --always --tags --long --dirty 2>/dev/null || echo unknown) echo "Klipper в $KLIPPER: $KLIPPER_VER" BEFORE="" if [ "$BUILD_ONLY" = 0 ]; then if want mcu || want toolhead; then ip -br link show "$CAN_IF" 2>/dev/null | grep -q " UP " || die "$CAN_IF не поднят" fi # Печать не должна идти PRINT_STATE=$(curl -s --max-time 5 "$MOONRAKER/printer/objects/query?print_stats" \ | jq -r '.result.status.print_stats.state // empty' 2>/dev/null || true) if [ -z "$PRINT_STATE" ]; then if [ "$FORCE" = 1 ]; then echo "Moonraker недоступен, проверка печати пропущена (--force)" else die "Moonraker недоступен — не могу проверить, идёт ли печать (--force чтобы пропустить)" fi else echo "print_stats.state = $PRINT_STATE" case "$PRINT_STATE" in printing|paused) die "Идёт печать ($PRINT_STATE) — прошивка отменена" ;; esac fi echo "Версии MCU до обновления:" BEFORE=$(mcu_versions); echo "$BEFORE" | sed 's/^/ /' # Пароль sudo — один раз, до остановки сервисов sudo -v || die "Нужны права sudo" fi # ---------- Сборка ---------- BUILD_TMP=$(mktemp -d) build() { # build <цель> <конфиг> <артефакт: bin|elf> local name=$1 ext=$3 STAGE="сборка $name" section "Сборка $name ($2)" # Собираем из копии: olddefconfig переписывает конфиг и создаёт <конфиг>.old, # а файлы в репозитории должны оставаться нетронутыми local cfg="$BUILD_TMP/$2" cp "$CONFIG_DIR/$2" "$cfg" make -C "$KLIPPER" clean KCONFIG_CONFIG="$cfg" >/dev/null make -C "$KLIPPER" olddefconfig KCONFIG_CONFIG="$cfg" make -C "$KLIPPER" -j"$JOBS" KCONFIG_CONFIG="$cfg" local src="$KLIPPER/out/klipper.$ext" [ -s "$src" ] || die "Сборка $name не дала $src" local dst="$OUT_DIR/${name}-${KLIPPER_VER}.$ext" cp "$src" "$dst" ln -sfn "$(basename "$dst")" "$OUT_DIR/${name}_klipper.$ext" echo "-> $dst ($(stat -c %s "$dst") байт)" } # host собирается последним: его out/ нужен для make flash want mcu && build mcu "$MCU_CFG" bin want toolhead && build toolhead "$TOOLHEAD_CFG" bin want host && build host "$HOST_CFG" elf if [ "$BUILD_ONLY" = 1 ]; then section "Готово (только сборка)" ls -l "$OUT_DIR"/*_klipper.* 2>/dev/null exit 0 fi # ---------- Прошивка ---------- STAGE="остановка klipper" section "Остановка klipper" sudo systemctl stop klipper KLIPPER_STOPPED=1 if want host; then STAGE="прошивка host MCU" section "Прошивка host (klipper-mcu)" # flash-linux.sh копирует out/klipper.elf в /usr/local/bin/klipper_mcu и перезапускает klipper-mcu make -C "$KLIPPER" flash KCONFIG_CONFIG="$BUILD_TMP/$HOST_CFG" [ "$(stat -c %Y "$HOST_BIN")" -ge "$START_TS" ] || die "$HOST_BIN не обновился" wait_for 10 "klipper-mcu не запустился" systemctl is-active --quiet klipper-mcu echo "klipper-mcu: $(systemctl is-active klipper-mcu), $HOST_BIN обновлён" fi if want mcu; then STAGE="прошивка Octopus Pro" section "Прошивка mcu (Octopus Pro, uuid $MCU_UUID)" can_has_uuid "$MCU_UUID" || die "Octopus ($MCU_UUID) не виден на $CAN_IF" python3 "$KATAPULT" -i "$CAN_IF" -u "$MCU_UUID" -r wait_for 20 "не появился katapult serial ($KATAPULT_SERIAL_GLOB). Проверьте USB или перезагрузите плату" katapult_serial DEV=$(katapult_serial) echo "katapult serial: $DEV" python3 "$KATAPULT" -d "$DEV" -f "$OUT_DIR/mcu_klipper.bin" # После прошивки плата перезагружается в Klipper и снова поднимает USB-CAN мост wait_for 30 "Octopus не вернулся на $CAN_IF после прошивки" can_has_uuid "$MCU_UUID" echo "Octopus снова на $CAN_IF" fi if want toolhead; then STAGE="прошивка тулхеда" section "Прошивка toolhead (EBB SB2209, uuid $TOOLHEAD_UUID)" can_has_uuid "$TOOLHEAD_UUID" || die "Тулхед ($TOOLHEAD_UUID) не виден на $CAN_IF" python3 "$KATAPULT" -i "$CAN_IF" -u "$TOOLHEAD_UUID" -f "$OUT_DIR/toolhead_klipper.bin" wait_for 30 "тулхед не вернулся на $CAN_IF после прошивки" can_has_uuid "$TOOLHEAD_UUID" echo "Тулхед снова на $CAN_IF" fi section "Узлы на $CAN_IF" python3 "$KATAPULT" -i "$CAN_IF" -q || true # ---------- Запуск и проверка ---------- STAGE="запуск klipper" section "Запуск klipper" sudo systemctl start klipper KLIPPER_STOPPED=0 STAGE="проверка после прошивки" if wait_for 90 "klippy не перешёл в ready" klippy_ready; then echo "klippy: ready" else echo "klippy state: $(klippy_state)" curl -s --max-time 5 "$MOONRAKER/printer/info" | jq -r '.result.state_message // empty' 2>/dev/null || true echo "--- хвост $KLIPPY_LOG ---" tail -n 40 "$KLIPPY_LOG" 2>/dev/null || true fi section "Версии" KLIPPY_VER=$(curl -s --max-time 5 "$MOONRAKER/printer/info" | jq -r '.result.software_version // "?"' 2>/dev/null || echo "?") echo "klippy (хост): $KLIPPY_VER" AFTER=$(mcu_versions) printf '%-12s %-32s %s\n' "MCU" "было" "стало" MISMATCH=0 while IFS=$'\t' read -r name ver _; do old=$(printf '%s\n' "$BEFORE" | awk -F'\t' -v n="$name" '$1==n{print $2}') printf '%-12s %-32s %s\n' "$name" "${old:-?}" "$ver" [ "$ver" = "$KLIPPY_VER" ] || { echo "!!! $name: версия $ver != klippy $KLIPPY_VER"; MISMATCH=1; } done <<< "$AFTER" if tail -n 200 "$KLIPPY_LOG" 2>/dev/null | grep -E "Command format mismatch|Protocol error|Lost communication" | tail -5 | grep .; then echo "!!! В klippy.log есть ошибки протокола (см. выше)"; MISMATCH=1 fi if [ "$MISMATCH" = 0 ]; then echo; echo "Готово: все MCU на $KLIPPY_VER. Дальше: QUAD_GANTRY_LEVEL и проверить TAP/z_offset." else echo; echo "Есть расхождения — проверьте вывод выше; при необходимости FIRMWARE_RESTART или повторить: $0 <цель>" exit 1 fi