Bambu Communication Notes (MQTT and FTP)
Bambu Lab printers do not use a conventional REST API. Instead, they expose two local protocols that this server uses directly:
MQTT (port 8883, TLS): All printer commands and state reports flow over an MQTT broker running on the printer itself. Authentication uses username bblp and your LAN access code; the serial number identifies the device topics. Commands like starting a print, cancelling a job, and dispatching G-code lines are all MQTT publishes to the device topic. Status data is received by subscribing to the printer's report topic and requesting a push_all refresh. This implementation is based on community reverse engineering documented in the OpenBambuAPI project.
FTPS (port 990, implicit TLS): File operations (upload and directory listing) use FTPS. The printer's SD card is accessible as a filesystem with directories including cache/ (for 3MF and G-code print files), timelapse/, and logs/. Authentication uses the username bblp and your access token as the password.
What this fork fixes
This package works around two protocol-level issues in the underlying bambu-js library.
Bug 1: FTP double-path error in bambu-js.
The bambu-js library's sendFile method has a path construction bug. It calls ensureDir to change the working directory into the target directory (e.g., /cache), and then calls uploadFrom with the full relative path including the directory prefix (e.g., cache/file.3mf). The result is that the file lands at the wrong path on the printer (e.g., /cache/cache/file.3mf instead of /cache/file.3mf), and the subsequent print command fails because it references a file that does not exist at the expected path.
This fork bypasses bambu-js for all uploads and uses basic-ftp directly. The upload function (ftpUpload) connects to the printer, resolves the absolute remote path, changes to the correct directory with ensureDir, and then uploads using only the basename -- avoiding the double-path construction entirely.
// From src/printers/bambu.ts
private async ftpUpload(host, token, localPath, remotePath): Promise<void> {
const client = new FTPClient(15_000);
await client.access({ host, port: 990, user: "bblp", password: token,
secure: "implicit", secureOptions: { rejectUnauthorized: false } });
const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
const remoteDir = path.posix.dirname(absoluteRemote);
await client.ensureDir(remoteDir);
// basename only -- no double-path
await client.uploadFrom(localPath, path.posix.basename(absoluteRemote));
client.close();
}Bug 2: AMS mapping format in the project_file MQTT command.
The bambu-js library's project file command hardcodes use_ams: true and does not support the ams_mapping field at all. Without the fix, the mapping is a simple array of slot indices (e.g., [0, 2]), which does not match the OpenBambuAPI specification.
For the non-H2/P2S project-file route, this implementation retains at least five positions in the ams_mapping array where position i is the project filament index and the value is the AMS slot feeding that filament. For example, a single-filament print from AMS slot 0 sends [0, -1, -1, -1, -1].
This fork sends the project_file command directly via bambu-node (bypassing bambu-js entirely for print initiation) and constructs the mapping in the format the target firmware expects:
// Non-H2/P2S project_file: at least five entries; preserve longer projects
ams_mapping = [0, -1, -1, -1, -1];
// H2S/H2D/H2C/P2S: project-length lookup table + parallel ams_mapping2
ams_mapping = [-1, 1, -1, -1];
ams_mapping2 = [
{ ams_id: 255, slot_id: 255 },
{ ams_id: 0, slot_id: 1 },
{ ams_id: 255, slot_id: 255 },
{ ams_id: 255, slot_id: 255 }
];The command payload also includes all required fields per the OpenBambuAPI spec: param (the internal gcode path within the 3MF), url (the sdcard path), md5 (computed from the plate's embedded gcode), and all calibration flags.
Verified print procedure (H2S, LAN-only, no client cert)
This is the sequence that successfully started a print on an H2S in the original LAN-only test. It's documented here because several common approaches fail on this firmware, and this fork's transport is what makes it reliable.
Result: print started in RUNNING state, printer accepted the MQTT project_file command, no client certificate was required. Authentication was plain bblp + LAN access code over TLS with rejectUnauthorized: false.
What doesn't work on stock bambu-cli:
bambu-cli print start <file>andbambu-cli files uploadboth fail with522 SSL connection failed: session reuse required. Bambu's FTPS server requires TLS session reuse between the control and data channels, which the Go FTPS client in bambu-cli does not negotiate correctly.bambu-cli print start --no-uploadstill opens an FTPS session (to stat the remote file) and hits the same 522.
What works — two-step upload + MQTT dispatch:
Upload the
.gcode.3mfvia curl (curl's OpenSSL backend negotiates FTPS session reuse correctly):bashcurl -k --ftp-pasv --ssl-reqd \ -u "bblp:<ACCESS_CODE>" \ -T /path/to/file.gcode.3mf \ "ftps://<PRINTER_IP>:990/<remote-name>.gcode.3mf"Keep
<remote-name>simple ASCII, ending in.gcode.3mf. The file lands at the FTP root, which corresponds to/data/on the printer's SD card.Send the
project_filecommand over MQTT todevice/<SERIAL>/request:jsimport mqtt from "mqtt"; const payload = { print: { sequence_id: "0", command: "project_file", param: "Metadata/plate_1.gcode", // path inside the 3MF subtask_name: "<remote-name>.gcode.3mf", file: "<remote-name>.gcode.3mf", url: "ftp:///<remote-name>.gcode.3mf", // three slashes, FTP root md5: "", project_id: "0", profile_id: "0", task_id: "0", subtask_id: "0", timelapse: false, bed_type: "auto", bed_leveling: true, bed_levelling: true, flow_cali: true, vibration_cali: true, layer_inspect: true, use_ams: true, ams_mapping: [0, -1, -1, -1, -1] } }; const client = mqtt.connect(`mqtts://<PRINTER_IP>:8883`, { username: "bblp", password: "<ACCESS_CODE>", rejectUnauthorized: false, }); client.on("connect", () => { client.publish(`device/<SERIAL>/request`, JSON.stringify(payload)); });
Notes:
urlmust beftp:///<filename>(three slashes) — the empty host component is required; the printer rejectsftp://<filename>as "unsupported print file path or name".paramuses the internal plate path inside the 3MF (Metadata/plate_1.gcodefor plate 1), not a filesystem path.md5: ""is accepted; populating it is optional.- On AMS-equipped H2 printers,
use_ams: falsedoes not suppress mapping lookup if the sliced file declares filaments. The working H2 path is to senduse_ams: trueplus a valid mapping. For H2, the mapping length must match the project-level filament declaration length, and the populated positions must matchplate_<n>.json.filament_ids. Preferams_slotsat the tool layer and let the server expand it. If no mapping is provided for an H2 pre-sliced job with declared filaments, the server fails before sending; pass explicitams_slots, rawams_mapping, orauto_match_ams: true. - No client X.509 certificate was needed. The earlier assumption that post-Jan 2025 firmware mandates mTLS on all models does not hold for the H2S in LAN mode — user/password over TLS is sufficient.
- The MCP server's
ftpUploadhelper (basic-ftp withsecure: "implicit"and a short idle timeout) performs the equivalent upload natively and is the preferred path when using the server itself; the curl form is the manual-debug equivalent.