chore(repo): keep only current main snapshot

History squashed to one root commit to permanently drop old branch history and objects.
This commit is contained in:
admin
2026-02-28 16:41:12 +08:00
commit 8df2095acf
89 changed files with 21634 additions and 0 deletions
@@ -0,0 +1,310 @@
#include "control_plane.h"
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "domain.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
static const char *TAG = "controller_lifecycle";
static SemaphoreHandle_t s_lock;
static controller_lifecycle_status_t s_status = {
.state = CONTROLLER_LIFECYCLE_STATE_STOPPED,
.last_error = ESP_OK,
.start_attempt = 0,
.last_transition_ms = 0,
.last_stage = "stopped",
};
static void lifecycle_lock_init_if_needed(void) {
if (s_lock == NULL) {
s_lock = xSemaphoreCreateMutex();
}
}
static bool lifecycle_lock_take(uint32_t timeout_ms) {
lifecycle_lock_init_if_needed();
return s_lock != NULL && xSemaphoreTake(s_lock, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
}
static void lifecycle_set_state_locked(controller_lifecycle_state_t state,
esp_err_t last_error,
const char *stage) {
s_status.state = state;
s_status.last_error = last_error;
s_status.last_transition_ms = esp_timer_get_time() / 1000;
strlcpy(s_status.last_stage, stage != NULL ? stage : "unknown", sizeof(s_status.last_stage));
domain_diag_set_gauge(DOMAIN_DIAG_GAUGE_LIFECYCLE_STATE, (int32_t)state);
}
const char *controller_lifecycle_state_str(controller_lifecycle_state_t state) {
switch (state) {
case CONTROLLER_LIFECYCLE_STATE_STOPPED:
return "stopped";
case CONTROLLER_LIFECYCLE_STATE_STARTING:
return "starting";
case CONTROLLER_LIFECYCLE_STATE_RUNNING:
return "running";
case CONTROLLER_LIFECYCLE_STATE_DEGRADED:
return "degraded";
case CONTROLLER_LIFECYCLE_STATE_STOPPING:
return "stopping";
default:
return "unknown";
}
}
static esp_err_t lifecycle_start_system_runtime(void) {
return system_runtime_start();
}
static esp_err_t lifecycle_start_printer_protocol(void) {
return printer_protocol_init();
}
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);
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());
if (rc == ESP_OK || rc == ESP_ERR_INVALID_STATE) {
return ESP_OK;
}
return rc;
}
static esp_err_t lifecycle_stop_system_runtime(void) {
return system_runtime_shutdown();
}
typedef esp_err_t (*lifecycle_step_fn_t)(void);
static esp_err_t lifecycle_run_step_with_retry(const char *stage, lifecycle_step_fn_t fn) {
uint32_t retry_count = domain_policy_lifecycle_start_retry_count();
uint32_t max_attempts = retry_count + 1;
for (uint32_t attempt = 0; attempt < max_attempts; ++attempt) {
esp_err_t rc = fn();
if (rc == ESP_OK) {
if (attempt > 0) {
ESP_LOGW(TAG, "stage %s recovered after retries, attempt=%u", stage, (unsigned)(attempt + 1));
}
return ESP_OK;
}
bool can_retry = (attempt + 1 < max_attempts) && domain_policy_is_retryable_error(rc);
ESP_LOGW(TAG,
"stage %s failed, rc=0x%x, attempt=%u/%u, retry=%d",
stage,
(unsigned)rc,
(unsigned)(attempt + 1),
(unsigned)max_attempts,
can_retry ? 1 : 0);
if (!can_retry) {
return rc;
}
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_START_RETRY, 1);
vTaskDelay(pdMS_TO_TICKS(domain_policy_lifecycle_retry_backoff_ms(attempt)));
}
return ESP_FAIL;
}
esp_err_t controller_lifecycle_start(void) {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_START_ATTEMPT, 1);
if (!lifecycle_lock_take(1000)) {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_START_FAILED, 1);
domain_diag_record_error("lifecycle_start", ESP_ERR_TIMEOUT, "lifecycle lock timeout");
return ESP_ERR_TIMEOUT;
}
if (s_status.state == CONTROLLER_LIFECYCLE_STATE_RUNNING) {
xSemaphoreGive(s_lock);
return ESP_OK;
}
if (s_status.state == CONTROLLER_LIFECYCLE_STATE_STARTING ||
s_status.state == CONTROLLER_LIFECYCLE_STATE_STOPPING) {
xSemaphoreGive(s_lock);
return ESP_ERR_INVALID_STATE;
}
s_status.start_attempt++;
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_STARTING, ESP_OK, "bootstrap");
xSemaphoreGive(s_lock);
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";
esp_err_t rc = lifecycle_run_step_with_retry("system_runtime", lifecycle_start_system_runtime);
if (rc != ESP_OK) {
goto start_failed;
}
system_started = true;
failed_stage = "printer_protocol";
rc = lifecycle_run_step_with_retry("printer_protocol", lifecycle_start_printer_protocol);
if (rc != ESP_OK) {
goto start_failed;
}
printer_started = true;
failed_stage = "voice_interaction";
rc = lifecycle_run_step_with_retry("voice_interaction", lifecycle_start_voice);
if (rc != ESP_OK) {
goto start_failed;
}
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)) {
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_RUNNING, ESP_OK, "running");
xSemaphoreGive(s_lock);
}
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_START_SUCCESS, 1);
ESP_LOGI(TAG, "controller lifecycle started");
return ESP_OK;
start_failed:
if (rest_started) {
(void)lifecycle_stop_rest_server();
}
if (voice_started) {
(void)lifecycle_stop_voice();
}
if (printer_started) {
(void)lifecycle_stop_printer_protocol();
}
if (system_started) {
(void)lifecycle_stop_system_runtime();
}
if (lifecycle_lock_take(1000)) {
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_DEGRADED, rc, failed_stage);
xSemaphoreGive(s_lock);
}
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_START_FAILED, 1);
domain_diag_record_error("lifecycle_start", rc, failed_stage);
ESP_LOGE(TAG,
"controller lifecycle start failed at stage=%s, rc=0x%x",
failed_stage,
(unsigned)rc);
return rc;
}
esp_err_t controller_lifecycle_stop(void) {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_STOP_ATTEMPT, 1);
if (!lifecycle_lock_take(1000)) {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_STOP_FAILED, 1);
domain_diag_record_error("lifecycle_stop", ESP_ERR_TIMEOUT, "lifecycle lock timeout");
return ESP_ERR_TIMEOUT;
}
if (s_status.state == CONTROLLER_LIFECYCLE_STATE_STOPPED) {
xSemaphoreGive(s_lock);
return ESP_OK;
}
if (s_status.state == CONTROLLER_LIFECYCLE_STATE_STARTING ||
s_status.state == CONTROLLER_LIFECYCLE_STATE_STOPPING) {
xSemaphoreGive(s_lock);
return ESP_ERR_INVALID_STATE;
}
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_STOPPING, ESP_OK, "stopping");
xSemaphoreGive(s_lock);
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();
if (rc != ESP_OK && first_err == ESP_OK) {
first_err = rc;
}
rc = lifecycle_stop_printer_protocol();
if (rc != ESP_OK && first_err == ESP_OK) {
first_err = rc;
}
rc = lifecycle_stop_system_runtime();
if (rc != ESP_OK && first_err == ESP_OK) {
first_err = rc;
}
if (lifecycle_lock_take(1000)) {
if (first_err == ESP_OK) {
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_STOPPED, ESP_OK, "stopped");
} else {
lifecycle_set_state_locked(CONTROLLER_LIFECYCLE_STATE_DEGRADED, first_err, "stop_failed");
}
xSemaphoreGive(s_lock);
}
if (first_err == ESP_OK) {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_STOP_SUCCESS, 1);
ESP_LOGI(TAG, "controller lifecycle stopped");
} else {
domain_diag_counter_add(DOMAIN_DIAG_COUNTER_LIFECYCLE_STOP_FAILED, 1);
domain_diag_record_error("lifecycle_stop", first_err, "controller stop failed");
ESP_LOGE(TAG, "controller lifecycle stop failed, rc=0x%x", (unsigned)first_err);
}
return first_err;
}
void controller_lifecycle_get_status(controller_lifecycle_status_t *out_status) {
if (out_status == NULL) {
return;
}
memset(out_status, 0, sizeof(*out_status));
if (!lifecycle_lock_take(200)) {
return;
}
*out_status = s_status;
xSemaphoreGive(s_lock);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
#include "rest_server_internal.h"
#include <errno.h>
#include <stdio.h>
#include <stdarg.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;
}
void rest_server_appendf(char *dst, size_t cap, size_t *offset, const char *fmt, ...) {
if (dst == NULL || cap == 0 || offset == NULL || fmt == NULL || *offset >= cap) {
return;
}
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(dst + *offset, cap - *offset, fmt, ap);
va_end(ap);
if (n <= 0) {
return;
}
size_t written = (size_t)n;
if (written >= cap - *offset) {
*offset = cap - 1;
return;
}
*offset += written;
}
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;
}
@@ -0,0 +1,173 @@
#include "rest_server_internal.h"
#include <stdlib.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;
}
esp_err_t rest_server_job_delete(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");
}
char cancel_err[128] = {0};
esp_err_t cancel_rc = printer_protocol_cancel_job(job_id, cancel_err, sizeof(cancel_err));
if (cancel_rc == ESP_ERR_NOT_FOUND) {
return rest_server_send_error(req, "404 Not Found", "job not found");
}
if (cancel_rc == ESP_ERR_INVALID_STATE) {
return rest_server_send_error(req,
"409 Conflict",
cancel_err[0] != '\0' ? cancel_err : "job already finished");
}
if (cancel_rc != ESP_OK) {
return rest_server_send_error(req,
"409 Conflict",
cancel_err[0] != '\0' ? cancel_err : "cancel failed");
}
print_job_info_t info;
bool ok = printer_protocol_get_job(job_id, &info);
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "job_id", job_id);
if (ok) {
cJSON_AddStringToObject(root, "state", printer_protocol_job_state_str(info.state));
cJSON_AddStringToObject(root, "error", info.error);
} else {
cJSON_AddStringToObject(root, "state", "canceled");
}
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_jobs_delete(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
bool include_success = true;
bool include_failed = true;
bool include_canceled = true;
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");
}
cJSON *json = cJSON_Parse(body);
free(body);
if (json == NULL) {
return rest_server_send_error(req, "400 Bad Request", "invalid json");
}
include_success = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_success"), true);
include_failed = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_failed"), true);
include_canceled = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_canceled"), true);
cJSON_Delete(json);
}
size_t removed = printer_protocol_cleanup_jobs(include_success, include_failed, include_canceled);
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "removed", (double)removed);
cJSON_AddBoolToObject(root, "include_success", include_success);
cJSON_AddBoolToObject(root, "include_failed", include_failed);
cJSON_AddBoolToObject(root, "include_canceled", include_canceled);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
@@ -0,0 +1,807 @@
#include "rest_server_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include "control_plane.h"
#include "domain.h"
static const char *printer_backend_str(printer_backend_t backend) {
switch (backend) {
case PRINTER_BACKEND_BLE:
return "ble";
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, "ble") == 0) {
*out_backend = PRINTER_BACKEND_BLE;
return true;
}
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, "ble_connected", st.backend == PRINTER_BACKEND_BLE && st.connected);
cJSON_AddBoolToObject(root, "ble_notify_ready", st.backend == PRINTER_BACKEND_BLE && st.notify_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, "mtu", st.mtu);
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;
char name[32] = "TQPrinter";
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 *jname = cJSON_GetObjectItemCaseSensitive(json, "name");
if (cJSON_IsString(jname) && jname->valuestring != NULL) {
strlcpy(name, jname->valuestring, sizeof(name));
}
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 ble or direct");
}
}
cJSON_Delete(json);
free(body);
}
printer_connect_options_t opt = {
.backend = backend,
.name = backend == PRINTER_BACKEND_BLE ? 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;
}
static void add_direct_debug_json_fields(cJSON *root, const printer_direct_debug_config_t *cfg) {
cJSON_AddNumberToObject(root, "shift_clock_high_us", cfg->shift_clock_high_us);
cJSON_AddNumberToObject(root, "shift_clock_low_us", cfg->shift_clock_low_us);
cJSON_AddNumberToObject(root, "latch_pulse_us", cfg->latch_pulse_us);
cJSON_AddBoolToObject(root, "strobe_active_high", cfg->strobe_active_high);
cJSON_AddBoolToObject(root, "boost_active_high", cfg->boost_active_high);
cJSON_AddNumberToObject(root, "override_strobe_on_us", cfg->override_strobe_on_us);
cJSON_AddNumberToObject(root, "override_strobe_interval_us", cfg->override_strobe_interval_us);
cJSON_AddNumberToObject(root, "override_motor_step_us", cfg->override_motor_step_us);
cJSON_AddNumberToObject(root, "override_steps_per_line", cfg->override_steps_per_line);
}
static esp_err_t direct_debug_error_response(httpd_req_t *req, esp_err_t rc, const char *msg) {
if (rc == ESP_ERR_INVALID_ARG) {
return rest_server_send_error(req, "400 Bad Request", msg);
}
if (rc == ESP_ERR_NOT_SUPPORTED || rc == ESP_ERR_INVALID_STATE) {
return rest_server_send_error(req, "409 Conflict", msg);
}
if (rc == ESP_ERR_TIMEOUT) {
return rest_server_send_error(req, "504 Gateway Timeout", msg);
}
return rest_server_send_error(req, "500 Internal Server Error", msg);
}
esp_err_t rest_server_printer_direct_debug_get(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
printer_direct_debug_config_t cfg = {0};
char get_err[128] = {0};
esp_err_t rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err));
if (rc != ESP_OK) {
return direct_debug_error_response(req,
rc,
get_err[0] != '\0' ? get_err : "get direct debug failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddStringToObject(root, "backend", printer_backend_str(printer_protocol_get_backend()));
add_direct_debug_json_fields(root, &cfg);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_printer_direct_debug_post(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
printer_direct_debug_config_t cfg = {0};
char get_err[128] = {0};
esp_err_t rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err));
if (rc != ESP_OK) {
return direct_debug_error_response(req,
rc,
get_err[0] != '\0' ? get_err : "get direct debug failed");
}
bool reset_defaults = false;
bool has_update = false;
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");
}
cJSON *json = cJSON_Parse(body);
free(body);
if (json == NULL) {
return rest_server_send_error(req, "400 Bad Request", "invalid json");
}
cJSON *jreset = cJSON_GetObjectItemCaseSensitive(json, "reset_defaults");
if (cJSON_IsBool(jreset)) {
reset_defaults = cJSON_IsTrue(jreset);
}
cJSON *jshift_hi = cJSON_GetObjectItemCaseSensitive(json, "shift_clock_high_us");
if (jshift_hi != NULL) {
if (!cJSON_IsNumber(jshift_hi) || jshift_hi->valuedouble < 1 || jshift_hi->valuedouble > 50) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "shift_clock_high_us must be 1..50");
}
cfg.shift_clock_high_us = (uint16_t)jshift_hi->valuedouble;
has_update = true;
}
cJSON *jshift_lo = cJSON_GetObjectItemCaseSensitive(json, "shift_clock_low_us");
if (jshift_lo != NULL) {
if (!cJSON_IsNumber(jshift_lo) || jshift_lo->valuedouble < 1 || jshift_lo->valuedouble > 50) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "shift_clock_low_us must be 1..50");
}
cfg.shift_clock_low_us = (uint16_t)jshift_lo->valuedouble;
has_update = true;
}
cJSON *jlatch = cJSON_GetObjectItemCaseSensitive(json, "latch_pulse_us");
if (jlatch != NULL) {
if (!cJSON_IsNumber(jlatch) || jlatch->valuedouble < 1 || jlatch->valuedouble > 50) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "latch_pulse_us must be 1..50");
}
cfg.latch_pulse_us = (uint16_t)jlatch->valuedouble;
has_update = true;
}
cJSON *jstb = cJSON_GetObjectItemCaseSensitive(json, "strobe_active_high");
if (cJSON_IsBool(jstb)) {
cfg.strobe_active_high = cJSON_IsTrue(jstb);
has_update = true;
}
cJSON *jboost = cJSON_GetObjectItemCaseSensitive(json, "boost_active_high");
if (cJSON_IsBool(jboost)) {
cfg.boost_active_high = cJSON_IsTrue(jboost);
has_update = true;
}
cJSON *jstrobe_on = cJSON_GetObjectItemCaseSensitive(json, "override_strobe_on_us");
if (jstrobe_on != NULL) {
if (!cJSON_IsNumber(jstrobe_on) || jstrobe_on->valuedouble < 0 || jstrobe_on->valuedouble > 10000) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_strobe_on_us must be 0..10000");
}
if (jstrobe_on->valuedouble > 0 && jstrobe_on->valuedouble < 100) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_strobe_on_us must be 0 or 100..10000");
}
cfg.override_strobe_on_us = (uint16_t)jstrobe_on->valuedouble;
has_update = true;
}
cJSON *jstrobe_gap = cJSON_GetObjectItemCaseSensitive(json, "override_strobe_interval_us");
if (jstrobe_gap != NULL) {
if (!cJSON_IsNumber(jstrobe_gap) || jstrobe_gap->valuedouble < 0 || jstrobe_gap->valuedouble > 10000) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_strobe_interval_us must be 0..10000");
}
cfg.override_strobe_interval_us = (uint16_t)jstrobe_gap->valuedouble;
has_update = true;
}
cJSON *jmotor = cJSON_GetObjectItemCaseSensitive(json, "override_motor_step_us");
if (jmotor != NULL) {
if (!cJSON_IsNumber(jmotor) || jmotor->valuedouble < 0 || jmotor->valuedouble > 20000) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_motor_step_us must be 0..20000");
}
if (jmotor->valuedouble > 0 && jmotor->valuedouble < 100) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_motor_step_us must be 0 or 100..20000");
}
cfg.override_motor_step_us = (uint16_t)jmotor->valuedouble;
has_update = true;
}
cJSON *jsteps = cJSON_GetObjectItemCaseSensitive(json, "override_steps_per_line");
if (jsteps != NULL) {
if (!cJSON_IsNumber(jsteps) || jsteps->valuedouble < 0 || jsteps->valuedouble > 8) {
cJSON_Delete(json);
return rest_server_send_error(req,
"400 Bad Request",
"override_steps_per_line must be 0..8");
}
cfg.override_steps_per_line = (uint8_t)jsteps->valuedouble;
has_update = true;
}
cJSON_Delete(json);
}
char set_err[128] = {0};
rc = printer_protocol_set_direct_debug_config(has_update ? &cfg : NULL,
reset_defaults,
set_err,
sizeof(set_err));
if (rc != ESP_OK) {
return direct_debug_error_response(req,
rc,
set_err[0] != '\0' ? set_err : "set direct debug failed");
}
memset(&cfg, 0, sizeof(cfg));
memset(get_err, 0, sizeof(get_err));
rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err));
if (rc != ESP_OK) {
return direct_debug_error_response(req,
rc,
get_err[0] != '\0' ? get_err : "get direct debug failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddBoolToObject(root, "reset_defaults", reset_defaults);
cJSON_AddBoolToObject(root, "updated", has_update);
cJSON_AddStringToObject(root, "backend", printer_backend_str(printer_protocol_get_backend()));
add_direct_debug_json_fields(root, &cfg);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_label_gap_move_post(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
uint32_t timeout_ms = domain_policy_rest_label_timeout_ms();
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");
}
cJSON *json = cJSON_Parse(body);
free(body);
if (json == NULL) {
return rest_server_send_error(req, "400 Bad Request", "invalid json");
}
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
if (jtimeout != NULL) {
if (!cJSON_IsNumber(jtimeout) ||
jtimeout->valuedouble < (double)domain_policy_rest_label_timeout_min_ms() ||
jtimeout->valuedouble > (double)domain_policy_rest_label_timeout_max_ms()) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "timeout_ms out of range");
}
timeout_ms = (uint32_t)jtimeout->valuedouble;
}
cJSON_Delete(json);
}
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_gap_move(timeout_ms, cmd_err, sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "gap move failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddStringToObject(root, "message", "gap move success");
cJSON_AddNumberToObject(root, "timeout_ms", timeout_ms);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_label_offset_get(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
uint8_t offset = 0;
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_get_label_offset(&offset,
domain_policy_rest_label_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "get offset failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "offset_tenths_mm", offset);
cJSON_AddNumberToObject(root, "offset_mm", ((double)offset) / 10.0);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_label_offset_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 *jvalue = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm");
if (!cJSON_IsNumber(jvalue) || jvalue->valuedouble < 0 || jvalue->valuedouble > 254) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254");
}
uint8_t value = (uint8_t)jvalue->valuedouble;
cJSON_Delete(json);
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_set_label_offset(value,
domain_policy_rest_label_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "set offset failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "offset_tenths_mm", value);
cJSON_AddNumberToObject(root, "offset_mm", ((double)value) / 10.0);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_version_get(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
printer_ota_version_t version = {0};
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_ota_get_version(&version,
domain_policy_rest_ota_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "get version failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "major", version.major);
cJSON_AddNumberToObject(root, "minor", version.minor);
cJSON_AddNumberToObject(root, "patch", version.patch);
char version_text[32];
snprintf(version_text, sizeof(version_text), "V%u.%u.%u", version.major, version.minor, version.patch);
cJSON_AddStringToObject(root, "version", version_text);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_jump_boot_post(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_ota_jump_boot(domain_policy_rest_ota_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump boot failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddStringToObject(root, "message", "jumped to boot");
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_jump_app_post(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_ota_jump_app(domain_policy_rest_ota_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump app failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddStringToObject(root, "message", "jumped to app");
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_erase_page_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 *jpage = cJSON_GetObjectItemCaseSensitive(json, "page_num");
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
if (!cJSON_IsNumber(jpage) || jpage->valuedouble < 0 || jpage->valuedouble > 65535) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "page_num must be 0..65535");
}
uint32_t timeout_ms = domain_policy_rest_ota_timeout_ms();
if (jtimeout != NULL) {
if (!cJSON_IsNumber(jtimeout) ||
jtimeout->valuedouble < (double)domain_policy_rest_ota_timeout_min_ms() ||
jtimeout->valuedouble > (double)domain_policy_rest_ota_timeout_max_ms()) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "timeout_ms out of range");
}
timeout_ms = (uint32_t)jtimeout->valuedouble;
}
uint16_t page_num = (uint16_t)jpage->valuedouble;
cJSON_Delete(json);
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_ota_erase_page(page_num, timeout_ms, cmd_err, sizeof(cmd_err));
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "erase page failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "page_num", page_num);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_write_frame_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 *jpack = cJSON_GetObjectItemCaseSensitive(json, "packet_num");
cJSON *jlast = cJSON_GetObjectItemCaseSensitive(json, "is_last_frame");
cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data");
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
if (!cJSON_IsNumber(jpack) || jpack->valuedouble < 0 || jpack->valuedouble > 65535 ||
!cJSON_IsString(jdata) || jdata->valuestring == NULL) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "packet_num and data are required");
}
uint32_t timeout_ms = domain_policy_rest_ota_timeout_ms();
if (jtimeout != NULL) {
if (!cJSON_IsNumber(jtimeout) ||
jtimeout->valuedouble < (double)domain_policy_rest_ota_timeout_min_ms() ||
jtimeout->valuedouble > (double)domain_policy_rest_ota_timeout_max_ms()) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "timeout_ms out of range");
}
timeout_ms = (uint32_t)jtimeout->valuedouble;
}
bool is_last = rest_server_json_bool_with_default(jlast, false);
uint16_t packet_num = (uint16_t)jpack->valuedouble;
uint8_t *frame_data = NULL;
size_t frame_len = 0;
esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &frame_data, &frame_len);
cJSON_Delete(json);
if (b64_rc != ESP_OK) {
return rest_server_send_error(req, "400 Bad Request", "invalid base64 data");
}
char cmd_err[128] = {0};
esp_err_t rc = printer_protocol_ota_write_frame(packet_num,
is_last,
frame_data,
frame_len,
timeout_ms,
cmd_err,
sizeof(cmd_err));
free(frame_data);
if (rc != ESP_OK) {
return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "write frame failed");
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "packet_num", packet_num);
cJSON_AddBoolToObject(root, "is_last_frame", is_last);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}
esp_err_t rest_server_ota_upgrade_post(httpd_req_t *req) {
if (!rest_server_auth_ok(req)) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
return rest_server_send_error(req,
"409 Conflict",
"bulk ota upgrade endpoint disabled; use jump/erase/write/version step APIs");
}
@@ -0,0 +1,135 @@
#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;
}
@@ -0,0 +1,661 @@
#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);
}
@@ -0,0 +1,540 @@
#include "rest_server_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "domain.h"
static esp_err_t decode_image_json_to_raster(cJSON *json,
uint16_t *out_width,
uint16_t *out_height,
uint8_t **out_raster,
size_t *out_len,
char *err,
size_t err_len) {
if (json == NULL || out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid args");
}
return ESP_ERR_INVALID_ARG;
}
*out_width = 0;
*out_height = 0;
*out_raster = NULL;
*out_len = 0;
cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width");
cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height");
cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding");
cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data");
if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) ||
!cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "missing required image fields");
}
return ESP_ERR_INVALID_ARG;
}
uint16_t width = (uint16_t)jwidth->valuedouble;
uint16_t height = (uint16_t)jheight->valuedouble;
if (width == 0 || height == 0) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid image size");
}
return ESP_ERR_INVALID_ARG;
}
if (strcmp(jencoding->valuestring, "base64_msb_1bpp") == 0) {
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) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid base64");
}
return ESP_ERR_INVALID_ARG;
}
*out_width = width;
*out_height = height;
*out_raster = raw;
*out_len = raw_len;
return ESP_OK;
}
if (strcmp(jencoding->valuestring, "base64_gray8") == 0) {
uint8_t *gray = NULL;
size_t gray_len = 0;
esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &gray, &gray_len);
if (b64_rc != ESP_OK) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid base64");
}
return ESP_ERR_INVALID_ARG;
}
size_t expected = (size_t)width * height;
if (gray_len != expected) {
free(gray);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "gray8 size mismatch");
}
return ESP_ERR_INVALID_SIZE;
}
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");
bool scale_to_width = rest_server_json_bool_with_default(jscale, true);
bool invert = rest_server_json_bool_with_default(jinvert, false);
uint8_t threshold = 160;
uint16_t max_height = 2200;
if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) {
threshold = (uint8_t)jthreshold->valuedouble;
}
if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) {
max_height = (uint16_t)jmaxh->valuedouble;
}
uint16_t out_w = 0;
uint16_t out_h = 0;
uint8_t *raster = NULL;
size_t raster_len = 0;
esp_err_t conv_rc = raster_tools_convert_gray8_to_raster_384(gray,
width,
height,
scale_to_width,
threshold,
invert,
max_height,
&out_w,
&out_h,
&raster,
&raster_len,
err,
err_len);
free(gray);
if (conv_rc != ESP_OK) {
return conv_rc;
}
*out_width = out_w;
*out_height = out_h;
*out_raster = raster;
*out_len = raster_len;
return ESP_OK;
}
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "unsupported encoding");
}
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t rest_server_print_qr_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 *jecc = cJSON_GetObjectItemCaseSensitive(json, "ecc");
cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "module_scale");
cJSON *jmargin = cJSON_GetObjectItemCaseSensitive(json, "margin_modules");
cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height");
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
if (!cJSON_IsString(jtext) || jtext->valuestring == NULL || jtext->valuestring[0] == '\0') {
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 ecc_level = 3; /* default H */
if (cJSON_IsString(jecc) && jecc->valuestring != NULL) {
if (strcasecmp(jecc->valuestring, "L") == 0) {
ecc_level = 0;
} else if (strcasecmp(jecc->valuestring, "M") == 0) {
ecc_level = 1;
} else if (strcasecmp(jecc->valuestring, "Q") == 0) {
ecc_level = 2;
} else if (strcasecmp(jecc->valuestring, "H") == 0) {
ecc_level = 3;
}
}
uint8_t module_scale = 0;
uint8_t margin_modules = 2;
uint16_t max_height = 2200;
if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 0 && jscale->valuedouble <= 16) {
module_scale = (uint8_t)jscale->valuedouble;
}
if (cJSON_IsNumber(jmargin) && jmargin->valuedouble >= 1 && jmargin->valuedouble <= 12) {
margin_modules = (uint8_t)jmargin->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 qr_rc = raster_tools_render_qr_384(jtext->valuestring,
module_scale,
margin_modules,
ecc_level,
max_height,
&width,
&height,
&raster,
&raster_len,
render_msg,
sizeof(render_msg));
cJSON_Delete(json);
if (qr_rc != ESP_OK) {
return rest_server_send_error(req,
"400 Bad Request",
render_msg[0] != '\0' ? render_msg : "qr 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,
NULL,
NULL);
free(raster);
return submit_rc;
}
esp_err_t rest_server_print_label_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 *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
cJSON *jgap = cJSON_GetObjectItemCaseSensitive(json, "gap_move_before");
cJSON *joffset = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm");
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
bool gap_move_before = rest_server_json_bool_with_default(jgap, true);
bool has_offset = cJSON_IsNumber(joffset);
uint8_t offset_value = 0;
if (has_offset) {
if (joffset->valuedouble < 0 || joffset->valuedouble > 254) {
cJSON_Delete(json);
return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254");
}
offset_value = (uint8_t)joffset->valuedouble;
}
const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL)
? jdensity->valuestring
: "中等";
bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false);
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 = decode_image_json_to_raster(json,
&width,
&height,
&raster,
&raster_len,
decode_err,
sizeof(decode_err));
cJSON_Delete(json);
if (decode_rc != ESP_OK) {
return rest_server_send_error(req,
"400 Bad Request",
decode_err[0] != '\0' ? decode_err : "decode image failed");
}
if (has_offset) {
char cmd_err[128] = {0};
esp_err_t offset_rc = printer_protocol_set_label_offset(offset_value,
domain_policy_rest_label_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (offset_rc != ESP_OK) {
free(raster);
return rest_server_send_error(req,
"409 Conflict",
cmd_err[0] != '\0' ? cmd_err : "set label offset failed");
}
}
if (gap_move_before) {
char cmd_err[128] = {0};
esp_err_t gap_rc = printer_protocol_gap_move(domain_policy_rest_label_timeout_ms(),
cmd_err,
sizeof(cmd_err));
if (gap_rc != ESP_OK) {
free(raster);
return rest_server_send_error(req,
"409 Conflict",
cmd_err[0] != '\0' ? cmd_err : "gap move failed");
}
}
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;
}
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;
}
esp_err_t rest_server_print_receipt_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 *jtitle = cJSON_GetObjectItemCaseSensitive(json, "title");
cJSON *jitems = cJSON_GetObjectItemCaseSensitive(json, "items");
cJSON *jfooter = cJSON_GetObjectItemCaseSensitive(json, "footer");
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale");
cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck");
const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL)
? jtitle->valuestring
: "TQ RECEIPT";
const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL)
? jfooter->valuestring
: "Thank you";
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;
if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 4) {
scale = (uint8_t)jscale->valuedouble;
}
size_t text_cap = 8192;
char *text = (char *)calloc(1, text_cap);
if (text == NULL) {
cJSON_Delete(json);
return rest_server_send_error(req, "500 Internal Server Error", "no memory");
}
size_t off = 0;
rest_server_appendf(text, text_cap, &off, "%s\n", title);
rest_server_appendf(text, text_cap, &off, "--------------------------------\n");
double total = 0.0;
if (cJSON_IsArray(jitems)) {
int n = cJSON_GetArraySize(jitems);
for (int i = 0; i < n; ++i) {
cJSON *it = cJSON_GetArrayItem(jitems, i);
if (!cJSON_IsObject(it)) {
continue;
}
cJSON *jname = cJSON_GetObjectItemCaseSensitive(it, "name");
cJSON *jqty = cJSON_GetObjectItemCaseSensitive(it, "qty");
cJSON *jprice = cJSON_GetObjectItemCaseSensitive(it, "price");
const char *name = (cJSON_IsString(jname) && jname->valuestring != NULL)
? jname->valuestring
: "item";
int qty = cJSON_IsNumber(jqty) ? (int)jqty->valuedouble : 1;
double price = cJSON_IsNumber(jprice) ? jprice->valuedouble : 0.0;
double line_total = qty * price;
total += line_total;
rest_server_appendf(text,
text_cap,
&off,
"%-16.16s x%-3d %8.2f\n",
name,
qty,
line_total);
}
} else {
rest_server_appendf(text, text_cap, &off, "(no items)\n");
}
rest_server_appendf(text, text_cap, &off, "--------------------------------\n");
rest_server_appendf(text, text_cap, &off, "TOTAL: %.2f\n", total);
rest_server_appendf(text, text_cap, &off, "%s\n", footer);
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(text,
scale,
2,
2200,
&width,
&height,
&raster,
&raster_len,
render_msg,
sizeof(render_msg));
free(text);
cJSON_Delete(json);
if (render_rc != ESP_OK) {
return rest_server_send_error(req,
"400 Bad Request",
render_msg[0] != '\0' ? render_msg : "receipt 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;
}
@@ -0,0 +1,213 @@
#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 canceled");
rest_server_add_voice_status(root);
esp_err_t err = rest_server_send_json(req, "200 OK", root);
cJSON_Delete(root);
return err;
}