- Go 95.7%
- Just 2.3%
- Shell 2%
| .just | ||
| .woodpecker | ||
| scripts | ||
| .gitignore | ||
| args.go | ||
| CHANGELOG.md | ||
| device.go | ||
| go.mod | ||
| go.sum | ||
| justfile | ||
| main.go | ||
| protocol.go | ||
| README.md | ||
| render.go | ||
| verify_test.go | ||
dymo
A small Go CLI for the DYMO LabelManager 280 (USB 0922:1005).
just build # or: go build -o dymo .
just install # installs to /usr/local/bin
dymo print "print this text"
It renders text to a bitmap and speaks DYMO's "matrix" raster protocol straight
to the kernel usblp character device (/dev/usb/lp0) — no libusb, no driver
detaching. The protocol/bit-packing is verified byte-for-byte against the
labelle reference implementation.
Usage
usage: dymo <command> [flags]
commands:
print render text and print it to a DYMO LabelManager 280
info list detected DYMO / usblp devices
version print version
Print flags
| Flag | Default | Meaning |
|---|---|---|
-tape |
12 |
Tape width in mm: 6, 9, 12, 19 |
-size |
fill tape | Text size: xs/sm/m/l/xl/xxl, xsmall..xxlarge, or a point size like 8/10/12/16/20/24 |
-vertical |
off | Rotate text 90° for labels read along a vertical surface (e.g. a cable) |
-flip |
off | Rotate the label 180° (read the other way up) |
-font |
built-in Go Bold | Path to a TrueType/OpenType font |
-margin |
0 |
Extra margin (mm) on each side, beyond the cutter gap |
-gap |
9.5 |
Print head-to-cutter distance (mm); sets the minimum, symmetric margin |
-dottab |
-1 |
Vertical bias in bytes; -1 auto-centres narrow tape on the head |
-device |
autodetect | usblp device path |
-cut |
off | Append auto-cut command (LM280 normally cuts manually) |
-o |
– | Dump raw protocol bytes to a file instead of printing |
-preview |
– | Write a PNG preview of the rendered label |
-dry-run |
off | Render only; send nothing |
-v |
off | Verbose |
Examples
./dymo print "Hello"
./dymo print -tape 9 "9mm tape"
./dymo print -size xl "big text"
./dymo print -size 12 "12 point"
./dymo print -flip "upside down"
./dymo print -tape 9 -vertical "cable-1" # rotated 90° for a cable
./dymo print 'WiFi\npassword: hunter2' # two lines
./dymo print -font /usr/share/fonts/TTF/DejaVuSans.ttf "custom font"
./dymo print -dry-run -preview out.png "check before printing"
echo "from stdin" | ./dymo print
Margins and the cutter gap
The print head sits a fixed distance behind the cutter (the gap), so the
leading margin can never be smaller than that gap — the printer can't
reverse-feed. To get equal margins, the tool feeds 2 × gap of trailing tape so
the trailing margin matches the leading one:
leading blanks = margin visible leading = gap + margin
trailing blanks = margin + 2·gap visible trailing = gap + margin
The default -gap 9.5 is calibrated from measured prints on a LabelManager 280.
If your trailing margin comes out larger than the leading one, lower -gap;
if it's smaller (text cut too soon), raise it. -margin adds equal space to
both sides on top of that.
Vertical centering
On tape narrower than the print head (anything under 12 mm), the print band is
biased to the centre of the head with the ESC B dot-tab command so the text
sits centred across the tape. This is automatic (-dottab -1); pass an explicit
-dottab 0 to disable it, or another value to nudge the band up/down in 1-byte
(8-dot) steps.
Permissions
usblp exposes /dev/usb/lp0 as root:lp (mode 660). Either run with sudo,
join the lp group, or install a udev rule:
echo 'SUBSYSTEM=="usbmisc", ATTRS{idVendor}=="0922", MODE="0660", GROUP="lp"' \
| sudo tee /etc/udev/rules.d/91-dymo.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo usermod -aG lp "$USER" # then re-login
Re-plug the printer afterwards.
How it works
180 DPI; the printable height is int(8 * tape_mm / 12) bytes → 8 bytes
(64 dots) for 12 mm tape. The label is a stream of vertical columns:
ESC C 0 tape colour / init
ESC D <n> n bytes per column
SYN <n bytes> one printed column (repeated, left → right)
...
ESC A status request / end of label
Each column packs pixels MSB-first with y = H-1-i, matching labelle's
image.transpose(ROTATE_270).tobytes(). Blank columns are added before and
after the text to produce symmetric margins around the cutter gap (see
Margins and the cutter gap).
Notes / limitations
- The LM280 has a manual cutter;
-cutis provided for completeness but may be a no-op on this model. - Long labels are paced with a status handshake: before each batch of ~64
columns the tool sends
ESC Aand waits for the printer to reply (it only does so once it has buffer room), so the tail of long labels isn't dropped.-no-handshakestreams without it (faster, but long labels may lose their end).