#include "domain.h" #include "printer_protocol_internal.h" #include #include #include #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" SemaphoreHandle_t s_mutex; QueueHandle_t s_job_queue; EventGroupHandle_t s_evt; uint32_t s_busy_refcnt; uint32_t s_status_poll_pause_next_token; uint32_t s_status_poll_pause_tokens[STATUS_POLL_PAUSE_SLOT_MAX]; parsed_status_t s_status = { .has_paper = true, .paper_gpio_level = -1, .paper_present_level = 0, .battery = 100, .temperature = 25.0f, .updated_ms = 0, }; uint8_t s_last_rsp_cmd; uint16_t s_last_rsp_payload_len; uint8_t s_last_rsp_payload[252]; uint32_t s_next_job_id = 1; job_slot_t s_jobs[JOB_SLOT_MAX]; TaskHandle_t s_worker_task; TaskHandle_t s_status_poll_task_handle; bool s_protocol_initialized; bool s_protocol_stopping; printer_backend_t s_backend = PRINTER_BACKEND_BLE; bool s_ble_client_initialized; static const char *TAG = "printer_protocol"; #if CONFIG_FREERTOS_UNICORE #define PRINT_WORKER_CORE_ID 0 #else #define PRINT_WORKER_CORE_ID 1 #endif static void on_rx_frame(const uint8_t *data, size_t len); static printer_backend_t default_backend_on_startup(void) { // This hardware has a first-class direct path; BLE remains opt-in via API. if (runtime_policy_direct_printer_enabled()) { return PRINTER_BACKEND_DIRECT; } return PRINTER_BACKEND_BLE; } static void backend_capabilities_for(printer_backend_t backend, printer_capabilities_t *out_caps) { if (out_caps == NULL) { return; } memset(out_caps, 0, sizeof(*out_caps)); out_caps->supports_connect = true; out_caps->supports_gap_move = true; if (backend == PRINTER_BACKEND_BLE) { out_caps->supports_label_offset = true; out_caps->supports_ota = true; } } static esp_err_t ensure_ble_client_ready(void) { if (s_ble_client_initialized) { return ESP_OK; } esp_err_t err = ble_printer_client_init(on_rx_frame); if (err == ESP_OK) { s_ble_client_initialized = true; } return err; } static esp_err_t ensure_backend_ready(printer_backend_t backend) { if (backend == PRINTER_BACKEND_BLE) { return ensure_ble_client_ready(); } if (backend == PRINTER_BACKEND_DIRECT) { if (!runtime_policy_direct_printer_enabled()) { return ESP_ERR_NOT_SUPPORTED; } return platform_direct_printer_init(); } return ESP_ERR_NOT_SUPPORTED; } static void refresh_direct_status_locked(void) { if (s_backend != PRINTER_BACKEND_DIRECT) { return; } platform_printer_sensors_t sensors = {0}; if (platform_direct_printer_get_sensors(&sensors) == ESP_OK) { s_status.has_paper = sensors.has_paper; s_status.paper_gpio_level = sensors.paper_gpio_level; s_status.paper_present_level = sensors.paper_present_level; s_status.battery = sensors.battery_percent; s_status.temperature = sensors.temperature_c; s_status.updated_ms = sensors.updated_ms; } } static size_t status_poll_pause_depth_locked(void) { size_t depth = 0; for (size_t i = 0; i < STATUS_POLL_PAUSE_SLOT_MAX; ++i) { if (s_status_poll_pause_tokens[i] != 0) { ++depth; } } return depth; } static void status_poll_pause_slots_clear_locked(void) { memset(s_status_poll_pause_tokens, 0, sizeof(s_status_poll_pause_tokens)); } static void reset_jobs_locked(void) { for (int i = 0; i < JOB_SLOT_MAX; ++i) { free(s_jobs[i].data); memset(&s_jobs[i], 0, sizeof(s_jobs[i])); } s_next_job_id = 1; } static void reset_runtime_state_locked(void) { s_busy_refcnt = 0; s_last_rsp_cmd = 0; s_last_rsp_payload_len = 0; memset(s_last_rsp_payload, 0, sizeof(s_last_rsp_payload)); status_poll_pause_slots_clear_locked(); s_status_poll_pause_next_token = 1; s_status.has_paper = true; s_status.paper_gpio_level = -1; s_status.paper_present_level = 0; s_status.battery = 100; s_status.temperature = 25.0f; s_status.updated_ms = 0; } bool printer_protocol_is_stopping(void) { if (!s_protocol_initialized) { return false; } if (s_mutex == NULL) { return true; } bool stopping = true; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { stopping = s_protocol_stopping; xSemaphoreGive(s_mutex); } return stopping; } static BaseType_t printer_create_task_prefer_psram(TaskFunction_t task_fn, const char *name, uint32_t stack_size, UBaseType_t priority, TaskHandle_t *out_task) { #if defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) && \ defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && \ (configSUPPORT_STATIC_ALLOCATION == 1) BaseType_t rc = xTaskCreatePinnedToCoreWithCaps(task_fn, name, (configSTACK_DEPTH_TYPE)stack_size, NULL, priority, out_task, PRINT_WORKER_CORE_ID, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (rc == pdPASS) { return pdPASS; } #endif return xTaskCreatePinnedToCore(task_fn, name, (configSTACK_DEPTH_TYPE)stack_size, NULL, priority, out_task, PRINT_WORKER_CORE_ID); } printer_status_poll_pause_token_t printer_protocol_status_poll_pause_acquire(void) { if (!s_protocol_initialized || s_mutex == NULL) { return 0; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { return 0; } if (s_protocol_stopping) { xSemaphoreGive(s_mutex); return 0; } int slot = -1; for (int i = 0; i < STATUS_POLL_PAUSE_SLOT_MAX; ++i) { if (s_status_poll_pause_tokens[i] == 0) { slot = i; break; } } if (slot < 0) { xSemaphoreGive(s_mutex); ESP_LOGW(TAG, "status poll pause acquire failed: no free slot"); return 0; } uint32_t token = s_status_poll_pause_next_token++; if (token == 0) { token = s_status_poll_pause_next_token++; } s_status_poll_pause_tokens[slot] = token; runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, (int32_t)status_poll_pause_depth_locked()); xSemaphoreGive(s_mutex); return token; } void printer_protocol_status_poll_pause_release(printer_status_poll_pause_token_t *token) { if (token == NULL || *token == 0) { return; } if (!s_protocol_initialized || s_mutex == NULL) { *token = 0; return; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { return; } for (int i = 0; i < STATUS_POLL_PAUSE_SLOT_MAX; ++i) { if (s_status_poll_pause_tokens[i] == *token) { s_status_poll_pause_tokens[i] = 0; break; } } runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, (int32_t)status_poll_pause_depth_locked()); xSemaphoreGive(s_mutex); *token = 0; } bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len) { if (!s_protocol_initialized || s_mutex == NULL) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer protocol not initialized"); } return false; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "lock timeout"); } return false; } if (s_protocol_stopping) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer protocol stopping"); } return false; } if (s_busy_refcnt != 0) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer busy"); } return false; } s_busy_refcnt = 1; xSemaphoreGive(s_mutex); return true; } void printer_protocol_release_control_lane(void) { if (s_mutex == NULL) { return; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { return; } if (s_busy_refcnt > 0) { --s_busy_refcnt; } xSemaphoreGive(s_mutex); } static uint8_t checksum8(const uint8_t *data, size_t len) { uint32_t sum = 0; for (size_t i = 0; i < len; ++i) { sum += data[i]; } return (uint8_t)(sum & 0xFF); } esp_err_t printer_protocol_send_frame(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum) { if (!s_protocol_initialized || printer_protocol_is_stopping()) { return ESP_ERR_INVALID_STATE; } if (s_backend != PRINTER_BACKEND_BLE) { return ESP_ERR_NOT_SUPPORTED; } uint8_t frame[260]; size_t total = 4 + payload_len + (with_checksum ? 1 : 0); if (total > sizeof(frame)) { return ESP_ERR_INVALID_SIZE; } frame[0] = PROTO_ADDR; frame[1] = cmd; frame[2] = (uint8_t)((payload_len >> 8) & 0xFF); frame[3] = (uint8_t)(payload_len & 0xFF); if (payload_len > 0 && payload != NULL) { memcpy(&frame[4], payload, payload_len); } if (with_checksum) { frame[4 + payload_len] = checksum8(frame, 4 + payload_len); } return ble_printer_client_write(frame, total); } static void clear_ack_signal(void) { if (s_evt != NULL) { xEventGroupClearBits(s_evt, EVT_ACK); } } bool printer_protocol_wait_response(uint8_t cmd, uint8_t *out_payload, size_t out_payload_cap, uint16_t *out_payload_len, uint32_t timeout_ms) { if (timeout_ms == 0 || s_evt == NULL) { return false; } int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; while (true) { if (printer_protocol_is_stopping()) { return false; } int64_t now = esp_timer_get_time() / 1000; if (now >= deadline) { return false; } uint32_t wait_ms = (uint32_t)(deadline - now); if (wait_ms > 100) { wait_ms = 100; } EventBits_t bits = xEventGroupWaitBits(s_evt, EVT_ACK | EVT_SHUTDOWN, pdFALSE, pdFALSE, pdMS_TO_TICKS(wait_ms)); if (bits & EVT_SHUTDOWN) { return false; } if (!(bits & EVT_ACK)) { continue; } xEventGroupClearBits(s_evt, EVT_ACK); if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { continue; } uint8_t rsp_cmd = s_last_rsp_cmd; uint16_t rsp_len = s_last_rsp_payload_len; uint8_t rsp_copy[sizeof(s_last_rsp_payload)]; if (rsp_len > sizeof(rsp_copy)) { rsp_len = sizeof(rsp_copy); } if (rsp_len > 0) { memcpy(rsp_copy, s_last_rsp_payload, rsp_len); } xSemaphoreGive(s_mutex); if (rsp_cmd == cmd) { if (out_payload != NULL && out_payload_cap > 0 && rsp_len > 0) { size_t copy_len = rsp_len; if (copy_len > out_payload_cap) { copy_len = out_payload_cap; } memcpy(out_payload, rsp_copy, copy_len); } if (out_payload_len != NULL) { *out_payload_len = rsp_len; } return true; } } } static bool wait_ack(uint8_t cmd, uint32_t timeout_ms) { uint8_t payload[252]; uint16_t payload_len = 0; if (!printer_protocol_wait_response(cmd, payload, sizeof(payload), &payload_len, timeout_ms)) { return false; } return payload_len >= 1 && payload[0] == 0x01; } static void reset_transport_after_timeout(void) { if (printer_protocol_is_stopping()) { return; } if (printer_protocol_get_backend() == PRINTER_BACKEND_BLE) { ble_printer_client_disconnect(); } } bool printer_protocol_send_cmd_with_ack(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum, uint32_t timeout_ms, bool reset_transport_on_timeout) { clear_ack_signal(); if (printer_protocol_send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { return false; } bool ok = wait_ack(cmd, timeout_ms); if (!ok && reset_transport_on_timeout) { reset_transport_after_timeout(); } return ok; } esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum, uint32_t timeout_ms, bool expect_ack, bool reset_transport_on_timeout, uint8_t *out_payload, size_t out_payload_cap, uint16_t *out_payload_len, char *err, size_t err_len) { clear_ack_signal(); if (printer_protocol_send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "send frame failed"); } return ESP_FAIL; } uint8_t rsp[252]; uint16_t rsp_len = 0; if (!printer_protocol_wait_response(cmd, rsp, sizeof(rsp), &rsp_len, timeout_ms)) { bool stopping = printer_protocol_is_stopping(); if (!stopping && reset_transport_on_timeout) { reset_transport_after_timeout(); } if (err != NULL && err_len > 0) { if (stopping) { snprintf(err, err_len, "protocol stopping"); } else { snprintf(err, err_len, reset_transport_on_timeout ? "response timeout, transport reset" : "response timeout"); } } return stopping ? ESP_ERR_INVALID_STATE : ESP_ERR_TIMEOUT; } if (expect_ack && (rsp_len < 1 || rsp[0] != 0x01)) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "cmd 0x%02X rejected", cmd); } return ESP_FAIL; } if (out_payload != NULL && out_payload_cap > 0 && rsp_len > 0) { size_t copy_len = rsp_len; if (copy_len > out_payload_cap) { copy_len = out_payload_cap; } memcpy(out_payload, rsp, copy_len); } if (out_payload_len != NULL) { *out_payload_len = rsp_len; } return ESP_OK; } static void status_poll_task(void *arg) { (void)arg; while (true) { bool should_stop = false; bool can_poll = false; printer_backend_t backend = PRINTER_BACKEND_BLE; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { should_stop = s_protocol_stopping; can_poll = (s_busy_refcnt == 0) && (status_poll_pause_depth_locked() == 0); backend = s_backend; xSemaphoreGive(s_mutex); } if (should_stop) { break; } if (can_poll) { if (backend == PRINTER_BACKEND_BLE) { if (ble_printer_client_is_connected()) { uint8_t dummy = 0x00; (void)printer_protocol_send_frame(CMD_GET_STATUS, &dummy, 0, true); } } else if (backend == PRINTER_BACKEND_DIRECT) { if (platform_direct_printer_is_connected() && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { refresh_direct_status_locked(); xSemaphoreGive(s_mutex); } } } vTaskDelay(pdMS_TO_TICKS(runtime_policy_printer_status_poll_interval_ms())); } if (s_evt != NULL) { xEventGroupSetBits(s_evt, EVT_STATUS_POLL_EXITED); } s_status_poll_task_handle = NULL; vTaskDelete(NULL); } static void on_rx_frame(const uint8_t *data, size_t len) { if (data == NULL || len < 4 || s_mutex == NULL) { return; } if (data[0] != PROTO_ADDR) { return; } uint8_t cmd = data[1]; uint16_t payload_len = ((uint16_t)data[2] << 8) | data[3]; size_t required = (size_t)payload_len + 4; if (len < required) { return; } const uint8_t *payload = &data[4]; if (cmd == CMD_GET_STATUS && payload_len >= 5) { bool has_paper = payload[0] != 0; uint8_t battery = payload[1]; int sign = (payload[2] == 0x2D) ? -1 : 1; int temp_x10 = ((int)payload[3] << 8) | payload[4]; float temperature = (float)(sign * temp_x10) / 10.0f; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { s_status.has_paper = has_paper; s_status.paper_gpio_level = -1; s_status.paper_present_level = 0; s_status.battery = battery; s_status.temperature = temperature; s_status.updated_ms = esp_timer_get_time() / 1000; xSemaphoreGive(s_mutex); } return; } if (payload_len >= 1) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { s_last_rsp_cmd = cmd; s_last_rsp_payload_len = payload_len; if (s_last_rsp_payload_len > sizeof(s_last_rsp_payload)) { s_last_rsp_payload_len = sizeof(s_last_rsp_payload); } memcpy(s_last_rsp_payload, payload, s_last_rsp_payload_len); xSemaphoreGive(s_mutex); } if (s_evt != NULL) { xEventGroupSetBits(s_evt, EVT_ACK); } } } static void destroy_runtime_objects(void) { if (s_job_queue != NULL) { vQueueDelete(s_job_queue); s_job_queue = NULL; } if (s_evt != NULL) { vEventGroupDelete(s_evt); s_evt = NULL; } if (s_mutex != NULL) { vSemaphoreDelete(s_mutex); s_mutex = NULL; } } esp_err_t printer_protocol_init(void) { if (s_protocol_initialized) { return ESP_OK; } s_mutex = xSemaphoreCreateMutex(); if (s_mutex == NULL) { return ESP_ERR_NO_MEM; } s_evt = xEventGroupCreate(); if (s_evt == NULL) { destroy_runtime_objects(); return ESP_ERR_NO_MEM; } s_job_queue = xQueueCreate(JOB_QUEUE_LEN, sizeof(uint32_t)); if (s_job_queue == NULL) { destroy_runtime_objects(); return ESP_ERR_NO_MEM; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { reset_jobs_locked(); reset_runtime_state_locked(); s_protocol_stopping = false; xSemaphoreGive(s_mutex); } runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, 0); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, 0); s_protocol_initialized = true; s_protocol_stopping = false; s_ble_client_initialized = false; s_backend = default_backend_on_startup(); esp_err_t err = ensure_backend_ready(s_backend); if (err != ESP_OK) { s_protocol_initialized = false; destroy_runtime_objects(); return err; } BaseType_t ok = printer_create_task_prefer_psram(printer_protocol_worker_task, "print_worker", 6144, 6, &s_worker_task); if (ok != pdPASS) { s_protocol_initialized = false; destroy_runtime_objects(); return ESP_ERR_NO_MEM; } ok = printer_create_task_prefer_psram(status_poll_task, "status_poll", 4096, 4, &s_status_poll_task_handle); if (ok != pdPASS) { if (s_worker_task != NULL) { vTaskDelete(s_worker_task); s_worker_task = NULL; } s_protocol_initialized = false; destroy_runtime_objects(); return ESP_ERR_NO_MEM; } ESP_LOGI(TAG, "printer protocol initialized"); return ESP_OK; } esp_err_t printer_protocol_stop(uint32_t timeout_ms) { if (!s_protocol_initialized) { return ESP_OK; } if (timeout_ms == 0) { timeout_ms = runtime_policy_printer_stop_timeout_ms(); } if (s_mutex != NULL && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) { s_protocol_stopping = true; s_busy_refcnt = 0; status_poll_pause_slots_clear_locked(); xSemaphoreGive(s_mutex); } runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, 0); if (s_evt != NULL) { xEventGroupSetBits(s_evt, EVT_SHUTDOWN); } if (s_job_queue != NULL) { const uint32_t sentinel = PRINTER_JOB_SENTINEL_STOP; (void)xQueueSend(s_job_queue, &sentinel, 0); } ble_printer_client_disconnect(); platform_direct_printer_disconnect(); bool worker_done = (s_worker_task == NULL); bool poll_done = (s_status_poll_task_handle == NULL); if (s_evt != NULL && (!worker_done || !poll_done)) { EventBits_t bits = xEventGroupWaitBits(s_evt, EVT_WORKER_EXITED | EVT_STATUS_POLL_EXITED, pdFALSE, pdTRUE, pdMS_TO_TICKS(timeout_ms)); worker_done = worker_done || ((bits & EVT_WORKER_EXITED) != 0) || s_worker_task == NULL; poll_done = poll_done || ((bits & EVT_STATUS_POLL_EXITED) != 0) || s_status_poll_task_handle == NULL; } if (!worker_done && s_worker_task != NULL) { ESP_LOGW(TAG, "worker task stop timeout, force delete"); vTaskDelete(s_worker_task); s_worker_task = NULL; } if (!poll_done && s_status_poll_task_handle != NULL) { ESP_LOGW(TAG, "status poll task stop timeout, force delete"); vTaskDelete(s_status_poll_task_handle); s_status_poll_task_handle = NULL; } if (s_mutex != NULL && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { reset_jobs_locked(); reset_runtime_state_locked(); xSemaphoreGive(s_mutex); } destroy_runtime_objects(); s_busy_refcnt = 0; s_status_poll_pause_next_token = 1; memset(s_status_poll_pause_tokens, 0, sizeof(s_status_poll_pause_tokens)); memset(s_last_rsp_payload, 0, sizeof(s_last_rsp_payload)); s_last_rsp_cmd = 0; s_last_rsp_payload_len = 0; s_worker_task = NULL; s_status_poll_task_handle = NULL; s_protocol_initialized = false; s_protocol_stopping = false; s_ble_client_initialized = false; s_backend = default_backend_on_startup(); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, 0); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, 0); platform_direct_printer_deinit(); ESP_LOGI(TAG, "printer protocol stopped"); return (worker_done && poll_done) ? ESP_OK : ESP_ERR_TIMEOUT; } esp_err_t printer_protocol_set_backend(printer_backend_t backend, char *err, size_t err_len) { if (backend != PRINTER_BACKEND_BLE && backend != PRINTER_BACKEND_DIRECT) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid backend"); } return ESP_ERR_INVALID_ARG; } if (backend == PRINTER_BACKEND_DIRECT && !runtime_policy_direct_printer_enabled()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "direct backend disabled"); } return ESP_ERR_NOT_SUPPORTED; } if (!s_protocol_initialized || s_protocol_stopping || s_mutex == NULL) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer protocol unavailable"); } return ESP_ERR_INVALID_STATE; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(runtime_policy_printer_control_lock_timeout_ms())) != pdTRUE) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "lock timeout"); } return ESP_ERR_TIMEOUT; } printer_backend_t old_backend = s_backend; if (old_backend == backend) { xSemaphoreGive(s_mutex); return ESP_OK; } if (s_busy_refcnt != 0 || (s_job_queue != NULL && uxQueueMessagesWaiting(s_job_queue) > 0)) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer busy"); } return ESP_ERR_INVALID_STATE; } s_backend = backend; xSemaphoreGive(s_mutex); ble_printer_client_disconnect(); platform_direct_printer_disconnect(); esp_err_t rc = ensure_backend_ready(backend); if (rc != ESP_OK) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { s_backend = old_backend; xSemaphoreGive(s_mutex); } (void)ensure_backend_ready(old_backend); if (err != NULL && err_len > 0) { snprintf(err, err_len, "backend init failed"); } return rc; } if (backend == PRINTER_BACKEND_DIRECT && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { refresh_direct_status_locked(); xSemaphoreGive(s_mutex); } return ESP_OK; } printer_backend_t printer_protocol_get_backend(void) { if (!s_protocol_initialized || s_mutex == NULL) { return s_backend; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { return s_backend; } printer_backend_t backend = s_backend; xSemaphoreGive(s_mutex); return backend; } esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms) { printer_connect_options_t opt = { .backend = printer_protocol_get_backend(), .name = target_name, .timeout_ms = timeout_ms, }; return printer_protocol_connect_ex(&opt, NULL, 0); } esp_err_t printer_protocol_connect_ex(const printer_connect_options_t *opt, char *err, size_t err_len) { if (!s_protocol_initialized || s_protocol_stopping) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer protocol unavailable"); } return ESP_ERR_INVALID_STATE; } printer_backend_t backend = printer_protocol_get_backend(); const char *name = NULL; uint32_t timeout_ms = 0; if (opt != NULL) { backend = opt->backend; name = opt->name; timeout_ms = opt->timeout_ms; } esp_err_t rc = printer_protocol_set_backend(backend, err, err_len); if (rc != ESP_OK) { return rc; } if (backend == PRINTER_BACKEND_BLE) { rc = ensure_backend_ready(PRINTER_BACKEND_BLE); if (rc != ESP_OK) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "ble backend unavailable"); } return rc; } return ble_printer_client_connect(name, timeout_ms); } if (backend == PRINTER_BACKEND_DIRECT) { rc = ensure_backend_ready(PRINTER_BACKEND_DIRECT); if (rc != ESP_OK) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "direct backend unavailable"); } return rc; } return platform_direct_printer_connect(timeout_ms); } if (err != NULL && err_len > 0) { snprintf(err, err_len, "unsupported backend"); } return ESP_ERR_NOT_SUPPORTED; } void printer_protocol_disconnect(void) { ble_printer_client_disconnect(); platform_direct_printer_disconnect(); } void printer_protocol_get_capabilities(printer_capabilities_t *out_capabilities) { if (out_capabilities == NULL) { return; } backend_capabilities_for(printer_protocol_get_backend(), out_capabilities); } void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { if (out_status == NULL) { return; } memset(out_status, 0, sizeof(*out_status)); out_status->paper_gpio_level = -1; out_status->backend = printer_protocol_get_backend(); printer_capabilities_t caps = {0}; backend_capabilities_for(out_status->backend, &caps); out_status->supports_gap_move = caps.supports_gap_move; out_status->supports_label_offset = caps.supports_label_offset; out_status->supports_ota = caps.supports_ota; if (out_status->backend == PRINTER_BACKEND_BLE) { ble_link_state_t link = {0}; ble_printer_client_get_link_state(&link); out_status->connected = link.connected; out_status->notify_ready = link.notify_ready; out_status->transport_ready = link.connected && link.notify_ready; out_status->mtu = link.mtu; } else if (out_status->backend == PRINTER_BACKEND_DIRECT) { out_status->connected = platform_direct_printer_is_connected(); out_status->transport_ready = out_status->connected; out_status->notify_ready = false; out_status->mtu = 0; platform_printer_sensors_t sensors = {0}; if (platform_direct_printer_get_sensors(&sensors) == ESP_OK && s_mutex != NULL && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { s_status.has_paper = sensors.has_paper; s_status.paper_gpio_level = sensors.paper_gpio_level; s_status.paper_present_level = sensors.paper_present_level; s_status.battery = sensors.battery_percent; s_status.temperature = sensors.temperature_c; s_status.updated_ms = sensors.updated_ms; xSemaphoreGive(s_mutex); } } if (s_mutex != NULL && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { out_status->busy = s_busy_refcnt > 0; out_status->has_paper = s_status.has_paper; out_status->paper_gpio_level = s_status.paper_gpio_level; out_status->paper_present_level = s_status.paper_present_level; out_status->battery_percent = s_status.battery; out_status->temperature = s_status.temperature; out_status->last_status_ms = s_status.updated_ms; out_status->queue_depth = (s_job_queue != NULL) ? (uint32_t)uxQueueMessagesWaiting(s_job_queue) : 0; xSemaphoreGive(s_mutex); } runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, (int32_t)out_status->queue_depth); }