932 lines
27 KiB
C
932 lines
27 KiB
C
#include "printer_protocol.h"
|
|
#include "printer_protocol_internal.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|