#include "printer_protocol.h" #include "printer_protocol_internal.h" #include #include #include #include #include "ble_printer_client.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/queue.h" #include "freertos/semphr.h" #include "freertos/task.h" #define PROTO_ADDR 0x01 #define CMD_POWER 0x00 #define CMD_GET_STATUS 0x01 #define CMD_SET_DISTANCE 0x02 #define CMD_SET_PARAM 0x03 #define CMD_SEND_DATA 0x04 #define EVT_ACK BIT0 #define JOB_QUEUE_LEN 8 #define JOB_SLOT_MAX 16 #define MAX_RASTER_BYTES (384 * 3000 / 8) typedef struct { bool used; bool cancel_requested; uint32_t id; print_job_state_t state; uint8_t progress; uint16_t width; uint16_t height; size_t data_len; char density[16]; char error[96]; int64_t created_ms; int64_t started_ms; int64_t finished_ms; uint8_t *data; } job_slot_t; typedef struct { bool has_paper; uint8_t battery; float temperature; int64_t updated_ms; } parsed_status_t; static SemaphoreHandle_t s_mutex; static QueueHandle_t s_job_queue; static EventGroupHandle_t s_evt; static uint32_t s_busy_refcnt; static parsed_status_t s_status = { .has_paper = true, .battery = 100, .temperature = 25.0f, .updated_ms = 0, }; static uint8_t s_last_rsp_cmd; static uint16_t s_last_rsp_payload_len; static uint8_t s_last_rsp_payload[252]; static uint32_t s_next_job_id = 1; static job_slot_t s_jobs[JOB_SLOT_MAX]; static int find_job_idx_locked(uint32_t id) { for (int i = 0; i < JOB_SLOT_MAX; ++i) { if (s_jobs[i].used && s_jobs[i].id == id) { return i; } } return -1; } static int alloc_job_slot_locked(void) { for (int i = 0; i < JOB_SLOT_MAX; ++i) { if (!s_jobs[i].used) { return i; } } /* Reuse completed slot when table is full. */ for (int i = 0; i < JOB_SLOT_MAX; ++i) { if (s_jobs[i].state == PRINT_JOB_STATE_SUCCESS || s_jobs[i].state == PRINT_JOB_STATE_FAILED || s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { free(s_jobs[i].data); memset(&s_jobs[i], 0, sizeof(s_jobs[i])); return i; } } return -1; } static bool is_terminal_state(print_job_state_t state) { return state == PRINT_JOB_STATE_SUCCESS || state == PRINT_JOB_STATE_FAILED || state == PRINT_JOB_STATE_CANCELED; } bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len) { 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_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 (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); } static esp_err_t send_frame(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum) { 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) { xEventGroupClearBits(s_evt, EVT_ACK); } static bool wait_response(uint8_t cmd, uint8_t *out_payload, size_t out_payload_cap, uint16_t *out_payload_len, uint32_t timeout_ms) { int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; while (true) { int64_t now = esp_timer_get_time() / 1000; if (now >= deadline) { return false; } uint32_t wait_ms = (uint32_t)(deadline - now); EventBits_t bits = xEventGroupWaitBits(s_evt, EVT_ACK, pdTRUE, pdFALSE, pdMS_TO_TICKS(wait_ms)); if (!(bits & EVT_ACK)) { return false; } 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 (!wait_response(cmd, payload, sizeof(payload), &payload_len, timeout_ms)) { return false; } return payload_len >= 1 && payload[0] == 0x01; } static bool send_cmd_with_ack(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum, uint32_t timeout_ms) { clear_ack_signal(); if (send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { return false; } return wait_ack(cmd, timeout_ms); } 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, uint8_t *out_payload, size_t out_payload_cap, uint16_t *out_payload_len, char *err, size_t err_len) { clear_ack_signal(); if (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 (!wait_response(cmd, rsp, sizeof(rsp), &rsp_len, timeout_ms)) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "response timeout"); } return 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 bool request_status_sync(uint32_t timeout_ms) { int64_t old_ms; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { return false; } old_ms = s_status.updated_ms; xSemaphoreGive(s_mutex); uint8_t payload = 0x00; if (send_frame(CMD_GET_STATUS, &payload, 0, true) != ESP_OK) { return false; } int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; while ((esp_timer_get_time() / 1000) < deadline) { vTaskDelay(pdMS_TO_TICKS(20)); if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { continue; } bool updated = s_status.updated_ms > old_ms; xSemaphoreGive(s_mutex); if (updated) { return true; } } return false; } static uint16_t density_to_hot_time(const char *density) { if (density == NULL) { return 2000; } if (strcmp(density, "较淡") == 0) { return 1000; } if (strcmp(density, "中等") == 0) { return 1500; } if (strcmp(density, "较浓") == 0) { return 2000; } if (strcmp(density, "最深") == 0) { return 3000; } return 2000; } static bool precheck_printer_ready(char *err, size_t err_len) { if (!request_status_sync(1200)) { snprintf(err, err_len, "status timeout"); return false; } parsed_status_t status; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { snprintf(err, err_len, "status lock failed"); return false; } status = s_status; xSemaphoreGive(s_mutex); if (!status.has_paper) { snprintf(err, err_len, "printer out of paper"); return false; } if (status.battery <= 40) { snprintf(err, err_len, "battery too low"); return false; } if (status.temperature >= 60.0f) { snprintf(err, err_len, "temperature too high"); return false; } return true; } static bool run_print_job(job_slot_t *job) { char err[96]; err[0] = '\0'; if (!ble_printer_client_is_connected()) { snprintf(job->error, sizeof(job->error), "printer not connected"); return false; } if (!precheck_printer_ready(err, sizeof(err))) { snprintf(job->error, sizeof(job->error), "%s", err); return false; } if (job->cancel_requested) { snprintf(job->error, sizeof(job->error), "job canceled"); return false; } uint8_t power_on = 0x01; if (!send_cmd_with_ack(CMD_POWER, &power_on, 1, true, 1500)) { snprintf(job->error, sizeof(job->error), "power on failed"); return false; } uint16_t hot_time = density_to_hot_time(job->density); uint8_t param[4] = { 0x01, 0x02, (uint8_t)((hot_time >> 8) & 0xFF), (uint8_t)(hot_time & 0xFF), }; if (!send_cmd_with_ack(CMD_SET_PARAM, param, sizeof(param), true, 1500)) { snprintf(job->error, sizeof(job->error), "set print param failed"); return false; } const size_t chunk_max = 240; size_t total_chunks = (size_t)ceil((double)job->data_len / (double)chunk_max); for (size_t i = 0; i < total_chunks; ++i) { if (job->cancel_requested) { snprintf(job->error, sizeof(job->error), "job canceled"); return false; } size_t start = i * chunk_max; size_t remain = job->data_len - start; size_t chunk_len = remain > chunk_max ? chunk_max : remain; if (!send_cmd_with_ack(CMD_SEND_DATA, &job->data[start], (uint16_t)chunk_len, false, 2500)) { snprintf(job->error, sizeof(job->error), "send chunk timeout at %u", (unsigned)i); return false; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { job->progress = (uint8_t)(((i + 1) * 90) / total_chunks); if (job->progress < 5) { job->progress = 5; } xSemaphoreGive(s_mutex); } } uint8_t power_off = 0x00; (void)send_cmd_with_ack(CMD_POWER, &power_off, 1, true, 1200); uint8_t feed_payload[3] = {0x2B, 0x00, 0x0C}; (void)send_cmd_with_ack(CMD_SET_DISTANCE, feed_payload, sizeof(feed_payload), true, 1200); return true; } static void worker_task(void *arg) { (void)arg; while (true) { uint32_t job_id; if (xQueueReceive(s_job_queue, &job_id, portMAX_DELAY) != pdTRUE) { continue; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { continue; } int idx = find_job_idx_locked(job_id); if (idx < 0) { xSemaphoreGive(s_mutex); continue; } if (s_jobs[idx].state == PRINT_JOB_STATE_CANCELED) { free(s_jobs[idx].data); s_jobs[idx].data = NULL; if (s_jobs[idx].finished_ms == 0) { s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; } xSemaphoreGive(s_mutex); continue; } if (s_busy_refcnt != 0) { xSemaphoreGive(s_mutex); xQueueSendToFront(s_job_queue, &job_id, 0); vTaskDelay(pdMS_TO_TICKS(20)); continue; } s_jobs[idx].state = PRINT_JOB_STATE_RUNNING; s_jobs[idx].started_ms = esp_timer_get_time() / 1000; s_jobs[idx].progress = 1; s_busy_refcnt = 1; xSemaphoreGive(s_mutex); bool ok = run_print_job(&s_jobs[idx]); if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { continue; } if (s_busy_refcnt > 0) { --s_busy_refcnt; } s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; if (s_jobs[idx].state != PRINT_JOB_STATE_CANCELED) { s_jobs[idx].progress = 100; } if (ok) { s_jobs[idx].state = PRINT_JOB_STATE_SUCCESS; s_jobs[idx].error[0] = '\0'; } else if (s_jobs[idx].cancel_requested) { s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; if (s_jobs[idx].error[0] == '\0') { strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); } } else { s_jobs[idx].state = PRINT_JOB_STATE_FAILED; } free(s_jobs[idx].data); s_jobs[idx].data = NULL; xSemaphoreGive(s_mutex); } } static void status_poll_task(void *arg) { (void)arg; while (true) { bool can_poll = false; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { can_poll = (s_busy_refcnt == 0); xSemaphoreGive(s_mutex); } if (ble_printer_client_is_connected() && can_poll) { uint8_t dummy = 0x00; (void)send_frame(CMD_GET_STATUS, &dummy, 0, true); } vTaskDelay(pdMS_TO_TICKS(5000)); } } static void on_rx_frame(const uint8_t *data, size_t len) { if (data == NULL || len < 4) { 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.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); } xEventGroupSetBits(s_evt, EVT_ACK); } } esp_err_t printer_protocol_init(void) { s_mutex = xSemaphoreCreateMutex(); if (s_mutex == NULL) { return ESP_ERR_NO_MEM; } s_evt = xEventGroupCreate(); if (s_evt == NULL) { return ESP_ERR_NO_MEM; } s_job_queue = xQueueCreate(JOB_QUEUE_LEN, sizeof(uint32_t)); if (s_job_queue == NULL) { return ESP_ERR_NO_MEM; } esp_err_t err = ble_printer_client_init(on_rx_frame); if (err != ESP_OK) { return err; } BaseType_t ok = xTaskCreate(worker_task, "print_worker", 6144, NULL, 6, NULL); if (ok != pdPASS) { return ESP_ERR_NO_MEM; } ok = xTaskCreate(status_poll_task, "status_poll", 4096, NULL, 4, NULL); if (ok != pdPASS) { return ESP_ERR_NO_MEM; } return ESP_OK; } esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms) { return ble_printer_client_connect(target_name, timeout_ms); } void printer_protocol_disconnect(void) { ble_printer_client_disconnect(); } void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { if (out_status == NULL) { return; } memset(out_status, 0, sizeof(*out_status)); 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->mtu = link.mtu; if (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->battery_percent = s_status.battery; out_status->temperature = s_status.temperature; out_status->last_status_ms = s_status.updated_ms; out_status->queue_depth = (uint32_t)uxQueueMessagesWaiting(s_job_queue); xSemaphoreGive(s_mutex); } } esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, size_t raster_len, uint16_t width, uint16_t height, const char *density, uint32_t *out_job_id, char *err, size_t err_len) { if (raster == NULL || raster_len == 0 || width == 0 || height == 0) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid args"); } return ESP_ERR_INVALID_ARG; } if (width != 384) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "width must be 384"); } return ESP_ERR_INVALID_ARG; } if (raster_len > MAX_RASTER_BYTES) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "raster too large"); } return ESP_ERR_INVALID_SIZE; } size_t expected_len = ((size_t)width / 8u) * (size_t)height; if (expected_len != raster_len) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "size mismatch exp=%u got=%u", (unsigned)expected_len, (unsigned)raster_len); } return ESP_ERR_INVALID_SIZE; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "lock timeout"); } return ESP_ERR_TIMEOUT; } int idx = alloc_job_slot_locked(); if (idx < 0) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "job table full"); } return ESP_ERR_NO_MEM; } uint8_t *copy = (uint8_t *)malloc(raster_len); if (copy == NULL) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "malloc failed"); } return ESP_ERR_NO_MEM; } memcpy(copy, raster, raster_len); uint32_t id = s_next_job_id++; if (s_next_job_id == 0) { s_next_job_id = 1; } memset(&s_jobs[idx], 0, sizeof(s_jobs[idx])); s_jobs[idx].used = true; s_jobs[idx].id = id; s_jobs[idx].state = PRINT_JOB_STATE_QUEUED; s_jobs[idx].progress = 0; s_jobs[idx].width = width; s_jobs[idx].height = height; s_jobs[idx].data_len = raster_len; s_jobs[idx].created_ms = esp_timer_get_time() / 1000; s_jobs[idx].data = copy; strlcpy(s_jobs[idx].density, density != NULL ? density : "中等", sizeof(s_jobs[idx].density)); xSemaphoreGive(s_mutex); if (xQueueSend(s_job_queue, &id, pdMS_TO_TICKS(500)) != pdTRUE) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { int rollback_idx = find_job_idx_locked(id); if (rollback_idx >= 0) { free(s_jobs[rollback_idx].data); memset(&s_jobs[rollback_idx], 0, sizeof(s_jobs[rollback_idx])); } xSemaphoreGive(s_mutex); } if (err != NULL && err_len > 0) { snprintf(err, err_len, "job queue full"); } return ESP_ERR_TIMEOUT; } if (out_job_id != NULL) { *out_job_id = id; } return ESP_OK; } bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info) { if (out_info == NULL || job_id == 0) { return false; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { return false; } int idx = find_job_idx_locked(job_id); if (idx < 0) { xSemaphoreGive(s_mutex); return false; } memset(out_info, 0, sizeof(*out_info)); out_info->id = s_jobs[idx].id; out_info->state = s_jobs[idx].state; out_info->progress = s_jobs[idx].progress; out_info->width = s_jobs[idx].width; out_info->height = s_jobs[idx].height; out_info->data_len = s_jobs[idx].data_len; out_info->created_ms = s_jobs[idx].created_ms; out_info->started_ms = s_jobs[idx].started_ms; out_info->finished_ms = s_jobs[idx].finished_ms; strlcpy(out_info->density, s_jobs[idx].density, sizeof(out_info->density)); strlcpy(out_info->error, s_jobs[idx].error, sizeof(out_info->error)); xSemaphoreGive(s_mutex); return true; } esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count) { if (out_count == NULL) { return ESP_ERR_INVALID_ARG; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { return ESP_ERR_TIMEOUT; } size_t count = 0; for (int i = 0; i < JOB_SLOT_MAX; ++i) { if (!s_jobs[i].used || s_jobs[i].state == PRINT_JOB_STATE_NONE) { continue; } if (out_jobs != NULL && count < max_jobs) { memset(&out_jobs[count], 0, sizeof(out_jobs[count])); out_jobs[count].id = s_jobs[i].id; out_jobs[count].state = s_jobs[i].state; out_jobs[count].progress = s_jobs[i].progress; out_jobs[count].width = s_jobs[i].width; out_jobs[count].height = s_jobs[i].height; out_jobs[count].data_len = s_jobs[i].data_len; out_jobs[count].created_ms = s_jobs[i].created_ms; out_jobs[count].started_ms = s_jobs[i].started_ms; out_jobs[count].finished_ms = s_jobs[i].finished_ms; strlcpy(out_jobs[count].density, s_jobs[i].density, sizeof(out_jobs[count].density)); strlcpy(out_jobs[count].error, s_jobs[i].error, sizeof(out_jobs[count].error)); } ++count; } xSemaphoreGive(s_mutex); *out_count = count; return ESP_OK; } esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len) { if (job_id == 0) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid job id"); } return ESP_ERR_INVALID_ARG; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "lock timeout"); } return ESP_ERR_TIMEOUT; } int idx = find_job_idx_locked(job_id); if (idx < 0) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "job not found"); } return ESP_ERR_NOT_FOUND; } if (is_terminal_state(s_jobs[idx].state)) { xSemaphoreGive(s_mutex); if (err != NULL && err_len > 0) { snprintf(err, err_len, "job already finished"); } return ESP_ERR_INVALID_STATE; } s_jobs[idx].cancel_requested = true; if (s_jobs[idx].state == PRINT_JOB_STATE_QUEUED) { s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; s_jobs[idx].progress = 0; strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); free(s_jobs[idx].data); s_jobs[idx].data = NULL; } else if (s_jobs[idx].state == PRINT_JOB_STATE_RUNNING) { strlcpy(s_jobs[idx].error, "cancel requested", sizeof(s_jobs[idx].error)); } xSemaphoreGive(s_mutex); return ESP_OK; } size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { return 0; } size_t removed = 0; for (int i = 0; i < JOB_SLOT_MAX; ++i) { if (!s_jobs[i].used) { continue; } bool match = false; if (include_success && s_jobs[i].state == PRINT_JOB_STATE_SUCCESS) { match = true; } if (include_failed && s_jobs[i].state == PRINT_JOB_STATE_FAILED) { match = true; } if (include_canceled && s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { match = true; } if (!match) { continue; } free(s_jobs[i].data); memset(&s_jobs[i], 0, sizeof(s_jobs[i])); ++removed; } xSemaphoreGive(s_mutex); return removed; }