refactor(control_plane): remove business http server stack
This commit is contained in:
250
README.md
250
README.md
@@ -1,42 +1,26 @@
|
||||
# ESP32-S3 TQ Printer Controller (Wi-Fi REST API)
|
||||
# ESP32-S3 TQ Printer Controller (Direct Thermal, No Business HTTP Server)
|
||||
|
||||
ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
||||
- REST client -> ESP32-S3 (`esp_http_server`)
|
||||
- ESP32-S3 -> onboard direct thermal printer driver
|
||||
This firmware now runs in direct-printer mode and does not expose the previous
|
||||
business REST endpoints (`/v1/*`).
|
||||
|
||||
## Features
|
||||
|
||||
- Wi-Fi STA-only mode (configured SSID/password; no SoftAP fallback)
|
||||
- Built-in minimal web UI at `/` for voice status and MIC_KEY guidance
|
||||
- Direct thermal printer control with sensor precheck
|
||||
- Async print queue with jobs (`queued/running/success/failed/canceled`)
|
||||
- Direct thermal printer backend (`PRINTER_BACKEND_DIRECT`)
|
||||
- Auto-connect direct printer during startup
|
||||
- Async print queue and worker lifecycle management
|
||||
- Printer precheck (paper / battery / temperature)
|
||||
- Built-in UTF-8 text rendering with Chinese support (GB2312 character set, 16x16 bitmap)
|
||||
- Push2talk 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`
|
||||
- REST APIs:
|
||||
- Root:
|
||||
- `GET /`
|
||||
- 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/text`
|
||||
- Jobs:
|
||||
- `GET /v1/jobs`
|
||||
- `GET /v1/jobs/{id}`
|
||||
- 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`
|
||||
- MIC key push-to-talk voice flow (button-triggered, no HTTP trigger)
|
||||
- Wi-Fi STA with SoftAP provisioning fallback
|
||||
- Provisioning portal page at `http://192.168.4.1` (unchanged)
|
||||
- Optional DashScope Z-Image integration for image generation
|
||||
|
||||
## What Was Removed
|
||||
|
||||
- Business HTTP server in `control_plane` (including previous `/`, `/v1/health`,
|
||||
`/v1/printer/*`, `/v1/print/*`, `/v1/jobs*`, `/v1/voice/*` endpoints)
|
||||
- Related Kconfig items:
|
||||
- `TQ_HTTP_PORT`
|
||||
- `TQ_API_KEY`
|
||||
|
||||
## Build
|
||||
|
||||
@@ -51,33 +35,35 @@ idf.py menuconfig
|
||||
idf.py build
|
||||
```
|
||||
|
||||
## Flash
|
||||
|
||||
```bash
|
||||
cd ai_printer
|
||||
idf.py -p /dev/tty.usbmodemXXXX flash monitor
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
In `menuconfig -> TQ Controller Config`:
|
||||
- `TQ_WIFI_PROV_SOFTAP_SSID`
|
||||
- `TQ_WIFI_PROV_SOFTAP_PASSWORD`
|
||||
- `TQ_WIFI_PROV_SOFTAP_CHANNEL`
|
||||
- `TQ_WIFI_PROV_SOFTAP_MAX_CONN`
|
||||
- `TQ_HTTP_PORT`
|
||||
- `TQ_API_KEY` (optional)
|
||||
- Z-Image HTTP auth and model fields:
|
||||
|
||||
- Wi-Fi provisioning:
|
||||
- `TQ_WIFI_PROV_SOFTAP_SSID`
|
||||
- `TQ_WIFI_PROV_SOFTAP_PASSWORD`
|
||||
- `TQ_WIFI_PROV_SOFTAP_CHANNEL`
|
||||
- `TQ_WIFI_PROV_SOFTAP_MAX_CONN`
|
||||
- Direct thermal printer runtime and safety policy:
|
||||
- `TQ_DIRECT_PRINTER_*`
|
||||
- `TQ_PRINTER_*`
|
||||
- Z-Image HTTP 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:
|
||||
- Voice and audio fields (used by MIC key push-to-talk flow):
|
||||
- `TQ_VOICE_*`
|
||||
- Board GPIO map:
|
||||
- `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:
|
||||
@@ -96,157 +82,23 @@ In `menuconfig -> TQ Controller Config`:
|
||||
Board pin assignment reference:
|
||||
- [`docs/gpio-map.md`](docs/gpio-map.md)
|
||||
|
||||
When `TQ_API_KEY` is not empty, each request must include:
|
||||
## Wi-Fi Provisioning Behavior
|
||||
|
||||
```text
|
||||
X-API-Key: <your-key>
|
||||
```
|
||||
|
||||
Wi-Fi provisioning behavior:
|
||||
- Device first tries saved STA credentials from NVS (`wifi_cfg` namespace).
|
||||
- If no valid credentials exist or STA connect fails, device starts a SoftAP portal.
|
||||
- Connect to the SoftAP and open `http://192.168.4.1`; the static page auto-scans nearby SSIDs for selection.
|
||||
- Provisioning page supports Chinese/English switching and remembers the selected language in browser local storage.
|
||||
- Credentials are written to NVS only after STA connect succeeds.
|
||||
|
||||
## 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 a minimal voice console:
|
||||
- MIC_KEY(IO14) trigger guide (press to talk, release to process)
|
||||
- voice status display
|
||||
|
||||
## 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 '{"timeout_ms":15000}'
|
||||
# Optional explicit backend field:
|
||||
# -d '{"backend":"direct","timeout_ms":15000}'
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
}'
|
||||
```
|
||||
|
||||
### Jobs
|
||||
```bash
|
||||
curl http://<esp-ip>/v1/jobs
|
||||
curl http://<esp-ip>/v1/jobs/1
|
||||
```
|
||||
|
||||
### Voice push2talk
|
||||
Start session:
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/session/start
|
||||
```
|
||||
|
||||
Press-to-talk start (sends `SendSpeech` when entering `Listening`):
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/tap/start
|
||||
```
|
||||
|
||||
Release-to-stop (sends `StopSpeech`):
|
||||
|
||||
```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
|
||||
```
|
||||
- Device first tries saved STA credentials from NVS (`wifi_cfg` namespace)
|
||||
- If credentials are invalid or STA connect fails, device starts SoftAP portal
|
||||
- Open `http://192.168.4.1` and submit credentials from the provisioning page
|
||||
- Provisioning page supports Chinese/English switching and local language cache
|
||||
- Credentials are written to NVS only after STA connect succeeds
|
||||
|
||||
## Notes
|
||||
|
||||
- `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).
|
||||
- `/v1/print/text` supports UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
|
||||
- Characters outside embedded glyph set are rendered as square fallback boxes.
|
||||
- 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.
|
||||
- Voice flow uses `push2talk`: client sends `SendSpeech` to start uplink and `StopSpeech` on release to end input.
|
||||
- Voice flow is now single-round: after recognition/response completes, `LocalRespondingEnded` and `Stop` are sent after local playback drain.
|
||||
- Voice flow blocks starting the next round while voice is Thinking/Responding, marker image generation is running, or printer queue is still busy.
|
||||
- This project no longer provides business HTTP API endpoints for print/voice/job
|
||||
control.
|
||||
- SoftAP provisioning HTTP service remains enabled by design.
|
||||
- UTF-8 Chinese text rendering uses embedded 16x16 GB2312 glyphs.
|
||||
- Large image buffers prefer PSRAM, then fall back to internal RAM.
|
||||
- Partition table uses `partitions.csv` with a 4MB `factory` app partition on
|
||||
16MB flash modules.
|
||||
|
||||
## Font Assets
|
||||
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/controller_lifecycle.c"
|
||||
"src/rest_server.c"
|
||||
"src/rest_server_common.c"
|
||||
"src/rest_server_ops.c"
|
||||
"src/rest_server_print.c"
|
||||
"src/rest_server_print_image.c"
|
||||
"src/rest_server_print_text.c"
|
||||
"src/rest_server_jobs.c"
|
||||
"src/rest_server_voice.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
PRIV_INCLUDE_DIRS
|
||||
"internal"
|
||||
REQUIRES
|
||||
domain
|
||||
esp_http_server
|
||||
json
|
||||
mbedtls
|
||||
)
|
||||
|
||||
@@ -29,9 +29,6 @@ esp_err_t controller_lifecycle_stop(void);
|
||||
void controller_lifecycle_get_status(controller_lifecycle_status_t *out_status);
|
||||
const char *controller_lifecycle_state_str(controller_lifecycle_state_t state);
|
||||
|
||||
esp_err_t rest_server_start(void);
|
||||
void rest_server_stop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
bool rest_server_auth_ok(httpd_req_t *req);
|
||||
esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root);
|
||||
esp_err_t rest_server_send_error(httpd_req_t *req, const char *status, const char *message);
|
||||
esp_err_t rest_server_read_body(httpd_req_t *req, char **out_body);
|
||||
bool rest_server_parse_uri_u32_tail(const char *uri, uint32_t *out_value);
|
||||
esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, size_t *out_len);
|
||||
bool rest_server_json_bool_with_default(cJSON *item, bool default_value);
|
||||
esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req,
|
||||
const uint8_t *raw,
|
||||
size_t raw_len,
|
||||
uint16_t width,
|
||||
uint16_t height,
|
||||
const char *density,
|
||||
bool direct_ignore_precheck,
|
||||
const char *warning,
|
||||
const char *trace_id);
|
||||
|
||||
esp_err_t rest_server_health_get(httpd_req_t *req);
|
||||
esp_err_t rest_server_connect_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_disconnect_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_status_get(httpd_req_t *req);
|
||||
|
||||
esp_err_t rest_server_print_raster_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_print_image_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_print_text_post(httpd_req_t *req);
|
||||
|
||||
esp_err_t rest_server_job_get(httpd_req_t *req);
|
||||
esp_err_t rest_server_jobs_get(httpd_req_t *req);
|
||||
|
||||
esp_err_t rest_server_voice_status_get(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_session_start_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_session_stop_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_tap_start_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_tap_cancel_post(httpd_req_t *req);
|
||||
@@ -69,25 +69,16 @@ static esp_err_t lifecycle_start_voice(void) {
|
||||
return voice_interaction_init();
|
||||
}
|
||||
|
||||
static esp_err_t lifecycle_start_rest_server(void) {
|
||||
return rest_server_start();
|
||||
}
|
||||
|
||||
static esp_err_t lifecycle_stop_rest_server(void) {
|
||||
rest_server_stop();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t lifecycle_stop_voice(void) {
|
||||
esp_err_t rc = voice_interaction_stop(NULL, 0);
|
||||
static esp_err_t lifecycle_stop_printer_protocol(void) {
|
||||
esp_err_t rc = printer_protocol_stop(domain_policy_printer_stop_timeout_ms());
|
||||
if (rc == ESP_OK || rc == ESP_ERR_INVALID_STATE) {
|
||||
return ESP_OK;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static esp_err_t lifecycle_stop_printer_protocol(void) {
|
||||
esp_err_t rc = printer_protocol_stop(domain_policy_printer_stop_timeout_ms());
|
||||
static esp_err_t lifecycle_stop_voice(void) {
|
||||
esp_err_t rc = voice_interaction_stop(NULL, 0);
|
||||
if (rc == ESP_OK || rc == ESP_ERR_INVALID_STATE) {
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -159,7 +150,6 @@ esp_err_t controller_lifecycle_start(void) {
|
||||
bool system_started = false;
|
||||
bool printer_started = false;
|
||||
bool voice_started = false;
|
||||
bool rest_started = false;
|
||||
const char *failed_stage = "unknown";
|
||||
|
||||
failed_stage = "system_runtime";
|
||||
@@ -183,13 +173,6 @@ esp_err_t controller_lifecycle_start(void) {
|
||||
}
|
||||
voice_started = true;
|
||||
|
||||
failed_stage = "rest_server";
|
||||
rc = lifecycle_run_step_with_retry("rest_server", lifecycle_start_rest_server);
|
||||
if (rc != ESP_OK) {
|
||||
goto start_failed;
|
||||
}
|
||||
rest_started = true;
|
||||
|
||||
image_generation_schedule_prewarm();
|
||||
|
||||
if (lifecycle_lock_take(1000)) {
|
||||
@@ -202,9 +185,6 @@ esp_err_t controller_lifecycle_start(void) {
|
||||
return ESP_OK;
|
||||
|
||||
start_failed:
|
||||
if (rest_started) {
|
||||
(void)lifecycle_stop_rest_server();
|
||||
}
|
||||
if (voice_started) {
|
||||
(void)lifecycle_stop_voice();
|
||||
}
|
||||
@@ -253,12 +233,7 @@ esp_err_t controller_lifecycle_stop(void) {
|
||||
|
||||
esp_err_t first_err = ESP_OK;
|
||||
|
||||
esp_err_t rc = lifecycle_stop_rest_server();
|
||||
if (rc != ESP_OK && first_err == ESP_OK) {
|
||||
first_err = rc;
|
||||
}
|
||||
|
||||
rc = lifecycle_stop_voice();
|
||||
esp_err_t rc = lifecycle_stop_voice();
|
||||
if (rc != ESP_OK && first_err == ESP_OK) {
|
||||
first_err = rc;
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
#include "control_plane.h"
|
||||
|
||||
#include "rest_server_internal.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "rest_server";
|
||||
|
||||
static httpd_handle_t s_server;
|
||||
|
||||
static const char *s_web_index =
|
||||
"<!doctype html>\n"
|
||||
"<html lang=\"zh-CN\">\n"
|
||||
"<head>\n"
|
||||
" <meta charset=\"utf-8\">\n"
|
||||
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
|
||||
" <title>TQ Voice Console</title>\n"
|
||||
" <style>\n"
|
||||
" :root { color-scheme: light; }\n"
|
||||
" body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; margin: 16px; background:#f7f8fb; color:#1b2430; }\n"
|
||||
" .box { max-width: 760px; margin: 0 auto; background:#fff; border:1px solid #dfe6ee; border-radius:12px; padding:16px; }\n"
|
||||
" h1 { margin: 0 0 8px 0; font-size: 22px; }\n"
|
||||
" p { margin: 6px 0 12px 0; color:#4b5d73; }\n"
|
||||
" .hint { font-size: 13px; color:#29583f; background:#e7f4ec; border:1px solid #cbe4d5; border-radius:8px; padding:10px; margin: 8px 0 12px 0; }\n"
|
||||
" .small { font-size: 13px; color:#44566c; }\n"
|
||||
" </style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
" <div class=\"box\">\n"
|
||||
" <h1>TQ Voice Console</h1>\n"
|
||||
" <p>ESP32-S3 本地语音控制页。已切换为 MIC_KEY(IO14)触发单轮语音对话。</p>\n"
|
||||
"\n"
|
||||
" <div class=\"hint\">按住机身 MIC_KEY 说话,松开后自动进入“语音交互→生图→显示→处理→打印”。</div>\n"
|
||||
" <div id=\"voiceState\" class=\"small\">语音状态:初始化中</div>\n"
|
||||
" </div>\n"
|
||||
"\n"
|
||||
" <script>\n"
|
||||
" const voiceStateEl = document.getElementById(\"voiceState\");\n"
|
||||
"\n"
|
||||
" let voiceStatusSyncing = false;\n"
|
||||
"\n"
|
||||
" function headers(withJson) {\n"
|
||||
" const h = {};\n"
|
||||
" if (withJson) h[\"Content-Type\"] = \"application/json\";\n"
|
||||
" return h;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function callApi(path, method, body) {\n"
|
||||
" const init = { method, headers: headers(body !== undefined) };\n"
|
||||
" if (body !== undefined) init.body = JSON.stringify(body);\n"
|
||||
" const r = await fetch(path, init);\n"
|
||||
" const text = await r.text();\n"
|
||||
" let data = text;\n"
|
||||
" try { data = JSON.parse(text); } catch (_) {}\n"
|
||||
" if (!r.ok) {\n"
|
||||
" throw { status: r.status, data };\n"
|
||||
" }\n"
|
||||
" return data;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceStatusText(st) {\n"
|
||||
" if (!st) return \"语音状态:未知\";\n"
|
||||
" const state = (st.voice_dialog_state || \"\").toLowerCase();\n"
|
||||
" const queueDepth = Number(st.queue_depth || 0);\n"
|
||||
" if (!st.voice_session_active) return \"语音状态:会话未启动\";\n"
|
||||
" if (!st.voice_ws_connected) return \"语音状态:会话断开,按 MIC_KEY 将自动重建\";\n"
|
||||
" if (st.voice_tap_active) return \"语音状态:录音中\";\n"
|
||||
" if (state === \"thinking\" || state === \"responding\") return \"语音状态:语音处理中\";\n"
|
||||
" if (!!st.voice_marker_image_gen_running) return \"语音状态:图片生成中\";\n"
|
||||
" if (!!st.printer_busy || queueDepth > 0) return \"语音状态:打印处理中\";\n"
|
||||
" return \"语音状态:空闲,等待 MIC_KEY 触发\";\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceErrorText(st) {\n"
|
||||
" if (!st || st.voice_last_error === undefined || st.voice_last_error === null) return \"\";\n"
|
||||
" const msg = String(st.voice_last_error).trim();\n"
|
||||
" if (!msg || msg === \"-\") return \"\";\n"
|
||||
" return \"最近错误:\" + msg;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function refreshVoiceStatus() {\n"
|
||||
" if (voiceStatusSyncing) return null;\n"
|
||||
" voiceStatusSyncing = true;\n"
|
||||
" try {\n"
|
||||
" const st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" let status = voiceStatusText(st);\n"
|
||||
" const err = voiceErrorText(st);\n"
|
||||
" if (err) status += \";\" + err;\n"
|
||||
" if (voiceStateEl) voiceStateEl.textContent = status;\n"
|
||||
" return st;\n"
|
||||
" } finally {\n"
|
||||
" voiceStatusSyncing = false;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" (async () => {\n"
|
||||
" try { await refreshVoiceStatus(); } catch (_) {}\n"
|
||||
" })();\n"
|
||||
"\n"
|
||||
" setInterval(() => {\n"
|
||||
" refreshVoiceStatus().catch(() => {\n"
|
||||
" if (voiceStateEl) voiceStateEl.textContent = \"语音状态:状态获取失败\";\n"
|
||||
" });\n"
|
||||
" }, 1500);\n"
|
||||
" </script>\n"
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
;
|
||||
|
||||
static esp_err_t index_get(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
return httpd_resp_send(req, s_web_index, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
static esp_err_t favicon_get(httpd_req_t *req) {
|
||||
httpd_resp_set_status(req, "204 No Content");
|
||||
return httpd_resp_send(req, NULL, 0);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_start(void) {
|
||||
if (s_server != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = CONFIG_TQ_HTTP_PORT;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.max_uri_handlers = 20;
|
||||
config.stack_size = 10240;
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
config.core_id = 0;
|
||||
#else
|
||||
config.core_id = 1;
|
||||
#endif
|
||||
|
||||
esp_err_t err = httpd_start(&s_server, &config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "httpd_start failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_uri_t index = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = index_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &index);
|
||||
|
||||
httpd_uri_t favicon = {
|
||||
.uri = "/favicon.ico",
|
||||
.method = HTTP_GET,
|
||||
.handler = favicon_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &favicon);
|
||||
|
||||
httpd_uri_t health = {
|
||||
.uri = "/v1/health",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_health_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &health);
|
||||
|
||||
httpd_uri_t connect = {
|
||||
.uri = "/v1/printer/connect",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_connect_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &connect);
|
||||
|
||||
httpd_uri_t disconnect = {
|
||||
.uri = "/v1/printer/disconnect",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_disconnect_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &disconnect);
|
||||
|
||||
httpd_uri_t status = {
|
||||
.uri = "/v1/printer/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_status_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &status);
|
||||
|
||||
httpd_uri_t voice_status = {
|
||||
.uri = "/v1/voice/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_voice_status_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_status);
|
||||
|
||||
httpd_uri_t voice_session_start = {
|
||||
.uri = "/v1/voice/session/start",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_session_start_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_session_start);
|
||||
|
||||
httpd_uri_t voice_session_stop = {
|
||||
.uri = "/v1/voice/session/stop",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_session_stop_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_session_stop);
|
||||
|
||||
httpd_uri_t voice_tap_start = {
|
||||
.uri = "/v1/voice/tap/start",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_tap_start_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_tap_start);
|
||||
|
||||
httpd_uri_t voice_tap_cancel = {
|
||||
.uri = "/v1/voice/tap/cancel",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_tap_cancel_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_tap_cancel);
|
||||
|
||||
httpd_uri_t print_raster = {
|
||||
.uri = "/v1/print/raster",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_print_raster_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &print_raster);
|
||||
|
||||
httpd_uri_t print_image = {
|
||||
.uri = "/v1/print/image",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_print_image_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &print_image);
|
||||
|
||||
httpd_uri_t print_text = {
|
||||
.uri = "/v1/print/text",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_print_text_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &print_text);
|
||||
|
||||
httpd_uri_t jobs = {
|
||||
.uri = "/v1/jobs",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_jobs_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &jobs);
|
||||
|
||||
httpd_uri_t job = {
|
||||
.uri = "/v1/jobs/*",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_job_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &job);
|
||||
|
||||
ESP_LOGI(TAG, "REST server started on port %d", CONFIG_TQ_HTTP_PORT);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void rest_server_stop(void) {
|
||||
if (s_server != NULL) {
|
||||
httpd_stop(s_server);
|
||||
s_server = NULL;
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "domain.h"
|
||||
|
||||
#define REST_SERVER_MAX_BODY_BYTES (4 * 1024 * 1024)
|
||||
|
||||
static void *alloc_prefer_psram(size_t size) {
|
||||
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (ptr == NULL) {
|
||||
ptr = malloc(size);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void *calloc_prefer_psram(size_t n, size_t size) {
|
||||
void *ptr = heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (ptr == NULL) {
|
||||
ptr = calloc(n, size);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
bool rest_server_auth_ok(httpd_req_t *req) {
|
||||
if (strlen(CONFIG_TQ_API_KEY) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
char buf[96] = {0};
|
||||
if (httpd_req_get_hdr_value_str(req, "X-API-Key", buf, sizeof(buf)) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strcmp(buf, CONFIG_TQ_API_KEY) == 0;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root) {
|
||||
char *text = cJSON_PrintUnformatted(root);
|
||||
if (text == NULL) {
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "json encode failed");
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_status(req, status);
|
||||
esp_err_t err = httpd_resp_sendstr(req, text);
|
||||
cJSON_free(text);
|
||||
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_REST_RESPONSES_TOTAL, 1);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_send_error(httpd_req_t *req, const char *status, const char *message) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", false);
|
||||
cJSON_AddStringToObject(root, "error", message != NULL ? message : "unknown");
|
||||
esp_err_t err = rest_server_send_json(req, status, root);
|
||||
cJSON_Delete(root);
|
||||
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_REST_ERRORS_TOTAL, 1);
|
||||
domain_diag_record_error("rest_api", ESP_FAIL, message != NULL ? message : "unknown");
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_read_body(httpd_req_t *req, char **out_body) {
|
||||
if (out_body == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*out_body = NULL;
|
||||
|
||||
if (req->content_len <= 0 || req->content_len > REST_SERVER_MAX_BODY_BYTES) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
char *buf = (char *)calloc_prefer_psram(1, (size_t)req->content_len + 1);
|
||||
if (buf == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
int received = 0;
|
||||
while (received < req->content_len) {
|
||||
int ret = httpd_req_recv(req, buf + received, req->content_len - received);
|
||||
if (ret <= 0) {
|
||||
free(buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received += ret;
|
||||
}
|
||||
|
||||
*out_body = buf;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool rest_server_parse_uri_u32_tail(const char *uri, uint32_t *out_value) {
|
||||
if (uri == NULL || out_value == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *slash = strrchr(uri, '/');
|
||||
if (slash == NULL || *(slash + 1) == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
char *endptr = NULL;
|
||||
unsigned long val = strtoul(slash + 1, &endptr, 10);
|
||||
if (errno != 0 || endptr == slash + 1 || *endptr != '\0' || val > UINT32_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_value = (uint32_t)val;
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, size_t *out_len) {
|
||||
if (b64 == NULL || out_raw == NULL || out_len == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*out_raw = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
size_t decoded_len = 0;
|
||||
int rc = mbedtls_base64_decode(NULL,
|
||||
0,
|
||||
&decoded_len,
|
||||
(const unsigned char *)b64,
|
||||
strlen(b64));
|
||||
if (!(rc == 0 || rc == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)alloc_prefer_psram(decoded_len);
|
||||
if (buf == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
rc = mbedtls_base64_decode(buf,
|
||||
decoded_len,
|
||||
&decoded_len,
|
||||
(const unsigned char *)b64,
|
||||
strlen(b64));
|
||||
if (rc != 0) {
|
||||
free(buf);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*out_raw = buf;
|
||||
*out_len = decoded_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool rest_server_json_bool_with_default(cJSON *item, bool default_value) {
|
||||
if (item == NULL) {
|
||||
return default_value;
|
||||
}
|
||||
if (cJSON_IsBool(item)) {
|
||||
return cJSON_IsTrue(item);
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include "domain.h"
|
||||
|
||||
esp_err_t rest_server_job_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
uint32_t job_id = 0;
|
||||
if (!rest_server_parse_uri_u32_tail(req->uri, &job_id)) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid job id");
|
||||
}
|
||||
|
||||
print_job_info_t info;
|
||||
if (!printer_protocol_get_job(job_id, &info)) {
|
||||
return rest_server_send_error(req, "404 Not Found", "job not found");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddNumberToObject(root, "job_id", info.id);
|
||||
cJSON_AddStringToObject(root, "state", printer_protocol_job_state_str(info.state));
|
||||
cJSON_AddNumberToObject(root, "progress", info.progress);
|
||||
cJSON_AddStringToObject(root, "density", info.density);
|
||||
cJSON_AddNumberToObject(root, "width", info.width);
|
||||
cJSON_AddNumberToObject(root, "height", info.height);
|
||||
cJSON_AddNumberToObject(root, "data_len", (double)info.data_len);
|
||||
cJSON_AddStringToObject(root, "error", info.error);
|
||||
cJSON_AddNumberToObject(root, "created_ms", (double)info.created_ms);
|
||||
cJSON_AddNumberToObject(root, "started_ms", (double)info.started_ms);
|
||||
cJSON_AddNumberToObject(root, "finished_ms", (double)info.finished_ms);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_jobs_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
print_job_info_t jobs[16];
|
||||
size_t total_count = 0;
|
||||
esp_err_t list_rc = printer_protocol_list_jobs(jobs, 16, &total_count);
|
||||
if (list_rc != ESP_OK) {
|
||||
return rest_server_send_error(req, "500 Internal Server Error", "list jobs failed");
|
||||
}
|
||||
|
||||
size_t emit_count = total_count;
|
||||
if (emit_count > 16) {
|
||||
emit_count = 16;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddNumberToObject(root, "total", (double)total_count);
|
||||
cJSON_AddNumberToObject(root, "returned", (double)emit_count);
|
||||
|
||||
cJSON *arr = cJSON_AddArrayToObject(root, "jobs");
|
||||
for (size_t i = 0; i < emit_count; ++i) {
|
||||
cJSON *item = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(item, "job_id", jobs[i].id);
|
||||
cJSON_AddStringToObject(item, "state", printer_protocol_job_state_str(jobs[i].state));
|
||||
cJSON_AddNumberToObject(item, "progress", jobs[i].progress);
|
||||
cJSON_AddStringToObject(item, "density", jobs[i].density);
|
||||
cJSON_AddNumberToObject(item, "width", jobs[i].width);
|
||||
cJSON_AddNumberToObject(item, "height", jobs[i].height);
|
||||
cJSON_AddNumberToObject(item, "data_len", (double)jobs[i].data_len);
|
||||
cJSON_AddStringToObject(item, "error", jobs[i].error);
|
||||
cJSON_AddNumberToObject(item, "created_ms", (double)jobs[i].created_ms);
|
||||
cJSON_AddNumberToObject(item, "started_ms", (double)jobs[i].started_ms);
|
||||
cJSON_AddNumberToObject(item, "finished_ms", (double)jobs[i].finished_ms);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
}
|
||||
|
||||
if (total_count > emit_count) {
|
||||
cJSON_AddStringToObject(root, "warning", "job list truncated");
|
||||
}
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "control_plane.h"
|
||||
#include "domain.h"
|
||||
|
||||
static const char *printer_backend_str(printer_backend_t backend) {
|
||||
switch (backend) {
|
||||
case PRINTER_BACKEND_DIRECT:
|
||||
return "direct";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static bool parse_printer_backend(const cJSON *json, printer_backend_t *out_backend) {
|
||||
if (json == NULL || out_backend == NULL) {
|
||||
return false;
|
||||
}
|
||||
cJSON *jbackend = cJSON_GetObjectItemCaseSensitive((cJSON *)json, "backend");
|
||||
if (!cJSON_IsString(jbackend) || jbackend->valuestring == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (strcasecmp(jbackend->valuestring, "direct") == 0) {
|
||||
*out_backend = PRINTER_BACKEND_DIRECT;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void fill_runtime_diag_json(cJSON *root) {
|
||||
domain_diag_snapshot_t snapshot = {0};
|
||||
domain_diag_get_snapshot(&snapshot);
|
||||
|
||||
cJSON *diag = cJSON_AddObjectToObject(root, "diagnostics");
|
||||
if (diag == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON *counters = cJSON_AddObjectToObject(diag, "counters");
|
||||
if (counters != NULL) {
|
||||
for (int i = 0; i < DOMAIN_DIAG_COUNTER_MAX; ++i) {
|
||||
cJSON_AddNumberToObject(counters,
|
||||
domain_diag_counter_name((domain_diag_counter_t)i),
|
||||
(double)snapshot.counters[i]);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *gauges = cJSON_AddObjectToObject(diag, "gauges");
|
||||
if (gauges != NULL) {
|
||||
for (int i = 0; i < DOMAIN_DIAG_GAUGE_MAX; ++i) {
|
||||
cJSON_AddNumberToObject(gauges,
|
||||
domain_diag_gauge_name((domain_diag_gauge_t)i),
|
||||
snapshot.gauges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *last_error = cJSON_AddObjectToObject(diag, "last_error");
|
||||
if (last_error != NULL) {
|
||||
cJSON_AddNumberToObject(last_error, "ts_ms", (double)snapshot.last_error_ms);
|
||||
cJSON_AddNumberToObject(last_error, "code", (double)snapshot.last_error_code);
|
||||
cJSON_AddStringToObject(last_error, "source", snapshot.last_error_source);
|
||||
cJSON_AddStringToObject(last_error, "message", snapshot.last_error_message);
|
||||
}
|
||||
}
|
||||
|
||||
static void fill_runtime_json(cJSON *root) {
|
||||
char ip[32] = {0};
|
||||
system_runtime_get_ip(ip, sizeof(ip));
|
||||
|
||||
printer_runtime_status_t st = {0};
|
||||
printer_protocol_get_runtime_status(&st);
|
||||
|
||||
cJSON_AddBoolToObject(root, "wifi_ready", system_runtime_wifi_ready());
|
||||
cJSON_AddStringToObject(root, "wifi_mode", "sta");
|
||||
cJSON_AddStringToObject(root, "ip", ip);
|
||||
|
||||
cJSON_AddStringToObject(root, "printer_backend", printer_backend_str(st.backend));
|
||||
cJSON_AddBoolToObject(root, "printer_connected", st.connected);
|
||||
cJSON_AddBoolToObject(root, "printer_transport_ready", st.transport_ready);
|
||||
cJSON_AddBoolToObject(root, "printer_busy", st.busy);
|
||||
cJSON_AddBoolToObject(root, "has_paper", st.has_paper);
|
||||
cJSON_AddNumberToObject(root, "paper_gpio_level", st.paper_gpio_level);
|
||||
cJSON_AddNumberToObject(root, "paper_present_level", st.paper_present_level);
|
||||
cJSON_AddNumberToObject(root, "battery_percent", st.battery_percent);
|
||||
cJSON_AddNumberToObject(root, "temperature", st.temperature);
|
||||
cJSON_AddBoolToObject(root, "supports_gap_move", st.supports_gap_move);
|
||||
cJSON_AddBoolToObject(root, "supports_label_offset", st.supports_label_offset);
|
||||
cJSON_AddBoolToObject(root, "supports_ota", st.supports_ota);
|
||||
cJSON_AddNumberToObject(root, "queue_depth", st.queue_depth);
|
||||
cJSON_AddNumberToObject(root, "last_status_ms", (double)st.last_status_ms);
|
||||
|
||||
voice_interaction_status_t voice = {0};
|
||||
voice_interaction_get_status(&voice);
|
||||
cJSON_AddBoolToObject(root, "voice_ready", voice.ready);
|
||||
cJSON_AddBoolToObject(root, "voice_session_active", voice.session_active);
|
||||
cJSON_AddBoolToObject(root, "voice_ws_connected", voice.ws_connected);
|
||||
cJSON_AddBoolToObject(root, "voice_started", voice.started);
|
||||
cJSON_AddBoolToObject(root, "voice_tap_active", voice.tap_active);
|
||||
cJSON_AddStringToObject(root,
|
||||
"voice_dialog_state",
|
||||
voice_interaction_dialog_state_str(voice.dialog_state));
|
||||
cJSON_AddStringToObject(root, "voice_task_id", voice.task_id);
|
||||
cJSON_AddStringToObject(root, "voice_dialog_id", voice.dialog_id);
|
||||
cJSON_AddStringToObject(root, "voice_last_error", voice.last_error);
|
||||
cJSON_AddNumberToObject(root, "voice_last_event_ms", (double)voice.last_event_ms);
|
||||
cJSON_AddNumberToObject(root, "voice_upstream_packets", (double)voice.upstream_packets);
|
||||
cJSON_AddNumberToObject(root, "voice_downstream_packets", (double)voice.downstream_packets);
|
||||
|
||||
controller_lifecycle_status_t lifecycle = {0};
|
||||
controller_lifecycle_get_status(&lifecycle);
|
||||
cJSON_AddStringToObject(root, "lifecycle_state", controller_lifecycle_state_str(lifecycle.state));
|
||||
cJSON_AddStringToObject(root, "lifecycle_stage", lifecycle.last_stage);
|
||||
cJSON_AddNumberToObject(root, "lifecycle_last_error", (double)lifecycle.last_error);
|
||||
cJSON_AddNumberToObject(root, "lifecycle_start_attempt", (double)lifecycle.start_attempt);
|
||||
cJSON_AddNumberToObject(root, "lifecycle_last_transition_ms", (double)lifecycle.last_transition_ms);
|
||||
|
||||
fill_runtime_diag_json(root);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_health_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
controller_lifecycle_status_t lifecycle = {0};
|
||||
controller_lifecycle_get_status(&lifecycle);
|
||||
bool healthy = (lifecycle.state == CONTROLLER_LIFECYCLE_STATE_RUNNING) && system_runtime_wifi_ready();
|
||||
cJSON_AddBoolToObject(root, "ok", healthy);
|
||||
fill_runtime_json(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, healthy ? "200 OK" : "503 Service Unavailable", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_connect_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
char *body = NULL;
|
||||
uint32_t timeout_ms = domain_policy_rest_printer_connect_timeout_ms();
|
||||
printer_backend_t backend = printer_protocol_get_backend();
|
||||
bool backend_specified = false;
|
||||
|
||||
if (req->content_len > 0) {
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
cJSON *json = cJSON_Parse(body);
|
||||
if (json == NULL) {
|
||||
free(body);
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid json");
|
||||
}
|
||||
|
||||
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
|
||||
if (cJSON_IsNumber(jtimeout)) {
|
||||
uint32_t min_ms = domain_policy_rest_printer_connect_timeout_min_ms();
|
||||
uint32_t max_ms = domain_policy_rest_printer_connect_timeout_max_ms();
|
||||
if (jtimeout->valuedouble >= (double)min_ms && jtimeout->valuedouble <= (double)max_ms) {
|
||||
timeout_ms = (uint32_t)jtimeout->valuedouble;
|
||||
} else {
|
||||
cJSON_Delete(json);
|
||||
free(body);
|
||||
return rest_server_send_error(req, "400 Bad Request", "timeout_ms out of range");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *jbackend = cJSON_GetObjectItemCaseSensitive(json, "backend");
|
||||
if (jbackend != NULL) {
|
||||
backend_specified = true;
|
||||
if (!parse_printer_backend(json, &backend)) {
|
||||
cJSON_Delete(json);
|
||||
free(body);
|
||||
return rest_server_send_error(req, "400 Bad Request", "backend must be direct");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
free(body);
|
||||
}
|
||||
|
||||
printer_connect_options_t opt = {
|
||||
.backend = backend,
|
||||
.name = NULL,
|
||||
.timeout_ms = timeout_ms,
|
||||
};
|
||||
char connect_err[96] = {0};
|
||||
esp_err_t err = printer_protocol_connect_ex(&opt, connect_err, sizeof(connect_err));
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_TIMEOUT) {
|
||||
return rest_server_send_error(req,
|
||||
"504 Gateway Timeout",
|
||||
connect_err[0] != 0 ? connect_err : "connect timeout");
|
||||
}
|
||||
if (err == ESP_ERR_NOT_SUPPORTED || err == ESP_ERR_INVALID_STATE) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
connect_err[0] != 0 ? connect_err : "connect failed");
|
||||
}
|
||||
if (err == ESP_ERR_INVALID_ARG) {
|
||||
return rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
connect_err[0] != 0 ? connect_err : "invalid connect params");
|
||||
}
|
||||
return rest_server_send_error(req,
|
||||
"500 Internal Server Error",
|
||||
connect_err[0] != 0 ? connect_err : "connect failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "connected");
|
||||
cJSON_AddStringToObject(root, "backend", printer_backend_str(backend));
|
||||
cJSON_AddBoolToObject(root, "backend_specified", backend_specified);
|
||||
fill_runtime_json(root);
|
||||
|
||||
err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_disconnect_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
printer_protocol_disconnect();
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "disconnected");
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_status_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
fill_runtime_json(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "domain.h"
|
||||
|
||||
static const char *TAG = "rest_print";
|
||||
|
||||
static const char *trace_id_or_default(const char *trace_id) {
|
||||
return (trace_id != NULL && trace_id[0] != '\0') ? trace_id : "-";
|
||||
}
|
||||
|
||||
esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req,
|
||||
const uint8_t *raw,
|
||||
size_t raw_len,
|
||||
uint16_t width,
|
||||
uint16_t height,
|
||||
const char *density,
|
||||
bool direct_ignore_precheck,
|
||||
const char *warning,
|
||||
const char *trace_id) {
|
||||
const char *trace = trace_id_or_default(trace_id);
|
||||
ESP_LOGI(TAG,
|
||||
"print submit start, trace=%s, raster_bytes=%u, size=%ux%u, density=%s, ignore_precheck=%d",
|
||||
trace,
|
||||
(unsigned)raw_len,
|
||||
(unsigned)width,
|
||||
(unsigned)height,
|
||||
density != NULL ? density : "中等",
|
||||
direct_ignore_precheck);
|
||||
|
||||
char submit_err[128] = {0};
|
||||
uint32_t job_id = 0;
|
||||
printer_print_options_t options = {
|
||||
.direct_ignore_precheck = direct_ignore_precheck,
|
||||
};
|
||||
esp_err_t submit_rc = printer_protocol_submit_raster_job_ex(raw,
|
||||
raw_len,
|
||||
width,
|
||||
height,
|
||||
density,
|
||||
&options,
|
||||
&job_id,
|
||||
submit_err,
|
||||
sizeof(submit_err));
|
||||
if (submit_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"print submit failed, trace=%s, rc=0x%x, msg=%s",
|
||||
trace,
|
||||
(unsigned)submit_rc,
|
||||
submit_err[0] != '\0' ? submit_err : "submit failed");
|
||||
return rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
submit_err[0] != '\0' ? submit_err : "submit failed");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "print submit done, trace=%s, job_id=%u", trace, (unsigned)job_id);
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddNumberToObject(root, "job_id", job_id);
|
||||
cJSON_AddStringToObject(root, "state", "queued");
|
||||
cJSON_AddBoolToObject(root, "ignore_precheck", direct_ignore_precheck);
|
||||
if (warning != NULL && warning[0] != '\0') {
|
||||
cJSON_AddStringToObject(root, "warning", warning);
|
||||
}
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "202 Accepted", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_print_raster_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
cJSON *json = cJSON_Parse(body);
|
||||
free(body);
|
||||
if (json == NULL) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid json");
|
||||
}
|
||||
|
||||
cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width");
|
||||
cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height");
|
||||
cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding");
|
||||
cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data");
|
||||
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
|
||||
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
|
||||
|
||||
if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) ||
|
||||
!cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) {
|
||||
cJSON_Delete(json);
|
||||
return rest_server_send_error(req, "400 Bad Request", "missing required fields");
|
||||
}
|
||||
|
||||
if (strcmp(jencoding->valuestring, "base64_msb_1bpp") != 0) {
|
||||
cJSON_Delete(json);
|
||||
return rest_server_send_error(req, "400 Bad Request", "unsupported encoding");
|
||||
}
|
||||
|
||||
const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL)
|
||||
? jdensity->valuestring
|
||||
: "中等";
|
||||
bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false);
|
||||
|
||||
uint8_t *raw = NULL;
|
||||
size_t raw_len = 0;
|
||||
esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &raw, &raw_len);
|
||||
if (b64_rc != ESP_OK) {
|
||||
cJSON_Delete(json);
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid base64");
|
||||
}
|
||||
|
||||
esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req,
|
||||
raw,
|
||||
raw_len,
|
||||
(uint16_t)jwidth->valuedouble,
|
||||
(uint16_t)jheight->valuedouble,
|
||||
density,
|
||||
direct_ignore_precheck,
|
||||
NULL,
|
||||
NULL);
|
||||
free(raw);
|
||||
cJSON_Delete(json);
|
||||
return submit_rc;
|
||||
}
|
||||
@@ -1,661 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "domain.h"
|
||||
|
||||
static const char *TAG = "rest_print";
|
||||
#define SCREEN_PREVIEW_TIMEOUT_MS 200
|
||||
|
||||
typedef struct {
|
||||
bool scale_to_width;
|
||||
uint8_t threshold;
|
||||
bool invert;
|
||||
uint16_t max_height;
|
||||
const char *density;
|
||||
bool direct_ignore_precheck;
|
||||
} image_print_options_t;
|
||||
|
||||
typedef struct {
|
||||
char prompt[801];
|
||||
char size[24];
|
||||
bool has_size;
|
||||
bool prompt_extend;
|
||||
bool has_seed;
|
||||
uint32_t seed;
|
||||
uint32_t timeout_ms;
|
||||
uint32_t fetch_timeout_ms;
|
||||
} image_generate_options_t;
|
||||
|
||||
static bool parse_bool_text(const char *text, bool *out_value) {
|
||||
if (text == NULL || out_value == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(text, "1") == 0 || strcasecmp(text, "true") == 0 || strcasecmp(text, "yes") == 0) {
|
||||
*out_value = true;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(text, "0") == 0 || strcasecmp(text, "false") == 0 || strcasecmp(text, "no") == 0) {
|
||||
*out_value = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *map_density_text(const char *text) {
|
||||
if (text == NULL || text[0] == '\0') {
|
||||
return "中等";
|
||||
}
|
||||
if (strcasecmp(text, "light") == 0 || strcmp(text, "较淡") == 0) {
|
||||
return "较淡";
|
||||
}
|
||||
if (strcasecmp(text, "medium") == 0 || strcmp(text, "中等") == 0) {
|
||||
return "中等";
|
||||
}
|
||||
if (strcasecmp(text, "dark") == 0 || strcmp(text, "较浓") == 0) {
|
||||
return "较浓";
|
||||
}
|
||||
if (strcasecmp(text, "max") == 0 || strcmp(text, "最深") == 0) {
|
||||
return "最深";
|
||||
}
|
||||
return "中等";
|
||||
}
|
||||
|
||||
static void parse_image_upload_options(httpd_req_t *req,
|
||||
bool *out_scale_to_width,
|
||||
uint8_t *out_threshold,
|
||||
bool *out_invert,
|
||||
uint16_t *out_max_height,
|
||||
const char **out_density,
|
||||
bool *out_direct_ignore_precheck) {
|
||||
if (out_scale_to_width != NULL) {
|
||||
*out_scale_to_width = true;
|
||||
}
|
||||
if (out_threshold != NULL) {
|
||||
*out_threshold = 160;
|
||||
}
|
||||
if (out_invert != NULL) {
|
||||
*out_invert = false;
|
||||
}
|
||||
if (out_max_height != NULL) {
|
||||
*out_max_height = 2200;
|
||||
}
|
||||
if (out_density != NULL) {
|
||||
*out_density = "中等";
|
||||
}
|
||||
if (out_direct_ignore_precheck != NULL) {
|
||||
*out_direct_ignore_precheck = false;
|
||||
}
|
||||
|
||||
if (req == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
int qlen = httpd_req_get_url_query_len(req);
|
||||
if (qlen <= 0 || qlen > 512) {
|
||||
return;
|
||||
}
|
||||
|
||||
char *query = (char *)calloc(1, (size_t)qlen + 1);
|
||||
if (query == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpd_req_get_url_query_str(req, query, (size_t)qlen + 1) != ESP_OK) {
|
||||
free(query);
|
||||
return;
|
||||
}
|
||||
|
||||
char value[48] = {0};
|
||||
if (httpd_query_key_value(query, "scale_to_width", value, sizeof(value)) == ESP_OK &&
|
||||
out_scale_to_width != NULL) {
|
||||
bool parsed = *out_scale_to_width;
|
||||
if (parse_bool_text(value, &parsed)) {
|
||||
*out_scale_to_width = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query, "invert", value, sizeof(value)) == ESP_OK && out_invert != NULL) {
|
||||
bool parsed = *out_invert;
|
||||
if (parse_bool_text(value, &parsed)) {
|
||||
*out_invert = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query, "threshold", value, sizeof(value)) == ESP_OK && out_threshold != NULL) {
|
||||
long v = strtol(value, NULL, 10);
|
||||
if (v >= 0 && v <= 255) {
|
||||
*out_threshold = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query, "max_height", value, sizeof(value)) == ESP_OK && out_max_height != NULL) {
|
||||
long v = strtol(value, NULL, 10);
|
||||
if (v >= 64 && v <= 3000) {
|
||||
*out_max_height = (uint16_t)v;
|
||||
}
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query, "density", value, sizeof(value)) == ESP_OK && out_density != NULL) {
|
||||
*out_density = map_density_text(value);
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query, "ignore_precheck", value, sizeof(value)) == ESP_OK &&
|
||||
out_direct_ignore_precheck != NULL) {
|
||||
bool parsed = *out_direct_ignore_precheck;
|
||||
if (parse_bool_text(value, &parsed)) {
|
||||
*out_direct_ignore_precheck = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
free(query);
|
||||
}
|
||||
|
||||
static void image_print_options_set_defaults(image_print_options_t *out) {
|
||||
if (out == NULL) {
|
||||
return;
|
||||
}
|
||||
out->scale_to_width = true;
|
||||
out->threshold = 160;
|
||||
out->invert = false;
|
||||
out->max_height = 2200;
|
||||
out->density = "中等";
|
||||
out->direct_ignore_precheck = false;
|
||||
}
|
||||
|
||||
static void parse_image_print_options_from_json(cJSON *json, image_print_options_t *out) {
|
||||
if (json == NULL || out == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width");
|
||||
cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold");
|
||||
cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert");
|
||||
cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height");
|
||||
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
|
||||
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
|
||||
|
||||
out->scale_to_width = rest_server_json_bool_with_default(jscale, out->scale_to_width);
|
||||
out->invert = rest_server_json_bool_with_default(jinvert, out->invert);
|
||||
if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) {
|
||||
out->threshold = (uint8_t)jthreshold->valuedouble;
|
||||
}
|
||||
if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) {
|
||||
out->max_height = (uint16_t)jmaxh->valuedouble;
|
||||
}
|
||||
if (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) {
|
||||
out->density = map_density_text(jdensity->valuestring);
|
||||
}
|
||||
out->direct_ignore_precheck = rest_server_json_bool_with_default(jignore, out->direct_ignore_precheck);
|
||||
}
|
||||
|
||||
static bool parse_image_size_text(const char *text, uint32_t *out_w, uint32_t *out_h) {
|
||||
if (text == NULL || out_w == NULL || out_h == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *sep = strchr(text, '*');
|
||||
if (sep == NULL || sep == text || *(sep + 1) == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
char wbuf[12] = {0};
|
||||
char hbuf[12] = {0};
|
||||
size_t wlen = (size_t)(sep - text);
|
||||
size_t hlen = strlen(sep + 1);
|
||||
if (wlen == 0 || hlen == 0 || wlen >= sizeof(wbuf) || hlen >= sizeof(hbuf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(wbuf, text, wlen);
|
||||
memcpy(hbuf, sep + 1, hlen);
|
||||
wbuf[wlen] = '\0';
|
||||
hbuf[hlen] = '\0';
|
||||
|
||||
char *wend = NULL;
|
||||
char *hend = NULL;
|
||||
long w = strtol(wbuf, &wend, 10);
|
||||
long h = strtol(hbuf, &hend, 10);
|
||||
if (wend == wbuf || hend == hbuf || *wend != '\0' || *hend != '\0') {
|
||||
return false;
|
||||
}
|
||||
if (w <= 0 || h <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_w = (uint32_t)w;
|
||||
*out_h = (uint32_t)h;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_image_generation_options(cJSON *json,
|
||||
image_generate_options_t *out,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (json == NULL || out == NULL) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "invalid args");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->timeout_ms = domain_policy_image_generation_timeout_default_ms();
|
||||
out->fetch_timeout_ms = domain_policy_image_download_timeout_default_ms();
|
||||
|
||||
cJSON *jprompt = cJSON_GetObjectItemCaseSensitive(json, "prompt");
|
||||
if (!cJSON_IsString(jprompt) || jprompt->valuestring == NULL || jprompt->valuestring[0] == '\0') {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "prompt is required");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t prompt_len = strlen(jprompt->valuestring);
|
||||
if (prompt_len > 800) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "prompt too long, max 800 chars");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
strlcpy(out->prompt, jprompt->valuestring, sizeof(out->prompt));
|
||||
|
||||
cJSON *jsize = cJSON_GetObjectItemCaseSensitive(json, "size");
|
||||
if (cJSON_IsString(jsize) && jsize->valuestring != NULL && jsize->valuestring[0] != '\0') {
|
||||
uint32_t w = 0;
|
||||
uint32_t h = 0;
|
||||
if (!parse_image_size_text(jsize->valuestring, &w, &h)) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "size must be like 1120*1440");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t pixels = (uint64_t)w * (uint64_t)h;
|
||||
if (pixels < (512ULL * 512ULL) || pixels > (2048ULL * 2048ULL)) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "size pixels must be in [512*512, 2048*2048]");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
out->has_size = true;
|
||||
strlcpy(out->size, jsize->valuestring, sizeof(out->size));
|
||||
}
|
||||
|
||||
cJSON *jextend = cJSON_GetObjectItemCaseSensitive(json, "prompt_extend");
|
||||
out->prompt_extend = rest_server_json_bool_with_default(jextend, false);
|
||||
|
||||
cJSON *jseed = cJSON_GetObjectItemCaseSensitive(json, "seed");
|
||||
if (cJSON_IsNumber(jseed)) {
|
||||
if (jseed->valuedouble < 0 || jseed->valuedouble > 2147483647.0) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "seed must be in [0, 2147483647]");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out->has_seed = true;
|
||||
out->seed = (uint32_t)jseed->valuedouble;
|
||||
}
|
||||
|
||||
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
|
||||
if (cJSON_IsNumber(jtimeout)) {
|
||||
if (jtimeout->valuedouble < domain_policy_image_generation_timeout_min_ms() ||
|
||||
jtimeout->valuedouble > domain_policy_image_generation_timeout_max_ms()) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err,
|
||||
err_len,
|
||||
"timeout_ms must be %u..%u",
|
||||
(unsigned)domain_policy_image_generation_timeout_min_ms(),
|
||||
(unsigned)domain_policy_image_generation_timeout_max_ms());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out->timeout_ms = (uint32_t)jtimeout->valuedouble;
|
||||
}
|
||||
|
||||
cJSON *jfetch_timeout = cJSON_GetObjectItemCaseSensitive(json, "fetch_timeout_ms");
|
||||
if (cJSON_IsNumber(jfetch_timeout)) {
|
||||
if (jfetch_timeout->valuedouble < domain_policy_image_download_timeout_min_ms() ||
|
||||
jfetch_timeout->valuedouble > domain_policy_image_download_timeout_max_ms()) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err,
|
||||
err_len,
|
||||
"fetch_timeout_ms must be %u..%u",
|
||||
(unsigned)domain_policy_image_download_timeout_min_ms(),
|
||||
(unsigned)domain_policy_image_download_timeout_max_ms());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out->fetch_timeout_ms = (uint32_t)jfetch_timeout->valuedouble;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static esp_err_t rest_server_send_image_generation_error(httpd_req_t *req,
|
||||
esp_err_t rc,
|
||||
const char *msg) {
|
||||
const char *message = (msg != NULL && msg[0] != '\0') ? msg : "image generation failed";
|
||||
if (rc == ESP_ERR_INVALID_ARG) {
|
||||
return rest_server_send_error(req, "400 Bad Request", message);
|
||||
}
|
||||
if (rc == ESP_ERR_INVALID_STATE) {
|
||||
return rest_server_send_error(req, "503 Service Unavailable", message);
|
||||
}
|
||||
if (rc == ESP_ERR_TIMEOUT) {
|
||||
return rest_server_send_error(req, "504 Gateway Timeout", message);
|
||||
}
|
||||
if (rc == ESP_ERR_NO_MEM) {
|
||||
return rest_server_send_error(req, "500 Internal Server Error", message);
|
||||
}
|
||||
return rest_server_send_error(req, "502 Bad Gateway", message);
|
||||
}
|
||||
|
||||
static void rest_server_try_preview_raster(const char *source,
|
||||
const uint8_t *raster,
|
||||
size_t raster_len,
|
||||
uint16_t width,
|
||||
uint16_t height) {
|
||||
if (raster == NULL || raster_len == 0 || width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char preview_err[96] = {0};
|
||||
esp_err_t preview_rc = screen_preview_show_raster(raster,
|
||||
raster_len,
|
||||
width,
|
||||
height,
|
||||
SCREEN_PREVIEW_TIMEOUT_MS,
|
||||
preview_err,
|
||||
sizeof(preview_err));
|
||||
if (preview_rc == ESP_OK) {
|
||||
ESP_LOGI(TAG,
|
||||
"screen preview updated (%s), size=%ux%u",
|
||||
source != NULL ? source : "unknown",
|
||||
(unsigned)width,
|
||||
(unsigned)height);
|
||||
return;
|
||||
}
|
||||
if (preview_rc == ESP_ERR_NOT_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"screen preview skipped (%s), rc=0x%x, msg=%s",
|
||||
source != NULL ? source : "unknown",
|
||||
(unsigned)preview_rc,
|
||||
preview_err[0] != '\0' ? preview_err : "preview failed");
|
||||
}
|
||||
|
||||
static esp_err_t rest_server_print_image_binary(httpd_req_t *req) {
|
||||
bool scale_to_width = true;
|
||||
uint8_t threshold = 160;
|
||||
bool invert = false;
|
||||
uint16_t max_height = 2200;
|
||||
const char *density = "中等";
|
||||
bool direct_ignore_precheck = false;
|
||||
parse_image_upload_options(req,
|
||||
&scale_to_width,
|
||||
&threshold,
|
||||
&invert,
|
||||
&max_height,
|
||||
&density,
|
||||
&direct_ignore_precheck);
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image process start (upload), png_bytes=%u, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s, ignore_precheck=%d",
|
||||
(unsigned)req->content_len,
|
||||
scale_to_width,
|
||||
(unsigned)threshold,
|
||||
invert,
|
||||
(unsigned)max_height,
|
||||
density != NULL ? density : "中等",
|
||||
direct_ignore_precheck);
|
||||
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
uint8_t *raster = NULL;
|
||||
size_t raster_len = 0;
|
||||
char decode_err[128] = {0};
|
||||
esp_err_t decode_rc = raster_tools_convert_png_to_raster_384((const uint8_t *)body,
|
||||
(size_t)req->content_len,
|
||||
scale_to_width,
|
||||
threshold,
|
||||
invert,
|
||||
max_height,
|
||||
&width,
|
||||
&height,
|
||||
&raster,
|
||||
&raster_len,
|
||||
decode_err,
|
||||
sizeof(decode_err));
|
||||
free(body);
|
||||
if (decode_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"image process failed (upload), rc=0x%x, msg=%s",
|
||||
(unsigned)decode_rc,
|
||||
decode_err[0] != '\0' ? decode_err : "decode png failed");
|
||||
return rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
decode_err[0] != '\0' ? decode_err : "decode png failed");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image process done (upload), raster_size=%ux%u, raster_bytes=%u",
|
||||
(unsigned)width,
|
||||
(unsigned)height,
|
||||
(unsigned)raster_len);
|
||||
rest_server_try_preview_raster("upload", raster, raster_len, width, height);
|
||||
|
||||
esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req,
|
||||
raster,
|
||||
raster_len,
|
||||
width,
|
||||
height,
|
||||
density,
|
||||
direct_ignore_precheck,
|
||||
NULL,
|
||||
NULL);
|
||||
free(raster);
|
||||
return submit_rc;
|
||||
}
|
||||
|
||||
static esp_err_t rest_server_print_image_generate(httpd_req_t *req) {
|
||||
char *body = NULL;
|
||||
cJSON *json = NULL;
|
||||
image_generation_result_t *gen_result = NULL;
|
||||
uint8_t *raster = NULL;
|
||||
char request_id[80] = {0};
|
||||
printer_status_poll_pause_token_t status_poll_pause_token = 0;
|
||||
esp_err_t ret = ESP_FAIL;
|
||||
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
json = cJSON_Parse(body);
|
||||
free(body);
|
||||
body = NULL;
|
||||
if (json == NULL) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid json");
|
||||
}
|
||||
|
||||
image_generate_options_t gen_opt = {0};
|
||||
char parse_err[128] = {0};
|
||||
if (!parse_image_generation_options(json, &gen_opt, parse_err, sizeof(parse_err))) {
|
||||
ret = rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
parse_err[0] != '\0' ? parse_err : "invalid image options");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
image_print_options_t print_opt = {0};
|
||||
image_print_options_set_defaults(&print_opt);
|
||||
parse_image_print_options_from_json(json, &print_opt);
|
||||
cJSON_Delete(json);
|
||||
json = NULL;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image generate start, prompt_len=%u, size=%s, prompt_extend=%d, seed=%s, timeout_ms=%u, fetch_timeout_ms=%u",
|
||||
(unsigned)strlen(gen_opt.prompt),
|
||||
gen_opt.has_size ? gen_opt.size : "default",
|
||||
gen_opt.prompt_extend,
|
||||
gen_opt.has_seed ? "set" : "auto",
|
||||
(unsigned)gen_opt.timeout_ms,
|
||||
(unsigned)gen_opt.fetch_timeout_ms);
|
||||
ESP_LOGI(TAG,
|
||||
"image process config, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s, ignore_precheck=%d",
|
||||
print_opt.scale_to_width,
|
||||
(unsigned)print_opt.threshold,
|
||||
print_opt.invert,
|
||||
(unsigned)print_opt.max_height,
|
||||
print_opt.density != NULL ? print_opt.density : "中等",
|
||||
print_opt.direct_ignore_precheck);
|
||||
|
||||
image_generation_request_t gen_req = {
|
||||
.prompt = gen_opt.prompt,
|
||||
.size = gen_opt.has_size ? gen_opt.size : NULL,
|
||||
.prompt_extend = gen_opt.prompt_extend,
|
||||
.has_seed = gen_opt.has_seed,
|
||||
.seed = gen_opt.seed,
|
||||
.generation_timeout_ms = gen_opt.timeout_ms,
|
||||
.download_timeout_ms = gen_opt.fetch_timeout_ms,
|
||||
};
|
||||
|
||||
gen_result = (image_generation_result_t *)calloc(1, sizeof(*gen_result));
|
||||
if (gen_result == NULL) {
|
||||
ret = rest_server_send_error(req, "500 Internal Server Error", "no memory");
|
||||
goto cleanup;
|
||||
}
|
||||
image_generation_result_reset(gen_result);
|
||||
|
||||
char model_err[160] = {0};
|
||||
status_poll_pause_token = printer_protocol_status_poll_pause_acquire();
|
||||
esp_err_t gen_rc = image_generation_generate_png(&gen_req,
|
||||
gen_result,
|
||||
model_err,
|
||||
sizeof(model_err));
|
||||
printer_protocol_status_poll_pause_release(&status_poll_pause_token);
|
||||
if (gen_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"image generate failed, rc=0x%x, msg=%s",
|
||||
(unsigned)gen_rc,
|
||||
model_err[0] != '\0' ? model_err : "image generation failed");
|
||||
ret = rest_server_send_image_generation_error(req, gen_rc, model_err);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (gen_result->request_id[0] != '\0') {
|
||||
strlcpy(request_id, gen_result->request_id, sizeof(request_id));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image generate done, request_id=%s, model_size=%ux%u, png_bytes=%u",
|
||||
request_id[0] != '\0' ? request_id : "-",
|
||||
(unsigned)gen_result->width,
|
||||
(unsigned)gen_result->height,
|
||||
(unsigned)gen_result->png_len);
|
||||
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
size_t raster_len = 0;
|
||||
char decode_err[128] = {0};
|
||||
esp_err_t decode_rc = raster_tools_convert_png_to_raster_384(gen_result->png,
|
||||
gen_result->png_len,
|
||||
print_opt.scale_to_width,
|
||||
print_opt.threshold,
|
||||
print_opt.invert,
|
||||
print_opt.max_height,
|
||||
&width,
|
||||
&height,
|
||||
&raster,
|
||||
&raster_len,
|
||||
decode_err,
|
||||
sizeof(decode_err));
|
||||
if (decode_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"image process failed (generated), rc=0x%x, msg=%s",
|
||||
(unsigned)decode_rc,
|
||||
decode_err[0] != '\0' ? decode_err : "decode generated image failed");
|
||||
ret = rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
decode_err[0] != '\0' ? decode_err : "decode generated image failed");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image process done (generated), raster_size=%ux%u, raster_bytes=%u",
|
||||
(unsigned)width,
|
||||
(unsigned)height,
|
||||
(unsigned)raster_len);
|
||||
rest_server_try_preview_raster("generated", raster, raster_len, width, height);
|
||||
|
||||
char warning[160] = {0};
|
||||
if (request_id[0] != '\0') {
|
||||
snprintf(warning, sizeof(warning), "z-image request_id=%s", request_id);
|
||||
}
|
||||
|
||||
ret = rest_server_print_submit_raster_job_and_reply(req,
|
||||
raster,
|
||||
raster_len,
|
||||
width,
|
||||
height,
|
||||
print_opt.density,
|
||||
print_opt.direct_ignore_precheck,
|
||||
warning,
|
||||
request_id);
|
||||
|
||||
cleanup:
|
||||
printer_protocol_status_poll_pause_release(&status_poll_pause_token);
|
||||
if (body != NULL) {
|
||||
free(body);
|
||||
}
|
||||
if (json != NULL) {
|
||||
cJSON_Delete(json);
|
||||
}
|
||||
if (raster != NULL) {
|
||||
free(raster);
|
||||
}
|
||||
if (gen_result != NULL) {
|
||||
image_generation_result_free(gen_result);
|
||||
free(gen_result);
|
||||
}
|
||||
printer_runtime_status_t runtime = {0};
|
||||
printer_protocol_get_runtime_status(&runtime);
|
||||
if (!runtime.busy) {
|
||||
image_generation_schedule_prewarm();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_print_image_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
char content_type[96] = {0};
|
||||
if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) {
|
||||
if (strstr(content_type, "application/json") != NULL) {
|
||||
return rest_server_print_image_generate(req);
|
||||
}
|
||||
if (strstr(content_type, "image/png") == NULL) {
|
||||
return rest_server_send_error(req,
|
||||
"415 Unsupported Media Type",
|
||||
"Content-Type must be application/json or image/png");
|
||||
}
|
||||
}
|
||||
|
||||
return rest_server_print_image_binary(req);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "domain.h"
|
||||
|
||||
esp_err_t rest_server_print_text_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
cJSON *json = cJSON_Parse(body);
|
||||
free(body);
|
||||
if (json == NULL) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid json");
|
||||
}
|
||||
|
||||
cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text");
|
||||
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
|
||||
cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale");
|
||||
cJSON *jline = cJSON_GetObjectItemCaseSensitive(json, "line_spacing");
|
||||
cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height");
|
||||
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
|
||||
|
||||
if (!cJSON_IsString(jtext) || jtext->valuestring == NULL) {
|
||||
cJSON_Delete(json);
|
||||
return rest_server_send_error(req, "400 Bad Request", "text is required");
|
||||
}
|
||||
|
||||
const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL)
|
||||
? jdensity->valuestring
|
||||
: "中等";
|
||||
bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false);
|
||||
|
||||
uint8_t scale = 2;
|
||||
uint8_t line_spacing = 2;
|
||||
uint16_t max_height = 2000;
|
||||
|
||||
if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 6) {
|
||||
scale = (uint8_t)jscale->valuedouble;
|
||||
}
|
||||
if (cJSON_IsNumber(jline) && jline->valuedouble >= 0 && jline->valuedouble <= 12) {
|
||||
line_spacing = (uint8_t)jline->valuedouble;
|
||||
}
|
||||
if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) {
|
||||
max_height = (uint16_t)jmaxh->valuedouble;
|
||||
}
|
||||
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
uint8_t *raster = NULL;
|
||||
size_t raster_len = 0;
|
||||
char render_msg[128] = {0};
|
||||
|
||||
esp_err_t render_rc = raster_tools_render_text_384(jtext->valuestring,
|
||||
scale,
|
||||
line_spacing,
|
||||
max_height,
|
||||
&width,
|
||||
&height,
|
||||
&raster,
|
||||
&raster_len,
|
||||
render_msg,
|
||||
sizeof(render_msg));
|
||||
cJSON_Delete(json);
|
||||
|
||||
if (render_rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"400 Bad Request",
|
||||
render_msg[0] != 0 ? render_msg : "text render failed");
|
||||
}
|
||||
|
||||
esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req,
|
||||
raster,
|
||||
raster_len,
|
||||
width,
|
||||
height,
|
||||
density,
|
||||
direct_ignore_precheck,
|
||||
render_msg,
|
||||
NULL);
|
||||
free(raster);
|
||||
return submit_rc;
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "domain.h"
|
||||
|
||||
#define VOICE_CONTROL_TIMEOUT_MIN_MS 500
|
||||
#define VOICE_CONTROL_TIMEOUT_MAX_MS 30000
|
||||
|
||||
static esp_err_t rest_server_parse_voice_timeout(httpd_req_t *req, uint32_t *out_timeout_ms) {
|
||||
if (out_timeout_ms == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_timeout_ms = 0;
|
||||
if (req->content_len <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return body_err;
|
||||
}
|
||||
|
||||
cJSON *json = cJSON_Parse(body);
|
||||
free(body);
|
||||
if (json == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
|
||||
if (jtimeout != NULL) {
|
||||
if (!cJSON_IsNumber(jtimeout) ||
|
||||
jtimeout->valuedouble < VOICE_CONTROL_TIMEOUT_MIN_MS ||
|
||||
jtimeout->valuedouble > VOICE_CONTROL_TIMEOUT_MAX_MS) {
|
||||
cJSON_Delete(json);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_timeout_ms = (uint32_t)jtimeout->valuedouble;
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void rest_server_add_voice_status(cJSON *root) {
|
||||
voice_interaction_status_t st = {0};
|
||||
voice_interaction_get_status(&st);
|
||||
printer_runtime_status_t printer = {0};
|
||||
printer_protocol_get_runtime_status(&printer);
|
||||
|
||||
cJSON_AddBoolToObject(root, "voice_ready", st.ready);
|
||||
cJSON_AddBoolToObject(root, "voice_session_active", st.session_active);
|
||||
cJSON_AddBoolToObject(root, "voice_ws_connected", st.ws_connected);
|
||||
cJSON_AddBoolToObject(root, "voice_started", st.started);
|
||||
cJSON_AddBoolToObject(root, "voice_tap_active", st.tap_active);
|
||||
cJSON_AddBoolToObject(root, "voice_marker_image_gen_running", st.marker_image_gen_running);
|
||||
cJSON_AddStringToObject(root,
|
||||
"voice_dialog_state",
|
||||
voice_interaction_dialog_state_str(st.dialog_state));
|
||||
cJSON_AddStringToObject(root, "voice_task_id", st.task_id);
|
||||
cJSON_AddStringToObject(root, "voice_dialog_id", st.dialog_id);
|
||||
cJSON_AddStringToObject(root, "voice_last_error", st.last_error);
|
||||
cJSON_AddNumberToObject(root, "voice_last_event_ms", (double)st.last_event_ms);
|
||||
cJSON_AddNumberToObject(root, "voice_upstream_packets", (double)st.upstream_packets);
|
||||
cJSON_AddNumberToObject(root, "voice_downstream_packets", (double)st.downstream_packets);
|
||||
cJSON_AddBoolToObject(root, "printer_busy", printer.busy);
|
||||
cJSON_AddNumberToObject(root, "queue_depth", (double)printer.queue_depth);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_status_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_session_start_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
uint32_t timeout_ms = 0;
|
||||
if (rest_server_parse_voice_timeout(req, &timeout_ms) != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
char start_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_start_with_timeout(timeout_ms, start_err, sizeof(start_err));
|
||||
if (rc != ESP_OK) {
|
||||
if (rc == ESP_ERR_TIMEOUT) {
|
||||
return rest_server_send_error(req,
|
||||
"504 Gateway Timeout",
|
||||
start_err[0] != '\0' ? start_err : "voice session start timeout");
|
||||
}
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
start_err[0] != '\0' ? start_err : "voice session start failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "voice session started");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_session_stop_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
uint32_t timeout_ms = 0;
|
||||
if (rest_server_parse_voice_timeout(req, &timeout_ms) != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
|
||||
char stop_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_stop_with_timeout(timeout_ms, stop_err, sizeof(stop_err));
|
||||
if (rc != ESP_OK) {
|
||||
if (rc == ESP_ERR_TIMEOUT) {
|
||||
return rest_server_send_error(req,
|
||||
"504 Gateway Timeout",
|
||||
stop_err[0] != '\0' ? stop_err : "voice session stop timeout");
|
||||
}
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
stop_err[0] != '\0' ? stop_err : "voice session stop failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "voice session stopped");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_tap_start_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char tap_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_tap_start(tap_err, sizeof(tap_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
tap_err[0] != '\0' ? tap_err : "tap start failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "tap started");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_tap_cancel_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char tap_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_tap_cancel(tap_err, sizeof(tap_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
tap_err[0] != '\0' ? tap_err : "tap cancel failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "tap stopped");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
@@ -37,10 +37,6 @@ config TQ_LIFECYCLE_RETRY_BACKOFF_MS
|
||||
range 100 10000
|
||||
default 800
|
||||
|
||||
config TQ_HTTP_PORT
|
||||
int "REST HTTP Port"
|
||||
default 80
|
||||
|
||||
config TQ_REST_PRINTER_CONNECT_TIMEOUT_MS
|
||||
int "REST printer connect timeout default (ms)"
|
||||
range 1000 60000
|
||||
@@ -186,10 +182,6 @@ config TQ_DIRECT_PRINTER_NTC_BETA
|
||||
default 3950
|
||||
depends on TQ_DIRECT_PRINTER_ENABLE
|
||||
|
||||
config TQ_API_KEY
|
||||
string "REST API Key (optional, empty means disabled)"
|
||||
default ""
|
||||
|
||||
config TQ_Z_IMAGE_API_KEY
|
||||
string "Z-Image API Key for DashScope HTTP (optional, fallback to Voice API key)"
|
||||
default "sk-7a50eca6856d4afb968ac3bf512f6d1b"
|
||||
|
||||
Reference in New Issue
Block a user