#include "printer_protocol.h" #include "printer_protocol_internal.h" #include #include #include #include "ble_printer_client.h" #include "esp_log.h" #include "esp_timer.h" #include "runtime_diagnostics.h" #include "runtime_policy.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" static const char *TAG = "printer_protocol"; static bool request_status_sync(uint32_t timeout_ms) { int64_t old_ms; if (printer_protocol_is_stopping()) { return false; } 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 (printer_protocol_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) { if (printer_protocol_is_stopping()) { return false; } 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'; uint8_t next_progress_log = 25; ESP_LOGI(TAG, "job %u print start, raster_bytes=%u, size=%ux%u, density=%s", (unsigned)job->id, (unsigned)job->data_len, (unsigned)job->width, (unsigned)job->height, job->density); if (printer_protocol_is_stopping()) { snprintf(job->error, sizeof(job->error), "protocol stopping"); return false; } if (!ble_printer_client_is_connected()) { snprintf(job->error, sizeof(job->error), "printer not connected"); ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); return false; } if (!precheck_printer_ready(err, sizeof(err))) { snprintf(job->error, sizeof(job->error), "%s", err); ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); return false; } if (job->cancel_requested) { snprintf(job->error, sizeof(job->error), "job canceled"); ESP_LOGW(TAG, "job %u print canceled before start", (unsigned)job->id); return false; } uint8_t power_on = 0x01; if (!printer_protocol_send_cmd_with_ack(CMD_POWER, &power_on, 1, true, 1500)) { snprintf(job->error, sizeof(job->error), "power on failed"); ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); 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 (!printer_protocol_send_cmd_with_ack(CMD_SET_PARAM, param, sizeof(param), true, 1500)) { snprintf(job->error, sizeof(job->error), "set print param failed"); ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); return false; } const size_t chunk_max = 240; size_t total_chunks = (size_t)ceil((double)job->data_len / (double)chunk_max); ESP_LOGI(TAG, "job %u data transfer start, chunks=%u, chunk_bytes=%u, hot_time=%u", (unsigned)job->id, (unsigned)total_chunks, (unsigned)chunk_max, (unsigned)hot_time); for (size_t i = 0; i < total_chunks; ++i) { if (printer_protocol_is_stopping()) { snprintf(job->error, sizeof(job->error), "protocol stopping"); return false; } if (job->cancel_requested) { snprintf(job->error, sizeof(job->error), "job canceled"); ESP_LOGW(TAG, "job %u print canceled at chunk %u/%u", (unsigned)job->id, (unsigned)(i + 1), (unsigned)total_chunks); 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 (!printer_protocol_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); ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); 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); } while (job->progress >= next_progress_log && next_progress_log <= 90) { ESP_LOGI(TAG, "job %u print progress=%u%% (%u/%u chunks)", (unsigned)job->id, (unsigned)job->progress, (unsigned)(i + 1), (unsigned)total_chunks); next_progress_log = (uint8_t)(next_progress_log + 25); } } ESP_LOGI(TAG, "job %u data transfer done", (unsigned)job->id); uint8_t power_off = 0x00; (void)printer_protocol_send_cmd_with_ack(CMD_POWER, &power_off, 1, true, 1200); uint8_t feed_payload[3] = {0x2B, 0x00, 0x0C}; (void)printer_protocol_send_cmd_with_ack(CMD_SET_DISTANCE, feed_payload, sizeof(feed_payload), true, 1200); ESP_LOGI(TAG, "job %u print command sequence done", (unsigned)job->id); return true; } void printer_protocol_worker_task(void *arg) { (void)arg; while (true) { if (printer_protocol_is_stopping()) { break; } uint32_t job_id = 0; if (xQueueReceive(s_job_queue, &job_id, pdMS_TO_TICKS(runtime_policy_printer_worker_queue_wait_ms())) != pdTRUE) { continue; } if (job_id == PRINTER_JOB_SENTINEL_STOP || printer_protocol_is_stopping()) { break; } if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { continue; } int idx = printer_protocol_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); runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_PRINTER_JOB_CANCELED, 1); continue; } if (s_busy_refcnt != 0) { xSemaphoreGive(s_mutex); (void)xQueueSendToFront(s_job_queue, &job_id, 0); vTaskDelay(pdMS_TO_TICKS(runtime_policy_printer_queue_retry_delay_ms())); continue; } job_slot_t *job = &s_jobs[idx]; job->state = PRINT_JOB_STATE_RUNNING; job->started_ms = esp_timer_get_time() / 1000; job->progress = 1; s_busy_refcnt = 1; int64_t queue_wait_ms = 0; if (job->started_ms >= job->created_ms) { queue_wait_ms = job->started_ms - job->created_ms; } ESP_LOGI(TAG, "job %u running, queue_wait_ms=%lld, raster_bytes=%u, size=%ux%u, density=%s", (unsigned)job->id, (long long)queue_wait_ms, (unsigned)job->data_len, (unsigned)job->width, (unsigned)job->height, job->density); xSemaphoreGive(s_mutex); bool ok = run_print_job(job); 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'; runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_PRINTER_JOB_SUCCESS, 1); } else if (s_jobs[idx].cancel_requested || printer_protocol_is_stopping()) { 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)); } runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_PRINTER_JOB_CANCELED, 1); } else { s_jobs[idx].state = PRINT_JOB_STATE_FAILED; runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_PRINTER_JOB_FAILED, 1); } int64_t duration_ms = 0; if (s_jobs[idx].finished_ms >= s_jobs[idx].started_ms) { duration_ms = s_jobs[idx].finished_ms - s_jobs[idx].started_ms; } if (s_jobs[idx].state == PRINT_JOB_STATE_SUCCESS) { ESP_LOGI(TAG, "job %u done, state=%s, duration_ms=%lld", (unsigned)s_jobs[idx].id, printer_protocol_job_state_str(s_jobs[idx].state), (long long)duration_ms); } else { ESP_LOGW(TAG, "job %u done, state=%s, duration_ms=%lld, error=%s", (unsigned)s_jobs[idx].id, printer_protocol_job_state_str(s_jobs[idx].state), (long long)duration_ms, s_jobs[idx].error[0] != '\0' ? s_jobs[idx].error : "-"); } free(s_jobs[idx].data); s_jobs[idx].data = NULL; runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, (int32_t)uxQueueMessagesWaiting(s_job_queue)); xSemaphoreGive(s_mutex); } if (s_evt != NULL) { xEventGroupSetBits(s_evt, EVT_WORKER_EXITED); } s_worker_task = NULL; vTaskDelete(NULL); }