diff --git a/README.md b/README.md index e45cf39..a3f6acb 100644 --- a/README.md +++ b/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: -``` - -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:/// -``` - -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:///v1/health -``` - -### Connect printer -```bash -curl -X POST http:///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:///v1/print/raster \ - -H 'Content-Type: application/json' \ - -d '{ - "width":384, - "height":200, - "density":"中等", - "encoding":"base64_msb_1bpp", - "data":"" - }' -``` - -### Image print (generate with prompt, then print) -```bash -curl -X POST http:///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:///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:///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:///v1/jobs -curl http:///v1/jobs/1 -``` - -### Voice push2talk -Start session: - -```bash -curl -X POST http:///v1/voice/session/start -``` - -Press-to-talk start (sends `SendSpeech` when entering `Listening`): - -```bash -curl -X POST http:///v1/voice/tap/start -``` - -Release-to-stop (sends `StopSpeech`): - -```bash -curl -X POST http:///v1/voice/tap/cancel -``` - -Get voice status: - -```bash -curl http:///v1/voice/status -``` - -Stop session: - -```bash -curl -X POST http:///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 diff --git a/components/control_plane/CMakeLists.txt b/components/control_plane/CMakeLists.txt index 6e58054..cad72f5 100644 --- a/components/control_plane/CMakeLists.txt +++ b/components/control_plane/CMakeLists.txt @@ -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 ) diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index c3e7c64..5f58105 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -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 diff --git a/components/control_plane/internal/rest_server_internal.h b/components/control_plane/internal/rest_server_internal.h deleted file mode 100644 index b47ef63..0000000 --- a/components/control_plane/internal/rest_server_internal.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include - -#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); diff --git a/components/control_plane/src/controller_lifecycle.c b/components/control_plane/src/controller_lifecycle.c index 25bf147..e95ebce 100644 --- a/components/control_plane/src/controller_lifecycle.c +++ b/components/control_plane/src/controller_lifecycle.c @@ -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; } diff --git a/components/control_plane/src/rest_server.c b/components/control_plane/src/rest_server.c deleted file mode 100644 index ec271a7..0000000 --- a/components/control_plane/src/rest_server.c +++ /dev/null @@ -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 = - "\n" - "\n" - "\n" - " \n" - " \n" - " TQ Voice Console\n" - " \n" - "\n" - "\n" - "
\n" - "

TQ Voice Console

\n" - "

ESP32-S3 本地语音控制页。已切换为 MIC_KEY(IO14)触发单轮语音对话。

\n" - "\n" - "
按住机身 MIC_KEY 说话,松开后自动进入“语音交互→生图→显示→处理→打印”。
\n" - "
语音状态:初始化中
\n" - "
\n" - "\n" - " \n" - "\n" - "\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; - } -} diff --git a/components/control_plane/src/rest_server_common.c b/components/control_plane/src/rest_server_common.c deleted file mode 100644 index 9e17e26..0000000 --- a/components/control_plane/src/rest_server_common.c +++ /dev/null @@ -1,163 +0,0 @@ -#include "rest_server_internal.h" - -#include -#include -#include - -#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; -} diff --git a/components/control_plane/src/rest_server_jobs.c b/components/control_plane/src/rest_server_jobs.c deleted file mode 100644 index 2f8a0c0..0000000 --- a/components/control_plane/src/rest_server_jobs.c +++ /dev/null @@ -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; -} diff --git a/components/control_plane/src/rest_server_ops.c b/components/control_plane/src/rest_server_ops.c deleted file mode 100644 index 9b567d1..0000000 --- a/components/control_plane/src/rest_server_ops.c +++ /dev/null @@ -1,258 +0,0 @@ -#include "rest_server_internal.h" - -#include -#include -#include - -#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; -} diff --git a/components/control_plane/src/rest_server_print.c b/components/control_plane/src/rest_server_print.c deleted file mode 100644 index b664e86..0000000 --- a/components/control_plane/src/rest_server_print.c +++ /dev/null @@ -1,135 +0,0 @@ -#include "rest_server_internal.h" - -#include -#include - -#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; -} diff --git a/components/control_plane/src/rest_server_print_image.c b/components/control_plane/src/rest_server_print_image.c deleted file mode 100644 index a28a6ef..0000000 --- a/components/control_plane/src/rest_server_print_image.c +++ /dev/null @@ -1,661 +0,0 @@ -#include "rest_server_internal.h" - -#include -#include -#include -#include - -#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); -} diff --git a/components/control_plane/src/rest_server_print_text.c b/components/control_plane/src/rest_server_print_text.c deleted file mode 100644 index 693afe2..0000000 --- a/components/control_plane/src/rest_server_print_text.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "rest_server_internal.h" - -#include - -#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; -} diff --git a/components/control_plane/src/rest_server_voice.c b/components/control_plane/src/rest_server_voice.c deleted file mode 100644 index 6363465..0000000 --- a/components/control_plane/src/rest_server_voice.c +++ /dev/null @@ -1,213 +0,0 @@ -#include "rest_server_internal.h" - -#include - -#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; -} diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index b261e11..2585cd6 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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"