GUIなしでBambu A1にスライス済みデータを投げる ― OrcaSlicer CLIの「-17 互換地獄」を攻略した話

3Dプリントの「STLを置いたらスライスされて勝手に印刷が始まる」を、GUIを一切立ち上げずにLinuxサーバ上のスクリプトだけで完結させたかった。手元にあるのは画面のないサーバと、LANに繋がった Bambu Lab A1。やることは3つだけに見える。

  • OrcaSlicer を CLI で叩いて STL → gcode(3mf)にスライス
  • できた 3mf をプリンタに転送
  • 印刷開始コマンドを送る

ところが最初の「スライス」で、ほぼ documented されていない壁に正面衝突した。-17 : The selected printer is not compatible with the process preset。この記事は、その沼の底とそこからの抜け方、そして転送・印刷開始までの一気通貫を残しておく。

全体像

最終的に動いたパイプラインはこの形になった。

  • スライス: OrcaSlicer AppImage を xvfb-run でヘッドレス起動し、プリンタ/プロセス/フィラメントのプロファイルを --load-settings / --load-filaments で食わせる
  • 転送: implicit FTPS(ポート990)で 3mf をプリンタ内ストレージへ STOR
  • 開始: MQTT(8883/TLS)で project_file コマンドを publish、report トピックで進捗を監視

プリンタの認証はすべてアクセスコード(本体画面のLAN設定に出る8桁)で通る。以下、IP・シリアル・コードはプレースホルダにしてある。

1. OrcaSlicer を CLI で動かす

OrcaSlicer は AppImage を展開すれば中の AppRun が CLI として使える。GUIのGL初期化を避けるため仮想ディスプレイ越しに起動する。

xvfb-run -a squashfs-root/AppRun model.stl \
  --load-settings "machine.json;process.json" \
  --load-filaments "filament.json" \
  --slice 0 --outputdir out --export-3mf model.gcode.3mf

プロファイル(machine/process/filament)の JSON は、システムプリセットが "inherits" で多段に継承されている。CLI に渡すときは継承を再帰的に解決して1枚に平坦化しておくのが安全だ。継承を辿って親の値を下ろし、inherits / from / instantiation / setting_id といったメタキーを落とす、というスクリプトを噛ませた。

ここまでは素直。問題はこの後だった。

2. 沼 ― 「process not compatible with printer」

スライスを走らせると、ジオメトリも読めているのに最後でこう吐いて止まる。

run 2559: process not compatible with printer.
Error: -17

OrcaSlicer(というか元の PrusaSlicer 系)は、プロセスプリセットが持つ compatible_printers(互換プリンタ名のリスト)か compatible_printers_condition(条件式)で、選択中プリンタとの互換性を判定する。素直に考えれば「プロセスの compatible_printers に今のプリンタ名を入れればいい」。

入れた。通らない。

  • プリンタ名(Bambu Lab A1 0.4 nozzle)を正確に入れる → ダメ
  • リストを空 [] にする(=全互換のはず)→ ダメ
  • 条件式をトートロジー(常に真)にする → ダメ
  • 思いつく候補名を全部リストに突っ込む → ダメ

--debug 5 でトレースを出すと、さらに頭を抱える内容だった。プロセスの compatible_printers に入れた候補が1件ずつログに出て、プリンタ名も Bambu Lab A1 0.4 nozzle と出ている。リストにその名前が入っている。なのに compatible 0。リスト照合なら一致するはずなのに、しない。

new printer Bambu Lab A1 0.4 nozzle, inherited from ,
new process A1 0.20 sup, inherited from ,
compatible 0

3. 攻略 ― compatible_printers に「空文字」を入れる

矛盾の出どころは「照合に使われるプリンタ名は、ログに出ている表示名とは別物」という仮説しか残らなかった。互換判定は実際には active_printer.preset.name を見ている。そして --load-settings で外部 JSON から流し込んだプリンタプリセットは、内部的な name が割り当てられず空文字のままだったのだ。

つまり照合先は ""。だから候補リストに何を並べても一致しない。検証は単純で、compatible_printers の候補に空文字を1個足すだけ。

# プロセス/フィラメント両方の compatible_printers に "" を含める
candidates = ["", "Bambu Lab A1 0.4 nozzle", "Bambu Lab A1"]
proc["compatible_printers"] = candidates
fil["compatible_printers"]  = candidates

再スライスすると、ログが一発で変わった。

new printer Bambu Lab A1 0.4 nozzle, ... compatible 1

compatible 1。十数回の試行錯誤の出口がこれだった。CLI で外部プロファイルを --load-settings する場合、プリンタプリセットの内部名は空文字になり得る。互換リストには "" を含めておけ。 これがこの記事で一番言いたいことだ。

4. 仕上げ ― 自動姿勢と「プレート種別の焼き込み」

スライスは通ったが、出てきた gcode には実用上の地雷が2つ残っていた。

姿勢。元の STL は縦長で、そのままだと高さ170mmの細長い姿勢で立って印刷される。A1 はベッドが前後に動くタイプ(bedslinger)なので、細く高い造形は加速のたびに揺れて剥がれや転倒のリスクが高い。OrcaSlicer CLI には自動姿勢最適化があるので任せる。

... --orient 1 --slice 0 ...

これで最長軸が寝て、高さが170mm → 77mm に下がった。同じ造形でも安定度が段違いになる。

プレート種別。スライス結果の gcode を覗くと curr_bed_type = Cool Plate になっていた。実機に載っているのはテクスチャPEIプレートなのに、だ。これだと(1)ベッド温度が低め(35℃)で定着が弱い (2)テクスチャ面用のZオフセット補正が効かず1層目が浮く。curr_bed_typeプロジェクト設定で、フィラメント側の bed_type を書いただけでは gcode に反映されない。--load-settings で渡す設定そのものに明示する必要がある。

proc["curr_bed_type"] = "Textured PEI Plate"
mach["curr_bed_type"] = "Textured PEI Plate"

再スライス後の gcode で M140 S65 / M190 S65(ベッド65℃)と、テクスチャPEI用の G29.1 Z-0.02 補正が焼き込まれているのを確認。プリンタに送る前に、gcode の中身(温度・プレート・最大Z高さ)を1回 grep で検算しておくと安心だ。

5. プリンタへ送る ― implicit FTPS(990) のクセ

Bambu の LAN 転送は暗黙TLSのFTPS(ポート990)。ユーザ bblp、パスワードはアクセスコード。Python の ftplib は implicit TLS を直接サポートしないので、ソケットを最初から TLS でラップするサブクラスを使う。

import ftplib, ssl, socket
class ImplicitFTP_TLS(ftplib.FTP_TLS):
    @property
    def sock(self): return self._sock
    @sock.setter
    def sock(self, v):
        if v is not None and not isinstance(v, ssl.SSLSocket):
            v = self.context.wrap_socket(v)
        self._sock = v

ここでもう一段ハマる。普通に storbinary() でアップロードすると、データ自体は送り終わっているのに最後の TLS クローズ(unwrap())でタイムアウトして例外になる。Bambu 側が TLS の close_notify を返さないためだ。storbinary 任せだと STOR が確定したのか分からない。

そこで転送を手動で組む。データを送り切ったら unwrap() を呼ばず、TCPレベルでソケットを閉じてから、コマンドチャネルで完了応答(226)を受け取る。

ftp.voidcmd("TYPE I")
conn = ftp.transfercmd("STOR model.gcode.3mf")
with open(local, "rb") as f:
    while (buf := f.read(262144)):
        conn.sendall(buf)
conn.shutdown(socket.SHUT_RDWR)   # unwrap()は呼ばない
conn.close()
print(ftp.voidresp())             # -> '226'

別接続で SIZE を引いてローカルとバイト一致を確認すれば、転送の成否がはっきりする。

6. 印刷開始 ― MQTT の project_file

最後は MQTT。ポート8883のTLS(証明書は検証しない)、ユーザ bblp・パスワードはアクセスコード。まず pushall で現在状態(gcode_state、ノズル/ベッド温度、載っているフィラメント)を取り、待機中なら project_file を投げる。

cmd = {"print": {
    "command": "project_file",
    "param": "Metadata/plate_1.gcode",       # 3mf内のgcodeパス
    "url": "ftp:///model.gcode.3mf",          # 本体ストレージ上のファイル
    "bed_type": "textured_plate",
    "use_ams": False,
    "timelapse": False, "bed_leveling": True,
    "flow_cali": False, "vibration_cali": True,
    "sequence_id": "1"
}}
client.publish(f"device/{SERIAL}/request", json.dumps(cmd))

device/<シリアル>/report を購読しておくと、gcode_stateFINISH → PREPARE → RUNNING と遷移し、ノズルがPLA温度へ、ベッドが狙った65℃へ上がっていくのがそのまま流れてくる。ここまで来れば、サーバから一切GUIを触らずにスライス→転送→印刷開始が回った。

gcode_state: FINISH -> PREPARE -> RUNNING
nozzle 23 -> 220 / bed 23 -> 65 / print_error 0

まとめ

ヘッドレスで Bambu A1 にデータを投げるうえでの要点。

  • OrcaSlicer CLI の -17 互換エラーは、compatible_printers に空文字 "" を入れると抜けられる--load-settings 由来のプリンタは内部名が空のため)
  • 細長い造形は --orient 1 で寝かせる。bedslinger では安定度が効く
  • curr_bed_type は渡す設定に明示しないと gcode に焼き込まれない。温度とZ補正に直結する
  • Bambu の implicit FTPS は unwrap() で固まる。手動STOR+TCPクローズ+226確認が確実
  • 印刷開始は MQTT の project_file、状態は pushallreport で取る

「沼も頑張れば抜け出せる」を地で行った。同じ -17 で消耗している人の検索に、この空文字が引っかかれば本望だ。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

IP: 取得中...
216.73.216.177216.73.216.177