Skip to main content

Crate letratag_protocol

Crate letratag_protocol 

Source
Expand description

Offline LetraTag LT-200B wire encoding. Not verified against real hardware.

§letratag-protocol

Bounded, offline wire-protocol encoding for the DYMO LetraTag 200B thermal label printer. No Bluetooth, CUPS, or hardware dependency: this crate turns a 32-row raster into the framed characteristic writes a BLE transport sends, and parses the printer’s status notifications.

crates.io repository

Part of letratag-linux.

§Install

[dependencies]
letratag-protocol = "0.1.1"

Rust 1.98 or newer. No default features; there are no feature flags.

§Examples

§Raster model and column packing

A Raster is a full 32-row frame (HEAD_ROWS), row-major, containing only 0 (white) or 1 (black). x is the tape travel (feed) direction; y = 0..31 runs across the tape.

use letratag_protocol::{HEAD_ROWS, Raster};

let mut pixels = vec![0; 2 * HEAD_ROWS];
pixels[0] = 1; // column 0, row 0
pixels[HEAD_ROWS + 31] = 1; // column 1, row 31
let raster = Raster::new(2, pixels).unwrap();
assert!(raster.get(0, 0));
assert!(raster.get(1, 31));
assert!(!raster.get(1, 0));

// Wire packing: column x occupies bytes 4x..4x+4; row 0 is bit 7 of the
// last byte, row 31 is bit 0 of the first byte.
assert_eq!(raster.pack_columns(), vec![0, 0, 0, 0x80, 0x01, 0, 0, 0]);
assert_eq!(
    Raster::from_packed_columns(2, &raster.pack_columns()).unwrap(),
    raster
);

§Encoding a print job into bounded writes

Job::writes(max_write) returns the ordered characteristic values: a 9-byte header first, then indexed body chunks. max_write is the usable characteristic value length, NOT the raw ATT MTU. Every chunk reserves one index byte and the final chunk two magic bytes, so the body ceiling is min(500, max_write - 3). At chunk position 27 the index 27 is skipped (i >= 27 sends i + 1), and at most 255 body chunks are produced.

use letratag_protocol::{Job, Profile, Raster};

let raster = Raster::new(4, vec![0; 4 * 32]).unwrap();
let job = Job::new(&raster, Profile::Modern).unwrap();
// Modern body: 28 bytes of framing plus 4 bytes per column.
assert_eq!(job.body().len(), 28 + 4 * 4);

let writes = job.writes(20).unwrap();
assert_eq!(writes[0].len(), 9); // header
assert!(writes.iter().all(|write| write.len() <= 20));
let last = writes.last().unwrap();
assert_eq!(&last[last.len() - 2..], &[0x12, 0x34]); // final magic

§Header and status parsing

use letratag_protocol::{parse_header, Job, Profile, Raster, Status};

let raster = Raster::new(1, vec![0; 32]).unwrap();
let job = Job::new(&raster, Profile::Legacy).unwrap();
let writes = job.writes(512).unwrap();
assert_eq!(parse_header(&writes[0]).unwrap(), job.body().len());

// Only code 0 is recognized as success; every other code is preserved
// unchanged as Unconfirmed.
assert_eq!(Status::parse(&[0x1b, 0x52, 0]).unwrap(), Status::Success);
assert_eq!(Status::parse(&[0x1b, 0x52, 2]).unwrap(), Status::Unconfirmed(2));
assert!(Status::parse(&[0x1b, 0x52]).is_err());

§Set-cassette-type short command

set_cassette_type_write builds one 23-byte non-print write for the auxiliary characteristic (HEADER + ESC s + ESC M <id> 00 00 00 + ESC Q). The cassette id enum is 1=6mm, 2=9mm, 3=12mm, 4=19mm, 5=24mm; the LT-200B chassis only accepts 12 mm (CASSETTE_ID_12MM).

use letratag_protocol::{set_cassette_type_write, CASSETTE_ID_12MM};

let write = set_cassette_type_write(CASSETTE_ID_12MM).unwrap();
assert_eq!(write.len(), 23);
assert!(set_cassette_type_write(0).is_err());
assert!(set_cassette_type_write(6).is_err());

§API overview

ItemPurpose
RasterValidated 32-row binary frame: new, width, pixels, get, pack_columns, from_packed_columns
ProfileWire profile: Modern (default, S2-derived) or Legacy (S1-derived)
JobOne encoded print job: new, body, writes
parse_headerValidate header framing/checksum, return the bounded body length
StatusSuccess / Unconfirmed(u8) status notifications: parse
set_cassette_type_writeStand-alone set-cassette-type write for the auxiliary characteristic
HEAD_ROWS, MAX_COLUMNSFrame height (32) and software width limit (8000 columns)
CASSETTE_ID_12MMCassette id 3, the only width the LT-200B chassis accepts
ErrorBounded, structured encoding/parsing errors

§Limitations

  • This crate encodes a wire format; it does not print. Transmitting writes to a printer is the job of a transport (see letratag-transport).
  • The legacy profile is hardware-verified on one LT200B firmware (RUTEP20220628); the modern profile is source-backed, not hardware-confirmed. The implementation keeps both profiles explicit and never switches automatically.
  • A status code 0 is not proof of a physical label: the printer reports success even with the lid open (phantom-success finding in the main repository). Completion policy belongs to the consumer.
  • MAX_COLUMNS (8000) is a software limit, not a guaranteed device capability. A job that needs more than 255 chunks is rejected before any write.
  • The literals and framing are transcribed from two MIT-licensed open-source references (ysfchn/dymo-bluetooth, thermal-label/letratag); see the main repository’s docs/protocol.md for sources and evidence levels.

§License

MIT. See the main repository for the full license text and the reference-notice inventory in docs/legal/.

Structs§

Job
Raster
A full 32-row frame, in row-major order, containing only 0 (white) or 1 (black).

Enums§

Error
Profile
Status

Constants§

CASSETTE_ID_12MM
Cassette id 3: 12 mm tape (the only width the LT-200B chassis accepts). The id enum is shared by the ESC M directive and the advertising-data cassette field (S2 docs/protocol/letratag-bt.md): 1=6mm, 2=9mm, 3=12mm, 4=19mm, 5=24mm.
HEAD_ROWS
MAX_COLUMNS
Software resource limit, not a claim about the printer’s capabilities.

Functions§

parse_header
Validate header framing and checksum, returning a software-bounded body length. This does not validate the body or assert that a length describes a print job.
set_cassette_type_write
Stand-alone set-cassette-type write for the short-command characteristic: HEADER + ESC s + ESC M <id> 00 00 00 + ESC Q as one non-print write (S2 docs/protocol/letratag-bt.md, “Stand-alone set-cassette-type payload”). The three zero pad bytes are part of the wire format. S1 documents the same command with a 3-byte ESC M and no padding; that variant was never bench-tested (S1’s CLI does not use it), so the S2 form is used.