This skill documents the ESP32-S3 printer bridge architecture used in PaRang Stikka, so it can be reused in forks and derivative projects. The ESP32 acts as a WiFi-to-USB bridge for thermal/label printers (Brother QL and ESC/POS), receiving pre-encoded raster bytes over HTTP and forwarding them raw to the printer.
┌─────────────┐ HTTP POST /print ┌─────────────┐ USB Host ┌──────────┐
│ Browser │ ────────────────────────> │ ESP32-S3 │ ──────────────> │ Brother │
│ (JS/WASM) │ raw binary raster bytes │ Bridge │ native protocol │ QL │
└─────────────┘ └─────────────┘ └──────────┘
│ ▲
│ GET /status │
└───────────────────────────────────────────┘
Key principle: The browser generates the exact native printer protocol bytes. The ESP32 is a dumb pipe — it does not re-encode, interpret, or spool. It receives raw bytes and writes them directly to the printer's USB/Serial interface.
Your ESP32 Arduino sketch must expose two HTTP endpoints:
Returns HTTP 200 when the ESP32 is alive. The browser polls this to show "ONLINE/OFFLINE" status.
Expected response: Any 2xx response is acceptable. A simple JSON or plain text body is fine.
Example Arduino handler:
server.on("/status", HTTP_GET, []() {
server.send(200, "text/plain", "ok");
});Receives raw binary data (Content-Type: application/octet-stream) and forwards it immediately to the printer.
Request body: Raw Uint8Array — already encoded in Brother QL Raster or ESC/POS format.
Important: Do not parse, inspect, or buffer the entire payload before sending. Stream/chunk the bytes to the printer as they arrive to avoid memory issues on the ESP32.
Example Arduino handler (pseudocode):
server.on("/print", HTTP_POST, []() {
// The WebServer library makes the body available after the route handler,
// so this is usually done in the handler or via stream reading.
String body = server.arg("plain"); // only for small payloads
// OR better: use server.handleClient() with stream reading for binary
});Note: The browser sends raw binary. Make sure your ESP32 HTTP server can accept
application/octet-streamand does not attempt URL decoding or form parsing. UsingESPAsyncWebServeris recommended for reliable binary POST handling.
The browser app generates two kinds of byte streams. Your ESP32 firmware only needs to forward them.
Used for: Brother QL-550, QL-570, QL-700, QL-800, QL-1110 series
Key parameters (for reference when writing decoders/debuggers):
| Parameter | Default (QL-550/700) | QL-1110 |
|---|---|---|
| Total dots/line | 720 | 1200 |
| Printable dots | 696 | 1164 |
| Left margin (dots) | 12 | 18 |
| Bytes per line | totalDots / 8 |
totalDots / 8 |
| Bit packing | MSB-first per byte | MSB-first |
| Tape widths | 12/17/29/38/50/54/62/102 mm | 102 mm |
Command sequence generated by browser:
0x00× 200 — invalidateESC @(0x1B 0x40) — initESC i a 0x01— raster modeESC i z <13 bytes>— print info (flags, media type, tape width, raster line count)ESC i M <1 byte>— auto-cut settingESC i K <1 byte>— expanded mode (cut-at-end)ESC i d 0x00 0x00— marging 0x00 <n> <data...>— raster lines, repeated for each line0x1A— print + feed
Used for: generic 58mm / 80mm / 104mm thermal receipt printers
Command sequence generated by browser:
ESC @(0x1B 0x40) — initGS v 0 0 xL xH yL yH <data...>— raster image commandLF× N — feed linesGS V 66 0x00(full cut) orGS V 0x01(half cut) — cut
- ESP32-S3 with USB OTG Host capability (e.g., ESP32-S3-DevKitC-1, LILYGO T-Display S3, or custom PCB)
- Why S3? Native USB OTG allows USB Host mode to talk to the printer directly.
ESP32-S3 USB OTG Port <──USB cable──> Brother QL Printer
(USB Host mode) (USB Device)
No level shifters needed — it's a standard USB connection. Use a good quality USB cable with data lines (not charge-only).
- The Brother QL pulls significant current during label feed/cutting. Power the ESP32 via a stable 5V/2A supply.
- If experiencing resets or brownouts when printing, add a 100µF+ electrolytic capacitor across the ESP32's 5V input, or use a powered USB hub between ESP32 and printer.
Below is a minimal viable sketch using ESPAsyncWebServer and USBHost (TinyUSB stack on ESP32-S3):
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include "usb/usb_host.h" // ESP32-S3 USB Host
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
Serial.print("IP: ");
Serial.println(WiFi.localIP());
// Initialize USB Host here (platform-specific)
// ... setup TinyUSB/USBHost for CDC/Printer class ...
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "ok");
});
server.on("/print", HTTP_POST, [](AsyncWebServerRequest *request) {},
NULL,
[](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
// Stream chunks to printer as they arrive
// forward_to_printer(data, len);
if (index + len == total) {
request->send(200, "text/plain", "printed");
}
}
);
server.begin();
}
void loop() {
// USB Host task — keep printer connection alive
}For a production-ready implementation, refer to ESP32 USB Host CDC/Printer class examples in the espressif/esp-usb repo, or use a library like USB_Host_Shield_2.0 if using an external USB Host Shield (e.g., MAX3421E) on classic ESP32.
In the web app (PaRang Stikka), users set the ESP32 URL in Printer Settings:
const ESP32_DEFAULT_URL = 'https://print.kiransk.xyz'; // fallback
let esp32Cfg = {
url: localStorage.getItem('stikka-esp32-url') || ESP32_DEFAULT_URL
};
// Sending print
const bytes = (proto === 'brother-ql') ? canvasToBrotherQL(canvas) : canvasToEscPos(canvas);
await fetch(url + '/print', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: bytes,
signal: AbortSignal.timeout(15000)
});To fork/reuse:
- Copy the
canvasToBrotherQL()andcanvasToEscPos()encoder functions fromparang-stikka.html - Point
esp32Cfg.urlat your ESP32's IP - Ensure your ESP32 exposes
/statusand/print
The browser supports these profiles out of the box. When forking, you can trim or extend this list:
| Profile ID | Protocol | Total Dots | Printable | Margin |
|---|---|---|---|---|
ql550 |
brother-ql | 720 | 696 | 12 |
ql570 |
brother-ql | 720 | 696 | 12 |
ql700 |
brother-ql | 720 | 696 | 12 |
ql800 |
brother-ql | 720 | 696 | 12 |
ql1110 |
brother-ql | 1200 | 1164 | 18 |
escpos58 |
escpos | 384 | 384 | 0 |
escpos80 |
escpos | 576 | 576 | 0 |
escpos104 |
escpos | 832 | 832 | 0 |
custom |
configurable | configurable | configurable | configurable |
To add a new printer: Create a new entry in PRINTER_PROFILES and add a corresponding card in the settings modal HTML.
- The ESP32 bridge has no authentication by default. If deployed on a shared WiFi, anyone on the network can
POST /print. - The browser may block cross-origin requests. If hosting the web app on a different domain than the ESP32, add CORS headers:
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "ok"); response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); request->send(response);
- For HTTPS: the ESP32 URL can be
https://. Ensure the ESP32 firmware terminates TLS (rare on ESP32) or use a reverse proxy (e.g., nginx, Caddy) in front of the ESP32.
| Symptom | Likely Cause | Fix |
|---|---|---|
| ESP32 shows "OFFLINE" | Wrong IP, ESP32 not on WiFi, firewall | Check /status with curl |
| Print job sends but nothing prints | Wrong protocol selected | Match printer profile (Brother QL vs ESC/POS) |
| Printer feeds blank paper | Wrong tape width in settings | Set tape width to match loaded media |
| CORS error in browser | ESP32 missing Access-Control-Allow-Origin |
Add CORS headers (see above) |
| ESP32 reboots on print | Insufficient power, brownout | Use external 5V/2A supply, add capacitor |
| Wrong image orientation | Brother QL scans right-to-left | The browser already flips horizontally; check canvasToBrotherQL() |
When creating a new project based on this architecture:
- Copy encoder functions:
canvasToBrotherQL(),canvasToEscPos(),floydSteinberg(),canvasToMono() - Copy
PRINTER_PROFILESarray (or define your own) - Flash an ESP32-S3 with
/statusand/printendpoints - Add ESP32 URL input to your UI
- Wire
printEsp32(canvas, jobName)to your print button - Add CORS headers to the ESP32 firmware
- Test with
curl -X POST -H "Content-Type: application/octet-stream" --data-binary @test.bin http://esp32-ip/print