2026-02-25 18:41:48 +08:00
2026-02-24 21:36:14 +08:00
png
2026-02-24 22:24:21 +08:00
2026-02-25 18:41:48 +08:00
2026-02-24 18:10:40 +08:00
2026-02-24 18:51:53 +08:00
2026-02-24 18:10:40 +08:00
2026-02-24 21:36:14 +08:00
2026-02-25 18:41:48 +08:00
2026-02-24 23:16:33 +08:00
2026-02-24 23:16:33 +08:00
2026-02-25 17:30:03 +08:00

ESP32-S3 TQ Printer Controller (Wi-Fi REST API)

ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:

  • REST client -> ESP32-S3 (esp_http_server)
  • ESP32-S3 -> BLE printer (TQPrinter / lyfPrinter, FFF2 write / FFF1 notify)
  • Command compatibility:
    • Print path: 0x00~0x07
    • OTA path: 0xA0~0xA4

Features

  • Wi-Fi STA-only mode (configured SSID/password; no SoftAP fallback)
  • Built-in web UI at / for connect/status, text print, and Z-Image prompt print
  • BLE central client auto-scan/connect to printer name
  • Async print queue with jobs (queued/running/success/failed/canceled)
  • Printer precheck (paper / battery / temperature)
  • Built-in UTF-8 text rendering with Chinese support (GB2312 character set, 16x16 bitmap)
  • Tap2talk multimodal voice interaction over DashScope WebSocket:
    • ES8311 microphone/speaker via esp_codec_dev
    • raw-opus uplink/downlink via esp_audio_codec
    • WebSocket transport via esp_websocket_client
  • Android-compatible print flow:
    • 0x00 power on
    • 0x03 set print param
    • 0x04 chunked raster send + ACK
    • 0x00 power off
    • 0x02 feed paper
  • REST APIs:
    • Health/connection:
      • GET /v1/health
      • POST /v1/printer/connect
      • POST /v1/printer/disconnect
      • GET /v1/printer/status
    • Print:
      • POST /v1/print/raster
      • POST /v1/print/image (JSON prompt -> generate image -> print, also compatible with direct PNG binary upload)
      • POST /v1/print/qr
      • POST /v1/print/text
      • POST /v1/print/receipt
      • POST /v1/print/label
    • Jobs:
      • GET /v1/jobs
      • DELETE /v1/jobs
      • GET /v1/jobs/{id}
      • DELETE /v1/jobs/{id}
    • Label control:
      • POST /v1/label/gap_move
      • GET /v1/label/offset
      • POST /v1/label/offset
    • OTA:
      • GET /v1/ota/version
      • POST /v1/ota/jump_boot
      • POST /v1/ota/jump_app
      • POST /v1/ota/erase_page
      • POST /v1/ota/write_frame
      • POST /v1/ota/upgrade
    • Voice:
      • GET /v1/voice/status
      • POST /v1/voice/session/start
      • POST /v1/voice/session/stop
      • POST /v1/voice/tap/start
      • POST /v1/voice/tap/cancel

Build

Prerequisites:

  • ESP-IDF v5.x
  • ESP32-S3 board
cd ai_printer
idf.py set-target esp32s3
idf.py menuconfig
idf.py build

Config

In menuconfig -> TQ Controller Config:

  • TQ_WIFI_SSID
  • TQ_WIFI_PASSWORD
  • TQ_HTTP_PORT
  • TQ_API_KEY (optional)
  • Z-Image HTTP auth and model fields:
    • TQ_Z_IMAGE_API_KEY (optional, empty means fallback to TQ_VOICE_API_KEY)
    • TQ_Z_IMAGE_API_ENDPOINT
    • TQ_Z_IMAGE_MODEL
    • TQ_Z_IMAGE_DEFAULT_SIZE
    • TQ_Z_IMAGE_TIMEOUT_MS
    • TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS
  • Voice WebSocket auth and app fields:
    • TQ_VOICE_API_KEY
    • TQ_VOICE_WORKSPACE_ID
    • TQ_VOICE_APP_ID
  • Voice audio and codec fields:
    • TQ_VOICE_SAMPLE_RATE
    • TQ_VOICE_OPUS_FRAME_MS
    • TQ_VOICE_OPUS_BITRATE_KBPS
    • TQ_VOICE_I2C_* / TQ_VOICE_I2S_*
    • TQ_VOICE_CODEC_*

When TQ_API_KEY is not empty, each request must include:

X-API-Key: <your-key>

STA-only behavior:

  • If SSID is empty, startup fails.
  • If STA connect fails, startup fails (no AP fallback).

Flash

cd ai_printer
idf.py -p /dev/tty.usbmodemXXXX flash monitor

Web UI

Open the controller IP in a browser:

http://<esp-ip>/

The page provides:

  • health/status check
  • connect/disconnect printer
  • submit a simple text print job
  • input an image prompt, then let ESP32-S3 call DashScope Z-Image, download PNG, decode/threshold/scale/raster conversion, and print
  • view jobs list

If API key is enabled, input it in the page before invoking actions.

REST Examples

Health

curl http://<esp-ip>/v1/health

Connect printer

curl -X POST http://<esp-ip>/v1/printer/connect \
  -H 'Content-Type: application/json' \
  -d '{"name":"TQPrinter","timeout_ms":15000}'
# Legacy device name is also supported:
# -d '{"name":"lyfPrinter","timeout_ms":15000}'

Auto-match a compatible printer (by BLE service 0xFFF0):

curl -X POST http://<esp-ip>/v1/printer/connect \
  -H 'Content-Type: application/json' \
  -d '{"name":"*","timeout_ms":20000}'

Raster print

curl -X POST http://<esp-ip>/v1/print/raster \
  -H 'Content-Type: application/json' \
  -d '{
    "width":384,
    "height":200,
    "density":"中等",
    "encoding":"base64_msb_1bpp",
    "data":"<BASE64_BYTES>"
  }'

Image print (generate with prompt, then print)

curl -X POST http://<esp-ip>/v1/print/image \
  -H 'Content-Type: application/json' \
  -d '{
    "prompt":"一只坐在窗边的橘猫,午后阳光,胶片质感,写实风格。",
    "size":"1120*1440",
    "prompt_extend":false,
    "threshold":160,
    "max_height":2200,
    "scale_to_width":true,
    "invert":false,
    "density":"中等",
    "timeout_ms":45000,
    "fetch_timeout_ms":15000
  }'

Image print (direct PNG upload, backward-compatible)

curl -X POST 'http://<esp-ip>/v1/print/image?scale_to_width=1&threshold=160&invert=0&max_height=2200&density=medium' \
  -H 'Content-Type: image/png' \
  --data-binary @./sample.png

Request constraints:

  • Request body limit: 4 MB
  • Suggested image size: <= 3 MB when using direct PNG upload compatibility mode

QR print

curl -X POST http://<esp-ip>/v1/print/qr \
  -H 'Content-Type: application/json' \
  -d '{
    "text":"https://example.com/pay/123",
    "ecc":"H",
    "module_scale":0,
    "margin_modules":2,
    "density":"中等"
  }'

Text print

curl -X POST http://<esp-ip>/v1/print/text \
  -H 'Content-Type: application/json' \
  -d '{
    "text":"欢迎使用TQ打印机\\n订单号: A1024\\n谢谢惠顾",
    "density":"中等",
    "scale":2,
    "line_spacing":2,
    "max_height":1800
  }'

Receipt print

curl -X POST http://<esp-ip>/v1/print/receipt \
  -H 'Content-Type: application/json' \
  -d '{
    "title":"TQ FOOD",
    "density":"中等",
    "footer":"Thanks!",
    "items":[
      {"name":"DishA","qty":2,"price":12.5},
      {"name":"DishB","qty":1,"price":8.0}
    ]
  }'

Label print (with optional gap/offset)

curl -X POST http://<esp-ip>/v1/print/label \
  -H 'Content-Type: application/json' \
  -d '{
    "width":384,
    "height":260,
    "encoding":"base64_msb_1bpp",
    "data":"<BASE64_BYTES>",
    "gap_move_before":true,
    "offset_tenths_mm":128,
    "density":"中等"
  }'

Jobs

curl http://<esp-ip>/v1/jobs
curl http://<esp-ip>/v1/jobs/1
curl -X DELETE http://<esp-ip>/v1/jobs/1
curl -X DELETE http://<esp-ip>/v1/jobs -d '{"include_success":true,"include_failed":true,"include_canceled":true}'

Label control

curl -X POST http://<esp-ip>/v1/label/gap_move
curl http://<esp-ip>/v1/label/offset
curl -X POST http://<esp-ip>/v1/label/offset \
  -H 'Content-Type: application/json' \
  -d '{"offset_tenths_mm":128}'

OTA primitives

curl http://<esp-ip>/v1/ota/version
curl -X POST http://<esp-ip>/v1/ota/jump_boot
curl -X POST http://<esp-ip>/v1/ota/erase_page -H 'Content-Type: application/json' -d '{"page_num":0}'
curl -X POST http://<esp-ip>/v1/ota/write_frame -H 'Content-Type: application/json' -d '{"packet_num":0,"is_last_frame":false,"data":"<BASE64_<=236_BYTES>"}'
curl -X POST http://<esp-ip>/v1/ota/jump_app

OTA full upgrade

curl -X POST http://<esp-ip>/v1/ota/upgrade \
  -H 'Content-Type: application/json' \
  -d '{
    "firmware":"<BASE64_FIRMWARE>",
    "jump_boot":true,
    "jump_app":true,
    "page_size":1024,
    "packet_size":236,
    "timeout_ms_per_step":5000
  }'

Voice tap2talk

Start session:

curl -X POST http://<esp-ip>/v1/voice/session/start

Start one tap2talk round:

curl -X POST http://<esp-ip>/v1/voice/tap/start

Cancel current tap2talk round:

curl -X POST http://<esp-ip>/v1/voice/tap/cancel

Get voice status:

curl http://<esp-ip>/v1/voice/status

Stop session:

curl -X POST http://<esp-ip>/v1/voice/session/stop

Notes

  • BLE side is central/client role, not printer peripheral role.
  • base64_msb_1bpp uses Android-compatible bit order (MSB first).
  • /v1/print/image supports two modes: JSON prompt generation (DashScope Z-Image) and raw image/png upload (no base64 wrapper).
  • Large buffers for image upload/decode prefer PSRAM first, then fallback to internal RAM.
  • Partition table uses partitions.csv with a 4MB factory app partition on 16MB flash modules.
  • /v1/print/text and /v1/print/receipt now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
  • Characters outside embedded glyph set are rendered as square fallback boxes.
  • QR encoding uses embedded qrcodegen (Project Nayuki C implementation).
  • Tap2talk uses server-side VAD and keeps uploading 100ms raw-opus frames in Listening state (silence when user is not tapping), matching docs/websocket.md requirements.

Font Assets

Embedded files:

  • main/domain/assets/fonts/cn16_index.bin
  • main/domain/assets/fonts/cn16_glyphs.bin

Regenerate from your own CJK font:

cd ai_printer
python3 -m pip install --user pillow
python3 tools/gen_cn16_font.py \
  --font app/src/main/assets/fonts/msyh.ttc \
  --font-index 0
Description
No description provided
Readme 165 MiB
Languages
C 67.4%
Python 29.2%
HTML 2.9%
CMake 0.5%