Skip to content

Instantly share code, notes, and snippets.

@5shekel
Last active May 30, 2026 12:48
Show Gist options
  • Select an option

  • Save 5shekel/4fd93dd57be1ba021c39aadef4c53e4e to your computer and use it in GitHub Desktop.

Select an option

Save 5shekel/4fd93dd57be1ba021c39aadef4c53e4e to your computer and use it in GitHub Desktop.

PaRang Stikka — ESP32 Bridge Skill

Purpose

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.


Architecture Overview

┌─────────────┐     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.


ESP32 HTTP Interface Contract

Your ESP32 Arduino sketch must expose two HTTP endpoints:

1. GET /status

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");
});

2. POST /print

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-stream and does not attempt URL decoding or form parsing. Using ESPAsyncWebServer is recommended for reliable binary POST handling.


Supported Printer Protocols

The browser app generates two kinds of byte streams. Your ESP32 firmware only needs to forward them.

Protocol A: Brother QL Raster

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:

  1. 0x00 × 200 — invalidate
  2. ESC @ (0x1B 0x40) — init
  3. ESC i a 0x01 — raster mode
  4. ESC i z <13 bytes> — print info (flags, media type, tape width, raster line count)
  5. ESC i M <1 byte> — auto-cut setting
  6. ESC i K <1 byte> — expanded mode (cut-at-end)
  7. ESC i d 0x00 0x00 — margin
  8. g 0x00 <n> <data...> — raster lines, repeated for each line
  9. 0x1A — print + feed

Protocol B: ESC/POS

Used for: generic 58mm / 80mm / 104mm thermal receipt printers

Command sequence generated by browser:

  1. ESC @ (0x1B 0x40) — init
  2. GS v 0 0 xL xH yL yH <data...> — raster image command
  3. LF × N — feed lines
  4. GS V 66 0x00 (full cut) or GS V 0x01 (half cut) — cut

ESP32 Hardware Setup

Recommended Board

  • 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.

Wiring (Brother QL via USB)

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).

Power Considerations

  • 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.

Example ESP32 Arduino Sketch

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.


Browser-Side Configuration

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:

  1. Copy the canvasToBrotherQL() and canvasToEscPos() encoder functions from parang-stikka.html
  2. Point esp32Cfg.url at your ESP32's IP
  3. Ensure your ESP32 exposes /status and /print

Printer Profiles

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.


Security Notes

  • 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.

Debug Checklist

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()

Forking Checklist

When creating a new project based on this architecture:

  • Copy encoder functions: canvasToBrotherQL(), canvasToEscPos(), floydSteinberg(), canvasToMono()
  • Copy PRINTER_PROFILES array (or define your own)
  • Flash an ESP32-S3 with /status and /print endpoints
  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment