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_state が FINISH → 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、状態はpushallとreportで取る
「沼も頑張れば抜け出せる」を地で行った。同じ -17 で消耗している人の検索に、この空文字が引っかかれば本望だ。

コメントを残す