Skip to content

J040M/marlin-binary-transfer

Repository files navigation

marlin-binary-transfer

crates.io docs.rs CI License: MIT OR Apache-2.0

Host-side Rust implementation of Marlin's Binary File Transfer Mark II protocol. Uploads G-code files to a 3D printer's SD card over serial — with framing checksums, sync acknowledgement, retransmit on timeout, and optional heatshrink payload compression.

Status: pre-1.0. The Marlin protocol itself is documented as experimental upstream and may evolve. This crate tracks Marlin bugfix-2.x. Public API follows semver within 0.x; pin a minor version.

Why this exists

The text-mode M28/M29 SD upload path is unreliable in practice — multiple open Marlin issues, slow over UART, no integrity checks. The binary protocol is the correct path: roughly 10 KiB/s on 115200-baud UART and 180 KiB/s on native USB CDC, with per-packet checksums and explicit retransmit.

Before this crate, the only host implementation was the Python marlin-binary-protocol used by OctoPrint's MarlinBft plugin. This is an independent Rust port — same wire format, sans-I/O core, three optional adapters for the common transports.

Design

┌────────────────────────────────────────────────────────────┐
│  adapters::{blocking, tokio, serialport}   ← optional I/O  │
├────────────────────────────────────────────────────────────┤
│  file_transfer  ← protocol-1 state machine (QUERY/OPEN/…)  │
├────────────────────────────────────────────────────────────┤
│  session        ← sync counter, retransmit, ack matching   │
├────────────────────────────────────────────────────────────┤
│  codec          ← packet framing + Fletcher-16 checksum    │
└────────────────────────────────────────────────────────────┘

The bottom three layers are sans-I/O: you feed in bytes and pull events out, plumbed through any transport you want. The adapters modules wrap the core into one-call upload helpers for the common cases.

Feature flags

Feature Default What you get
std yes Currently unconditional; reserved for the future no_std + alloc split.
blocking no Synchronous upload(transport, src, opts) over any Read + Write transport.
tokio no Async upload(transport, src, opts).await over AsyncRead + AsyncWrite.
serial no serialport::open helper preconfigured with sensible defaults. Implies blocking.
heatshrink no Negotiate and apply heatshrink payload compression with the device.

Quickstart (blocking + serial)

use marlin_binary_transfer::adapters::blocking::{upload, UploadOptions};
use marlin_binary_transfer::adapters::serialport;
use marlin_binary_transfer::file_transfer::Compression;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut port = serialport::open("/dev/ttyUSB0", 250_000)?;
    let file = std::fs::File::open("model.gco")?;

    let stats = upload(&mut *port, file, UploadOptions {
        dest_filename: "model.gco".into(),
        compression: Compression::Auto,
        dummy: false,
        chunk_size: 0, // 0 = use the device-advertised maximum
        progress: Some(Box::new(|p| {
            eprintln!("  {} chunks, {} bytes", p.chunks_sent, p.bytes_sent);
        })),
    })?;

    println!(
        "uploaded {} bytes in {} chunks ({:?})",
        stats.bytes_sent, stats.chunks_sent, stats.compression,
    );
    Ok(())
}

Cargo:

[dependencies]
marlin-binary-transfer = { version = "0.1", features = ["blocking", "serial", "heatshrink"] }

Run the bundled CLI example:

cargo run --example upload --features blocking,serial,heatshrink -- \
    --port /dev/ttyUSB0 --baud 250000 \
    --src ./model.gco --dest model.gco --compression auto

Quickstart (tokio)

use marlin_binary_transfer::adapters::tokio::{upload, UploadOptions};
use marlin_binary_transfer::file_transfer::Compression;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // `transport` is any AsyncRead + AsyncWrite + Unpin — e.g. a tokio-serial
    // SerialStream, a TCP-to-serial bridge, etc.
    # let mut transport: tokio::io::DuplexStream = unreachable!();
    let mut src = tokio::fs::File::open("model.gco").await?;

    let stats = upload(&mut transport, &mut src, UploadOptions {
        dest_filename: "model.gco".into(),
        compression: Compression::Auto,
        ..UploadOptions::default()
    }).await?;
    println!("uploaded {} bytes", stats.bytes_sent);
    Ok(())
}

What the upload helper does for you

The adapters::{blocking,tokio}::upload helpers handle the full lifecycle so you don't have to drive the state machines manually:

  1. Send the ASCII trigger M28 B1 to put the firmware in binary mode.
  2. Drive the SYNC handshake; capture the device-advertised block size and protocol version.
  3. Issue QUERY to negotiate compression (none or heatshrink per the device's advertised capabilities).
  4. OPEN the destination file with the chosen compression mode.
  5. Stream the source through WRITE packets, retransmitting any packet the device requests via rs<n> or that times out.
  6. CLOSE the file (commits to SD).
  7. Send the control-plane CLOSE (proto=0, type=2) so the device returns to ASCII g-code mode. Without this, subsequent ASCII commands on the same serial session are ignored — this was added in 0.1.1.

On any unrecoverable failure (PFT:busy, PFT:fail, PFT:ioerror, PFT:invalid, fatal session error, sync drift) you get a specific UploadError::Transfer(FileError::…) rather than a generic timeout.

Sans-I/O usage

If the helpers don't fit (custom transport, fan-out to many printers, your own event loop), build directly on Session and FileTransfer:

use std::time::Instant;
use marlin_binary_transfer::session::{Session, Event};
use marlin_binary_transfer::file_transfer::{FileTransfer, FileEvent, Compression};

let mut session = Session::new();
session.connect(Instant::now());

// Drain `session.poll_outbound()` and write bytes to your transport.
// Push received bytes via `session.feed(bytes, Instant::now())`.
// Call `session.tick(Instant::now())` at least as often as
// `session.response_timeout()` so retransmits fire.
// Drain `session.poll_event()` until `Event::Synced` is observed.

# loop {
#     break;
# }
let mut ft = FileTransfer::new(&mut session);
ft.query(Compression::Auto, Instant::now());
// Same pump pattern: poll_outbound → write, read → feed, then poll() for FileEvents.

See the rustdoc on session and file_transfer for the full event vocabulary.

Timeouts & reliability

  • Session::with_response_timeout(d) — how long to wait before retransmitting an in-flight packet. Default: 1 second.
  • Session::with_total_timeout(d) — total budget for a single packet across all retransmits. Default: 20 seconds.

The tokio adapter wraps every inbound read in tokio::time::timeout(...) keyed to response_timeout, so a quiet transport never deadlocks the loop. The blocking adapter relies on the transport returning ErrorKind::TimedOut on idle — adapters::serialport::open configures a 100 ms read timeout for this.

Protocol prerequisites on the printer

The Marlin firmware must have:

  • BINARY_FILE_TRANSFER enabled at compile time
  • MEATPACK disabled (mutually exclusive with BFT)

You can detect both via the M115 capability output before attempting an upload.

MSRV

Rust 1.75 for the library itself. Development tooling (criterion, recent test deps) may require a more recent toolchain.

License

Dual-licensed under either of:

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.

Credits

Protocol design and the original Python reference implementation by Chris Pepper (@p3p). This crate is an independent Rust port of the wire format described in MarlinFirmware/Marlin#14817, cross-checked against the trippwill/marlin-binary-protocol reference.

About

Rust crate implementing host-side Marlin Binary File Transfer Mark II protocol for uploading G-code to 3D printer SD cards.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Contributors

Languages