Skip to content

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.

typescript
// 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:

typescript
// 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> and bambu-cli files upload both fail with 522 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-upload still opens an FTPS session (to stat the remote file) and hits the same 522.

What works — two-step upload + MQTT dispatch:

  1. Upload the .gcode.3mf via curl (curl's OpenSSL backend negotiates FTPS session reuse correctly):

    bash
    curl -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.

  2. Send the project_file command over MQTT to device/<SERIAL>/request:

    js
    import 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:

  • url must be ftp:///<filename> (three slashes) — the empty host component is required; the printer rejects ftp://<filename> as "unsupported print file path or name".
  • param uses the internal plate path inside the 3MF (Metadata/plate_1.gcode for plate 1), not a filesystem path.
  • md5: "" is accepted; populating it is optional.
  • On AMS-equipped H2 printers, use_ams: false does not suppress mapping lookup if the sliced file declares filaments. The working H2 path is to send use_ams: true plus a valid mapping. For H2, the mapping length must match the project-level filament declaration length, and the populated positions must match plate_<n>.json.filament_ids. Prefer ams_slots at 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 explicit ams_slots, raw ams_mapping, or auto_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 ftpUpload helper (basic-ftp with secure: "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.

Released under the GPL-2.0 license. An independent open-source project, not affiliated with Bambu Lab.