A Linux CLI for the Rongta RP332 thermal receipt printer. The protocol was undocumented, the config tool was Windows-only, and the vendor mobile SDKs ship the relevant methods as
mov x0, #0; retstubs. So we routedPrinterTool.exe's output through Wine and a custom logging CUPS backend, captured every byte it emits, and re-implemented all of it in seven Python scripts, stdlib only, zero third-party dependencies.
Every NV-RAM setting the proprietary PrinterTool.exe v2.63.0 can
flip — auto-cutter, buzzer, drawer kick, font, density, paper width,
DHCP, static IP, MAC address, Chinese character mode, 43 code pages,
black-mark sensor, paper-save trimming — is now flippable from any
Linux host with python3 and the printer attached over USB.
unspooled reverse-engineered every command the vendor tool emits
and re-implements them in seven small Python scripts. Hardware on
hand is a Rongta RP332; the protocol family likely also covers
RP325, RP326, RP328, and other Rongta SKUs that share the
PrinterTool.exe config tool (untested — see CONTRIBUTING.md).
The vendor tool talks to the printer through the Windows print spooler. We routed that spool through a logging CUPS backend on Linux (the printer presented to Wine as a CUPS printer) and captured every byte. The project is the printer literally being "unspooled" out of the vendor pipeline — and the protocol itself being unspooled into something documented.
- Printer: Rongta RP332, 80mm thermal, auto-cut, USB+Serial+Ethernet.
- USB id:
0fe6:811e(the printer presents as an ICS Advent Parallel Adapter — Rongta licenses the USB-to-parallel chip from ICS Advent). - Likely also works on: other Rongta SKUs that share the
PrinterTool.execonfig tool (RP325, RP326, RP328 are reported to share the protocol family but untested; PRs welcome).
The RP332 has no WiFi or Bluetooth hardware, so PrinterTool.exe's WiFi tab and Bluetooth-setting tab were never reverse-engineered. If you have a Rongta SKU with WiFi or BT and are willing to repeat the technique on it, open a "Help wanted" issue. Same goes for the UDP-discovery protocol behind the "Search Printer" tab. See
CONTRIBUTING.md.
- Python 3.9+ — stdlib only, no third-party packages. That's the whole runtime dependency footprint.
- A Linux host with the printer attached via USB
- Membership in the
plugdevgroup (so you can write to the printer withoutsudo) - The udev rule in this repo (
99-rongta-receipt.rules) installed to/etc/udev/rules.d/
git clone git@github.com:bryanwintermute/unspooled.git
cd unspooled
# Install the udev rule (one-time, requires sudo)
sudo install -o root -g root -m 0644 99-rongta-receipt.rules \
/etc/udev/rules.d/99-rongta-receipt.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --action=change /sys/class/usbmisc/lp0
sudo udevadm settle
# Verify the symlink exists
ls -la /dev/rongta-receipt # should point to usb/lp0Add yourself to plugdev if you're not already (log out + back in
after):
sudo usermod -aG plugdev "$USER"Most commands in this CLI write to the printer's NV-RAM. The writes are persistent across power cycles — there is no "undo" beyond writing the previous value back. Wrong values can leave the printer in a state where the only recovery path is this CLI itself (which is also the project's de-facto factory-reset).
Three concrete failure modes worth knowing before you flip anything:
rongta_config.py other1 usb-mode virtual-serial— makes the printer re-enumerate as/dev/ttyACM*instead of/dev/usb/lp0. The udev rule in this repo won't fire forttyACMdevices, so/dev/rongta-receiptwill not exist. You'll need a different recovery path. Don't run this casually.rongta_config.py ethernet mac <bad-mac>— if you change the MAC and forget the original, you can't read it back over USB (the firmware echoes confirmations, but only of the value you sent). Always note the existing MAC from the power-on-self-test report before changing it.rongta_config.py ethernet static --ip <bad-ip>— wrong static IP / gateway / subnet can isolate the printer on its own Ethernet but it's harmless if you're driving over USB.
Always use --dry-run first. Every command supports it.
Print the bytes, eyeball them, then drop the flag.
./rongta_config.py base --cutter on --buzzer on --dry-run
# 1f 73 02 00 00 01 00 00 00 00 00 1f 72 00 1f 74 00If you do botch a setting, re-run with the desired values. The CLI is its own factory-reset.
The unified entry point is rongta_config.py. It dispatches to
six per-tab modules (each of which is also runnable standalone if
you prefer narrower help):
# Out-of-the-box: enable DHCP so the printer is reachable on the LAN.
./rongta_config.py ethernet dhcp on
# Out-of-the-box: enable the NV-gated auto-cutter (off from factory).
./rongta_config.py base --cutter on
# Aggressive paper-saving for shopping-list-style receipts.
./rongta_config.py papersave --delete-top enable --cut-line-interval 75%
# Switch paper width to 58mm.
./rongta_config.py other1 print-width 58mm
# Print a list.
echo -e 'milk\neggs\nbread' | ./rongta_config.py print --title 'Costco'
# Full help for any area:
./rongta_config.py <area> --help| Area | Module | Coverage |
|---|---|---|
base |
nv_config.py |
Cutter, buzzer, drawer kick, font, density, char/line, code page (43 named entries, sourced from the printer's own self-test report; 5 reserved slots accessible via --code-page-raw), baud rate, parity, auto-reprint, buzzer-after-print. |
ethernet |
ethernet_config.py |
DHCP, static IP, submask, gateway, MAC address, link mode. |
papersave |
papersave_config.py |
Whitespace trimming (uses standard Epson GS ( E). |
blackmark |
blackmark_config.py |
Black-mark sensor: enable/disable, length, width, print/cut offset. |
other1 |
other1_config.py |
Paper width (80mm/58mm), buzzer volume, alarm, USB enumeration mode, Chinese character mode, cutter-count query. |
print |
receipt_print.py |
Render a list (with --title, --style, etc.) as ESC/POS. |
Roughly half of what the vendor tool emits is standard Epson ESC/POS — documented in the public Epson TM-T88 / TM-T20 spec. The other half is Rongta-vendor extensions with no public docs.
| Prefix | Family | Coverage |
|---|---|---|
1f 73 XX <args> |
Rongta vendor | Base tab base-config (+ sub-fns 1f 72, 1f 74) |
1f 69, 1f 25, 1f 4e, 1f 6d, 1f 70, 1f 62 44 |
Rongta vendor | Ethernet (IP/submask/gateway/MAC/duplex/DHCP) |
1f 1b 1f XX <args> |
Rongta vendor extended | BlackMark + Other1 |
1f 7b X <arg> |
Rongta vendor mode toggles | Paper sensor ('p'), USB mode ('u') |
1d 28 45 ... |
Standard Epson GS ( E |
PaperSave + Volume |
1d 28 46 ... |
Standard Epson GS ( F |
BlackMark print/cut-after offsets |
1d 56 00 |
Standard Epson GS V 0 |
Full-cut (runtime) |
12 54 |
Standard Epson DC2 'T' | Self-test trigger |
1b 1b 45 ... 0c 5a |
Rongta vendor "structured" | Reset button — emits a "Setting Fail!" on this firmware. Documented but non-functional. |
Full technique in
docs/wine-cups-backend-recovers-nv-bytes.md.
Short version:
- usbip-export the printer from the Pi it lives on to an x86_64 Linux host (so Wine can run on x86 while the printer stays on the Pi).
- Run
PrinterTool.exeunder Wine (Xvfb + x11vnc lets you click through it from a phone VNC client). - Install a custom CUPS backend at
/usr/lib/cups/backend/rongta(mode0700so it runs as root) thattees every print-spool job to/tmp/rongta-writes/<ns>.bin. - Click through the GUI: each click = one labelled
.binfile. Diff them to find the bytes that change.
A concrete example — the Base tab's "Set" command with four isolated states (all-off, only-cutter, only-drawer, only-buzzer) produces these four 17-byte files. Aligning them column-wise:
┌─cutter
│ ┌─buzzer
│ │ ┌─drawer
all-off : 1f 73 02 | 01 01 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00
cutter-only : 1f 73 02 | 00 01 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00
drawer-only : 1f 73 02 | 01 01 00 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00
buzzer-only : 1f 73 02 | 01 00 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00
└────────────┘
three settings,
inverted booleans
(0 = on, 1 = off)
Position 3 only changes when Cutter is toggled, position 4 only when Buzzer, position 5 only when Drawer. The encoding is inverted (0 = on, 1 = off) because the factory firmware is shipped with everything off and "0" means "default no-add-ons". Four clicks → complete bit-mapping in 30 seconds of diffing.
For big enum dropdowns (like code pages), there's an even
cheaper trick: static-analyse the PE binary. MFC dropdown
labels are stored as contiguous string literals in the binary's
.rdata section. MSVC emits them bottom-up (reverse source
order), so:
strings -el -t d PrinterTool.exe | grep -E '^(CP|WCP|ISO|Katakana)' | sort -rn…gives you the dropdown labels in their visual order. Important: dropdown order is NOT the same as wire-byte order. The RP332's first 6 code-page entries (CP437, Katakana, CP850/860/863/865) happen to be wire bytes 0-5 because the most-common pages are listed first AND happen to have the lowest enum values — but past that, the dropdown order diverges.
The truly cheap source of truth turned out to be the printer's
own self-test report: the RP332's power-on diagnostic prints
its full 48-entry code-page table verbatim. We just hadn't read
all the way to the bottom of the receipt. Always read every
diagnostic output the device exposes before reaching for static
analysis. Full debrief in
docs/wine-cups-backend-recovers-nv-bytes.md.
See docs/ for the full lesson set:
wine-cups-backend-recovers-nv-bytes.md— the core technique with 7 distinct RE patterns.vendor-mobile-sdks-may-stub-nv-config.md— the prequel: how we disassembled Rongta's iOS/Android SDKs and proved the NV-config methods were stubs.escpos-thermal-printers-need-no-cups-driver.md— the foundational lesson: ESC/POS printers don't need CUPS for the basic print path.rongta-rp332-vendor-tool-replacement-recap.md— project-recap: what was built, what's still TODO.udev-settle-after-trigger-or-rebind.md— the trigger/settle discipline used in the setup recipe.
All major NV-setting tabs in PrinterTool.exe v2.63.0 are
reverse-engineered. Remaining items (all nice-to-haves):
- Bluetooth setting tab — RP332 has no BT hardware; tab might emit no-op commands or preview a different family for other Rongta SKUs.
- UDP discovery (the tool's "Search Printer" tab) — would be nice as a Python equivalent.
- Capture cutter-stats response — the cutter-count query is
exposed as
rongta_config.py other1 cutter-query, but the response comes back on BULK-IN, which the CUPS backend doesn't relay. Capture withusbmonto decode and parse. - Find a working factory-reset command — the GUI's Reset
button emits a 13-byte structured packet that the firmware
rejects ("Setting Fail!"). Trailing
0c 5asmells like a checksum; figuring it out + sending a real reset would be neat.
See docs/rongta-rp332-vendor-tool-replacement-recap.md
for the full wishlist.
Apache-2.0. See LICENSE.
PRs welcome. See CONTRIBUTING.md for the
quick path to adding a new Rongta SKU (or extending the protocol
catalogue with bytes we haven't captured).