375 lines
9.9 KiB
Markdown
375 lines
9.9 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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_*`
|
|
- Board GPIO map fields:
|
|
- `TQ_POWER_*`, `TQ_LED_*`, `TQ_SCREEN_*`, `TQ_PRINT_*`, `TQ_SPI_*`
|
|
- `TQ_KEY_PRINT_BOOST_ENABLE_ON_BOOT` / `TQ_KEY_PRINT_BOOST_ACTIVE_HIGH`
|
|
- ST7789 screen fields:
|
|
- `TQ_SCREEN_ENABLE`
|
|
- `TQ_SCREEN_TEST_PATTERN_ON_BOOT`
|
|
- `TQ_SCREEN_PIXEL_CLOCK_HZ`
|
|
- `TQ_SCREEN_SPI_MODE`
|
|
- `TQ_SCREEN_H_RES` / `TQ_SCREEN_V_RES`
|
|
- `TQ_SCREEN_DRAW_LINES`
|
|
- `TQ_SCREEN_COLOR_ORDER_BGR`
|
|
- `TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH`
|
|
- `TQ_SCREEN_MIRROR_*` / `TQ_SCREEN_SWAP_XY`
|
|
- `TQ_SCREEN_X_GAP` / `TQ_SCREEN_Y_GAP`
|
|
- `TQ_SCREEN_RESET_PIN`
|
|
|
|
Board pin assignment reference:
|
|
- [`docs/gpio-map.md`](docs/gpio-map.md)
|
|
|
|
When `TQ_API_KEY` is not empty, each request must include:
|
|
|
|
```text
|
|
X-API-Key: <your-key>
|
|
```
|
|
|
|
STA-only behavior:
|
|
- If SSID is empty, startup fails.
|
|
- If STA connect fails, startup fails (no AP fallback).
|
|
|
|
## Flash
|
|
|
|
```bash
|
|
cd ai_printer
|
|
idf.py -p /dev/tty.usbmodemXXXX flash monitor
|
|
```
|
|
|
|
## Web UI
|
|
|
|
Open the controller IP in a browser:
|
|
|
|
```text
|
|
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
|
|
```bash
|
|
curl http://<esp-ip>/v1/health
|
|
```
|
|
|
|
### Connect printer
|
|
```bash
|
|
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`):
|
|
|
|
```bash
|
|
curl -X POST http://<esp-ip>/v1/printer/connect \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"name":"*","timeout_ms":20000}'
|
|
```
|
|
|
|
### Raster print
|
|
```bash
|
|
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)
|
|
```bash
|
|
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)
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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)
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
curl -X POST http://<esp-ip>/v1/voice/session/start
|
|
```
|
|
|
|
Start one tap2talk round:
|
|
|
|
```bash
|
|
curl -X POST http://<esp-ip>/v1/voice/tap/start
|
|
```
|
|
|
|
Cancel current tap2talk round:
|
|
|
|
```bash
|
|
curl -X POST http://<esp-ip>/v1/voice/tap/cancel
|
|
```
|
|
|
|
Get voice status:
|
|
|
|
```bash
|
|
curl http://<esp-ip>/v1/voice/status
|
|
```
|
|
|
|
Stop session:
|
|
|
|
```bash
|
|
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).
|
|
- Voice flow is now single-round: audio uplink stops after `SpeechEnded`, then `LocalRespondingEnded` and `Stop` are sent after local playback drain.
|
|
- Web UI blocks starting the next voice round while voice is Thinking/Responding, marker image generation is running, or printer queue is still busy.
|
|
|
|
## Font Assets
|
|
|
|
Embedded files:
|
|
- `components/domain/assets/fonts/cn16_index.bin`
|
|
- `components/domain/assets/fonts/cn16_glyphs.bin`
|
|
|
|
Regenerate from your own CJK font:
|
|
|
|
```bash
|
|
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
|
|
```
|