Files
AI_Printer/components/platform/src/direct_thermal_printer.c

1021 lines
34 KiB
C

#include "platform.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#include "driver/gpio.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#ifndef CONFIG_TQ_DIRECT_PRINTER_ENABLE
#define CONFIG_TQ_DIRECT_PRINTER_ENABLE 1
#endif
#ifndef CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH
#define CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH 1
#endif
#if CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH
#define BOOST_DEFAULT_ACTIVE_HIGH 1
#else
#define BOOST_DEFAULT_ACTIVE_HIGH 0
#endif
#define ADC_RAW_MAX 4095
#define DEBUG_SHIFT_CLOCK_US_MIN 1
#define DEBUG_SHIFT_CLOCK_US_MAX 50
#define DEBUG_LATCH_PULSE_US_MIN 1
#define DEBUG_LATCH_PULSE_US_MAX 50
static const uint16_t k_default_shift_clock_high_us = 5;
static const uint16_t k_default_shift_clock_low_us = 5;
static const uint16_t k_default_latch_pulse_us = 1;
static const bool k_default_strobe_active_high = true;
static const char *TAG = "direct_printer";
static const uint8_t k_print_yield_lines = 8;
static const uint16_t k_gap_yield_steps = 32;
typedef struct {
bool ready;
adc_unit_t unit;
adc_channel_t channel;
} adc_pin_t;
typedef struct {
uint16_t shift_clock_high_us;
uint16_t shift_clock_low_us;
uint16_t latch_pulse_us;
bool strobe_active_high;
bool boost_active_high;
uint16_t override_strobe_on_us;
uint16_t override_strobe_interval_us;
uint16_t override_motor_step_us;
uint8_t override_steps_per_line;
} direct_debug_config_t;
typedef struct {
bool initialized;
bool connected;
uint8_t motor_phase;
direct_debug_config_t debug;
SemaphoreHandle_t lock;
adc_oneshot_unit_handle_t adc1_handle;
adc_oneshot_unit_handle_t adc2_handle;
adc_pin_t battery_adc;
adc_pin_t ntc_adc;
} direct_printer_state_t;
static direct_printer_state_t s_state;
static const uint8_t k_motor_step_table[4] = {0x05, 0x09, 0x0A, 0x06};
static int boost_on_level_locked(void) {
return s_state.debug.boost_active_high ? 1 : 0;
}
static int boost_off_level_locked(void) {
return s_state.debug.boost_active_high ? 0 : 1;
}
static int strobe_active_level_locked(void) {
return s_state.debug.strobe_active_high ? 1 : 0;
}
static int strobe_idle_level_locked(void) {
return s_state.debug.strobe_active_high ? 0 : 1;
}
static void set_debug_defaults_locked(void) {
memset(&s_state.debug, 0, sizeof(s_state.debug));
s_state.debug.shift_clock_high_us = k_default_shift_clock_high_us;
s_state.debug.shift_clock_low_us = k_default_shift_clock_low_us;
s_state.debug.latch_pulse_us = k_default_latch_pulse_us;
s_state.debug.strobe_active_high = k_default_strobe_active_high;
s_state.debug.boost_active_high = BOOST_DEFAULT_ACTIVE_HIGH != 0;
}
static bool is_gpio_valid(int gpio_num) {
return gpio_num >= 0;
}
static void write_err(char *err, size_t err_len, const char *text) {
if (err != NULL && err_len > 0) {
strlcpy(err, text, err_len);
}
}
static void set_gpio_level_if_valid(int gpio_num, int level) {
if (!is_gpio_valid(gpio_num)) {
return;
}
(void)gpio_set_level(gpio_num, level);
}
static esp_err_t config_output_pin(int gpio_num, int level, const char *name) {
if (!is_gpio_valid(gpio_num)) {
return ESP_OK;
}
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << gpio_num,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t err = gpio_config(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "gpio_config output failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err));
return err;
}
err = gpio_set_level(gpio_num, level);
if (err != ESP_OK) {
ESP_LOGE(TAG, "gpio_set_level failed: %s pin=%d level=%d err=%s", name, gpio_num, level, esp_err_to_name(err));
}
return err;
}
static esp_err_t config_input_pin(int gpio_num, bool pull_up, const char *name) {
if (!is_gpio_valid(gpio_num)) {
return ESP_OK;
}
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << gpio_num,
.mode = GPIO_MODE_INPUT,
.pull_up_en = pull_up ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t err = gpio_config(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "gpio_config input failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err));
}
return err;
}
static esp_err_t ensure_shared_shift_pins_gpio_locked(void) {
platform_display_mark_shared_spi_dirty();
esp_err_t err = config_output_pin(CONFIG_TQ_SPI_SHARED_MOSI_PIN, 0, "print_mosi");
if (err != ESP_OK) {
return err;
}
return config_output_pin(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0, "print_clk");
}
static void set_stb_level_locked(int level) {
int out_level = level ? strobe_active_level_locked() : strobe_idle_level_locked();
set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_12_PIN, out_level);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_34_PIN, out_level);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_56_PIN, out_level);
}
static void motor_apply_pattern_locked(uint8_t pattern) {
set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTA_P_PIN, (pattern >> 0) & 0x01);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTA_N_PIN, (pattern >> 1) & 0x01);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTB_P_PIN, (pattern >> 2) & 0x01);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTB_N_PIN, (pattern >> 3) & 0x01);
}
static void motor_off_locked(void) {
motor_apply_pattern_locked(0x00);
}
static void safe_drive_off_locked(bool keep_boost_on) {
set_stb_level_locked(0);
set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_MOSI_PIN, 0);
set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 1);
motor_off_locked();
if (!keep_boost_on) {
set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, boost_off_level_locked());
}
}
static void pulse_latch_locked(void) {
uint16_t latch_us = s_state.debug.latch_pulse_us;
if (latch_us < DEBUG_LATCH_PULSE_US_MIN) {
latch_us = DEBUG_LATCH_PULSE_US_MIN;
} else if (latch_us > DEBUG_LATCH_PULSE_US_MAX) {
latch_us = DEBUG_LATCH_PULSE_US_MAX;
}
set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 0);
esp_rom_delay_us(latch_us);
set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 1);
}
static void fire_strobe_locked(uint16_t on_us, uint16_t interval_us) {
set_stb_level_locked(1);
esp_rom_delay_us(on_us);
set_stb_level_locked(0);
if (interval_us > 0) {
esp_rom_delay_us(interval_us);
}
}
static void motor_step_once_locked(uint16_t step_delay_us) {
uint8_t pattern = k_motor_step_table[s_state.motor_phase & 0x03];
motor_apply_pattern_locked(pattern);
if (step_delay_us > 0) {
esp_rom_delay_us(step_delay_us);
}
if (runtime_policy_direct_printer_motor_reverse()) {
s_state.motor_phase = (uint8_t)((s_state.motor_phase + 3) & 0x03);
} else {
s_state.motor_phase = (uint8_t)((s_state.motor_phase + 1) & 0x03);
}
}
static void write_line_bits_locked(const uint8_t *line, size_t line_bytes) {
uint16_t high_us = s_state.debug.shift_clock_high_us;
uint16_t low_us = s_state.debug.shift_clock_low_us;
if (high_us < DEBUG_SHIFT_CLOCK_US_MIN) {
high_us = DEBUG_SHIFT_CLOCK_US_MIN;
} else if (high_us > DEBUG_SHIFT_CLOCK_US_MAX) {
high_us = DEBUG_SHIFT_CLOCK_US_MAX;
}
if (low_us < DEBUG_SHIFT_CLOCK_US_MIN) {
low_us = DEBUG_SHIFT_CLOCK_US_MIN;
} else if (low_us > DEBUG_SHIFT_CLOCK_US_MAX) {
low_us = DEBUG_SHIFT_CLOCK_US_MAX;
}
for (size_t i = 0; i < line_bytes; ++i) {
uint8_t byte = line[i];
for (int bit = 7; bit >= 0; --bit) {
int pixel = ((byte >> bit) & 0x01) ? 1 : 0;
set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_MOSI_PIN, pixel);
set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 1);
esp_rom_delay_us(high_us);
set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0);
esp_rom_delay_us(low_us);
}
}
}
static esp_err_t ensure_adc_unit(adc_unit_t unit, adc_oneshot_unit_handle_t *out_handle) {
if (out_handle == NULL) {
return ESP_ERR_INVALID_ARG;
}
adc_oneshot_unit_handle_t *slot = NULL;
if (unit == ADC_UNIT_1) {
slot = &s_state.adc1_handle;
} else if (unit == ADC_UNIT_2) {
slot = &s_state.adc2_handle;
} else {
return ESP_ERR_NOT_SUPPORTED;
}
if (*slot == NULL) {
adc_oneshot_unit_init_cfg_t cfg = {
.unit_id = unit,
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
esp_err_t err = adc_oneshot_new_unit(&cfg, slot);
if (err != ESP_OK) {
ESP_LOGW(TAG, "adc unit init failed: unit=%d err=%s", (int)unit, esp_err_to_name(err));
return err;
}
}
*out_handle = *slot;
return ESP_OK;
}
static void init_adc_pin_if_possible(int gpio_num, const char *name, adc_pin_t *out_pin) {
if (out_pin == NULL) {
return;
}
memset(out_pin, 0, sizeof(*out_pin));
if (!is_gpio_valid(gpio_num)) {
return;
}
adc_unit_t unit = ADC_UNIT_1;
adc_channel_t channel = ADC_CHANNEL_0;
esp_err_t err = adc_oneshot_io_to_channel(gpio_num, &unit, &channel);
if (err != ESP_OK) {
ESP_LOGW(TAG, "adc gpio unsupported: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err));
return;
}
adc_oneshot_unit_handle_t handle = NULL;
err = ensure_adc_unit(unit, &handle);
if (err != ESP_OK) {
return;
}
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
err = adc_oneshot_config_channel(handle, channel, &chan_cfg);
if (err != ESP_OK) {
ESP_LOGW(TAG, "adc channel config failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err));
return;
}
out_pin->ready = true;
out_pin->unit = unit;
out_pin->channel = channel;
}
static esp_err_t read_adc_raw_locked(const adc_pin_t *pin, int *out_raw) {
if (pin == NULL || out_raw == NULL || !pin->ready) {
return ESP_ERR_INVALID_STATE;
}
adc_oneshot_unit_handle_t handle = NULL;
if (pin->unit == ADC_UNIT_1) {
handle = s_state.adc1_handle;
} else if (pin->unit == ADC_UNIT_2) {
handle = s_state.adc2_handle;
}
if (handle == NULL) {
return ESP_ERR_INVALID_STATE;
}
return adc_oneshot_read(handle, pin->channel, out_raw);
}
static bool read_paper_present_locked(int *out_level) {
if (!is_gpio_valid(CONFIG_TQ_PRINT_PAPER_PIN)) {
if (out_level != NULL) {
*out_level = -1;
}
return true;
}
int level = gpio_get_level(CONFIG_TQ_PRINT_PAPER_PIN);
if (out_level != NULL) {
*out_level = level;
}
return level == (int)runtime_policy_direct_printer_paper_present_level();
}
static uint16_t battery_raw_to_mv(int raw) {
if (raw < 0) {
raw = 0;
}
if (raw > ADC_RAW_MAX) {
raw = ADC_RAW_MAX;
}
float adc_mv = ((float)raw * 3300.0f) / (float)ADC_RAW_MAX;
float ratio = (float)runtime_policy_direct_printer_battery_divider_ratio_x1000() / 1000.0f;
float batt_mv = adc_mv * ratio;
if (batt_mv < 0.0f) {
batt_mv = 0.0f;
}
if (batt_mv > 65535.0f) {
batt_mv = 65535.0f;
}
return (uint16_t)batt_mv;
}
static uint8_t battery_mv_to_percent(uint16_t batt_mv) {
uint16_t empty_mv = runtime_policy_direct_printer_battery_empty_mv();
uint16_t full_mv = runtime_policy_direct_printer_battery_full_mv();
if (full_mv <= empty_mv) {
return 100;
}
if (batt_mv <= empty_mv) {
return 0;
}
if (batt_mv >= full_mv) {
return 100;
}
return (uint8_t)(((uint32_t)(batt_mv - empty_mv) * 100u) / (uint32_t)(full_mv - empty_mv));
}
static float ntc_raw_to_temp_c(int raw) {
if (raw <= 0 || raw >= ADC_RAW_MAX) {
return 25.0f;
}
float pullup = (float)runtime_policy_direct_printer_ntc_pullup_ohms();
float r25 = (float)runtime_policy_direct_printer_ntc_r25_ohms();
float beta = (float)runtime_policy_direct_printer_ntc_beta();
if (pullup <= 0.0f || r25 <= 0.0f || beta <= 0.0f) {
return 25.0f;
}
float ratio = (float)raw / (float)(ADC_RAW_MAX - raw);
float r_ntc = pullup * ratio;
if (r_ntc <= 1.0f) {
return 25.0f;
}
float inv_t = (1.0f / (273.15f + 25.0f)) + (logf(r_ntc / r25) / beta);
if (inv_t <= 0.0f) {
return 25.0f;
}
return (1.0f / inv_t) - 273.15f;
}
static void sample_sensors_locked(platform_printer_sensors_t *out_sensors) {
if (out_sensors == NULL) {
return;
}
memset(out_sensors, 0, sizeof(*out_sensors));
int paper_level = -1;
out_sensors->has_paper = read_paper_present_locked(&paper_level);
out_sensors->paper_gpio_level = (int8_t)paper_level;
out_sensors->paper_present_level = runtime_policy_direct_printer_paper_present_level();
out_sensors->battery_percent = 100;
out_sensors->temperature_c = 25.0f;
out_sensors->updated_ms = esp_timer_get_time() / 1000;
int battery_raw = 0;
if (read_adc_raw_locked(&s_state.battery_adc, &battery_raw) == ESP_OK) {
uint16_t batt_mv = battery_raw_to_mv(battery_raw);
out_sensors->battery_percent = battery_mv_to_percent(batt_mv);
}
int ntc_raw = 0;
if (read_adc_raw_locked(&s_state.ntc_adc, &ntc_raw) == ESP_OK) {
out_sensors->temperature_c = ntc_raw_to_temp_c(ntc_raw);
}
}
static esp_err_t precheck_before_print_locked(char *err, size_t err_len) {
platform_printer_sensors_t sensors = {0};
sample_sensors_locked(&sensors);
if (!sensors.has_paper) {
write_err(err, err_len, "printer out of paper");
return ESP_ERR_INVALID_STATE;
}
float temp_min = runtime_policy_direct_printer_temp_min_c();
float temp_max = runtime_policy_direct_printer_temp_max_c();
if (temp_max <= temp_min) {
temp_max = temp_min + 1.0f;
}
if (sensors.temperature_c < temp_min || sensors.temperature_c > temp_max) {
write_err(err, err_len, "temperature out of range");
return ESP_ERR_INVALID_STATE;
}
uint8_t battery_min = runtime_policy_direct_printer_battery_min_percent();
if (sensors.battery_percent < battery_min) {
write_err(err, err_len, "battery too low");
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
static bool has_timed_out(int64_t deadline_ms) {
if (deadline_ms <= 0) {
return false;
}
return (esp_timer_get_time() / 1000) > deadline_ms;
}
esp_err_t platform_direct_printer_init(void) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
return ESP_ERR_NOT_SUPPORTED;
#else
if (s_state.initialized) {
return ESP_OK;
}
memset(&s_state, 0, sizeof(s_state));
s_state.lock = xSemaphoreCreateMutex();
if (s_state.lock == NULL) {
return ESP_ERR_NO_MEM;
}
set_debug_defaults_locked();
esp_err_t err = ESP_OK;
err = config_output_pin(CONFIG_TQ_PRINT_STB_12_PIN, strobe_idle_level_locked(), "print_stb12");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_STB_34_PIN, strobe_idle_level_locked(), "print_stb34");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_STB_56_PIN, strobe_idle_level_locked(), "print_stb56");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_LAT_PIN, 1, "print_lat");
if (err != ESP_OK) {
return err;
}
err = ensure_shared_shift_pins_gpio_locked();
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_OUTA_P_PIN, 0, "print_outa_p");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_OUTA_N_PIN, 0, "print_outa_n");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_OUTB_P_PIN, 0, "print_outb_p");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_PRINT_OUTB_N_PIN, 0, "print_outb_n");
if (err != ESP_OK) {
return err;
}
err = config_output_pin(CONFIG_TQ_KEY_PRINT_PIN, boost_off_level_locked(), "print_boost");
if (err != ESP_OK) {
return err;
}
err = config_input_pin(CONFIG_TQ_PRINT_PAPER_PIN, true, "print_paper");
if (err != ESP_OK) {
return err;
}
init_adc_pin_if_possible(CONFIG_TQ_BATTERY_ADC_PIN, "battery_adc", &s_state.battery_adc);
init_adc_pin_if_possible(CONFIG_TQ_NTC_ADC_PIN, "ntc_adc", &s_state.ntc_adc);
s_state.motor_phase = 0;
s_state.connected = false;
s_state.initialized = true;
ESP_LOGI(TAG, "direct printer initialized");
return ESP_OK;
#endif
}
void platform_direct_printer_deinit(void) {
if (!s_state.initialized) {
return;
}
if (s_state.lock != NULL && xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(100)) == pdTRUE) {
safe_drive_off_locked(false);
s_state.connected = false;
xSemaphoreGive(s_state.lock);
}
if (s_state.adc1_handle != NULL) {
(void)adc_oneshot_del_unit(s_state.adc1_handle);
s_state.adc1_handle = NULL;
}
if (s_state.adc2_handle != NULL) {
(void)adc_oneshot_del_unit(s_state.adc2_handle);
s_state.adc2_handle = NULL;
}
if (s_state.lock != NULL) {
vSemaphoreDelete(s_state.lock);
}
memset(&s_state, 0, sizeof(s_state));
}
esp_err_t platform_direct_printer_connect(uint32_t timeout_ms) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
(void)timeout_ms;
return ESP_ERR_NOT_SUPPORTED;
#else
esp_err_t err = platform_direct_printer_init();
if (err != ESP_OK) {
return err;
}
uint32_t lock_timeout_ms = timeout_ms == 0 ? 500 : timeout_ms;
if (lock_timeout_ms > 5000) {
lock_timeout_ms = 5000;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(lock_timeout_ms)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
if (!s_state.connected) {
err = ensure_shared_shift_pins_gpio_locked();
if (err != ESP_OK) {
xSemaphoreGive(s_state.lock);
return err;
}
set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, boost_on_level_locked());
vTaskDelay(pdMS_TO_TICKS(10));
safe_drive_off_locked(true);
s_state.connected = true;
}
xSemaphoreGive(s_state.lock);
return ESP_OK;
#endif
}
void platform_direct_printer_disconnect(void) {
if (!s_state.initialized || s_state.lock == NULL) {
return;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) {
return;
}
safe_drive_off_locked(false);
s_state.connected = false;
xSemaphoreGive(s_state.lock);
}
bool platform_direct_printer_is_connected(void) {
if (!s_state.initialized || s_state.lock == NULL) {
return false;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(100)) != pdTRUE) {
return false;
}
bool connected = s_state.connected;
xSemaphoreGive(s_state.lock);
return connected;
}
esp_err_t platform_direct_printer_get_sensors(platform_printer_sensors_t *out_sensors) {
if (out_sensors == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!s_state.initialized || s_state.lock == NULL) {
return ESP_ERR_INVALID_STATE;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
sample_sensors_locked(out_sensors);
xSemaphoreGive(s_state.lock);
return ESP_OK;
}
esp_err_t platform_direct_printer_get_debug_config(uint16_t *out_shift_clock_high_us,
uint16_t *out_shift_clock_low_us,
uint16_t *out_latch_pulse_us,
bool *out_strobe_active_high,
bool *out_boost_active_high,
uint16_t *out_override_strobe_on_us,
uint16_t *out_override_strobe_interval_us,
uint16_t *out_override_motor_step_us,
uint8_t *out_override_steps_per_line) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
(void)out_shift_clock_high_us;
(void)out_shift_clock_low_us;
(void)out_latch_pulse_us;
(void)out_strobe_active_high;
(void)out_boost_active_high;
(void)out_override_strobe_on_us;
(void)out_override_strobe_interval_us;
(void)out_override_motor_step_us;
(void)out_override_steps_per_line;
return ESP_ERR_NOT_SUPPORTED;
#else
if (out_shift_clock_high_us == NULL || out_shift_clock_low_us == NULL || out_latch_pulse_us == NULL ||
out_strobe_active_high == NULL || out_boost_active_high == NULL || out_override_strobe_on_us == NULL ||
out_override_strobe_interval_us == NULL || out_override_motor_step_us == NULL ||
out_override_steps_per_line == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t init_rc = platform_direct_printer_init();
if (init_rc != ESP_OK) {
return init_rc;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
*out_shift_clock_high_us = s_state.debug.shift_clock_high_us;
*out_shift_clock_low_us = s_state.debug.shift_clock_low_us;
*out_latch_pulse_us = s_state.debug.latch_pulse_us;
*out_strobe_active_high = s_state.debug.strobe_active_high;
*out_boost_active_high = s_state.debug.boost_active_high;
*out_override_strobe_on_us = s_state.debug.override_strobe_on_us;
*out_override_strobe_interval_us = s_state.debug.override_strobe_interval_us;
*out_override_motor_step_us = s_state.debug.override_motor_step_us;
*out_override_steps_per_line = s_state.debug.override_steps_per_line;
xSemaphoreGive(s_state.lock);
return ESP_OK;
#endif
}
esp_err_t platform_direct_printer_set_debug_config(bool has_config,
uint16_t shift_clock_high_us,
uint16_t shift_clock_low_us,
uint16_t latch_pulse_us,
bool strobe_active_high,
bool boost_active_high,
uint16_t override_strobe_on_us,
uint16_t override_strobe_interval_us,
uint16_t override_motor_step_us,
uint8_t override_steps_per_line,
bool reset_defaults,
char *err,
size_t err_len) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
(void)has_config;
(void)shift_clock_high_us;
(void)shift_clock_low_us;
(void)latch_pulse_us;
(void)strobe_active_high;
(void)boost_active_high;
(void)override_strobe_on_us;
(void)override_strobe_interval_us;
(void)override_motor_step_us;
(void)override_steps_per_line;
(void)reset_defaults;
write_err(err, err_len, "direct backend disabled");
return ESP_ERR_NOT_SUPPORTED;
#else
if (!has_config && !reset_defaults) {
write_err(err, err_len, "invalid args");
return ESP_ERR_INVALID_ARG;
}
esp_err_t init_rc = platform_direct_printer_init();
if (init_rc != ESP_OK) {
write_err(err, err_len, "direct backend not initialized");
return init_rc;
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
write_err(err, err_len, "direct printer lock timeout");
return ESP_ERR_TIMEOUT;
}
if (reset_defaults) {
set_debug_defaults_locked();
}
if (has_config) {
if (shift_clock_high_us < DEBUG_SHIFT_CLOCK_US_MIN || shift_clock_high_us > DEBUG_SHIFT_CLOCK_US_MAX ||
shift_clock_low_us < DEBUG_SHIFT_CLOCK_US_MIN || shift_clock_low_us > DEBUG_SHIFT_CLOCK_US_MAX) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "shift clock us out of range (1..50)");
return ESP_ERR_INVALID_ARG;
}
if (latch_pulse_us < DEBUG_LATCH_PULSE_US_MIN || latch_pulse_us > DEBUG_LATCH_PULSE_US_MAX) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "latch pulse us out of range (1..50)");
return ESP_ERR_INVALID_ARG;
}
if (override_strobe_on_us != 0 && (override_strobe_on_us < 100 || override_strobe_on_us > 10000)) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "strobe_on_us must be 0 or 100..10000");
return ESP_ERR_INVALID_ARG;
}
if (override_strobe_interval_us > 10000) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "strobe_interval_us must be 0..10000");
return ESP_ERR_INVALID_ARG;
}
if (override_motor_step_us != 0 && (override_motor_step_us < 100 || override_motor_step_us > 20000)) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "motor_step_us must be 0 or 100..20000");
return ESP_ERR_INVALID_ARG;
}
if (override_steps_per_line > 8) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "steps_per_line must be 0..8");
return ESP_ERR_INVALID_ARG;
}
s_state.debug.shift_clock_high_us = shift_clock_high_us;
s_state.debug.shift_clock_low_us = shift_clock_low_us;
s_state.debug.latch_pulse_us = latch_pulse_us;
s_state.debug.strobe_active_high = strobe_active_high;
s_state.debug.boost_active_high = boost_active_high;
s_state.debug.override_strobe_on_us = override_strobe_on_us;
s_state.debug.override_strobe_interval_us = override_strobe_interval_us;
s_state.debug.override_motor_step_us = override_motor_step_us;
s_state.debug.override_steps_per_line = override_steps_per_line;
}
safe_drive_off_locked(s_state.connected);
xSemaphoreGive(s_state.lock);
return ESP_OK;
#endif
}
esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *request,
char *err,
size_t err_len) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
write_err(err, err_len, "direct backend disabled");
return ESP_ERR_NOT_SUPPORTED;
#else
if (request == NULL || request->raster == NULL || request->width == 0 || request->height == 0) {
write_err(err, err_len, "invalid args");
return ESP_ERR_INVALID_ARG;
}
if (request->width != 384) {
write_err(err, err_len, "width must be 384");
return ESP_ERR_INVALID_ARG;
}
const size_t line_bytes = (size_t)request->width / 8u;
const size_t expected_len = line_bytes * (size_t)request->height;
if (request->raster_len != expected_len) {
write_err(err, err_len, "raster size mismatch");
return ESP_ERR_INVALID_SIZE;
}
if (!s_state.initialized || s_state.lock == NULL) {
write_err(err, err_len, "direct backend not initialized");
return ESP_ERR_INVALID_STATE;
}
uint32_t timeout_ms = request->timeout_ms;
if (timeout_ms == 0) {
timeout_ms = runtime_policy_direct_printer_operation_timeout_ms();
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
write_err(err, err_len, "direct printer lock timeout");
return ESP_ERR_TIMEOUT;
}
if (!s_state.connected) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "direct printer not connected");
return ESP_ERR_INVALID_STATE;
}
esp_err_t rc = ensure_shared_shift_pins_gpio_locked();
if (rc != ESP_OK) {
safe_drive_off_locked(true);
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "direct printer gpio setup failed");
return rc;
}
uint16_t strobe_on_us = (request->strobe_on_time_us > 0)
? request->strobe_on_time_us
: runtime_policy_direct_printer_strobe_on_us();
uint16_t strobe_interval_us = (request->strobe_interval_us > 0)
? request->strobe_interval_us
: runtime_policy_direct_printer_strobe_interval_us();
uint16_t motor_step_us = (request->motor_step_delay_us > 0)
? request->motor_step_delay_us
: runtime_policy_direct_printer_motor_step_us();
uint8_t steps_per_line = (request->motor_steps_per_line > 0)
? request->motor_steps_per_line
: runtime_policy_direct_printer_steps_per_line();
if (s_state.debug.override_strobe_on_us > 0) {
strobe_on_us = s_state.debug.override_strobe_on_us;
}
if (s_state.debug.override_strobe_interval_us > 0) {
strobe_interval_us = s_state.debug.override_strobe_interval_us;
}
if (s_state.debug.override_motor_step_us > 0) {
motor_step_us = s_state.debug.override_motor_step_us;
}
if (s_state.debug.override_steps_per_line > 0) {
steps_per_line = s_state.debug.override_steps_per_line;
}
rc = ESP_OK;
if (!request->ignore_precheck) {
rc = precheck_before_print_locked(err, err_len);
if (rc != ESP_OK) {
safe_drive_off_locked(true);
xSemaphoreGive(s_state.lock);
return rc;
}
}
int64_t deadline_ms = (int64_t)(esp_timer_get_time() / 1000) + (int64_t)timeout_ms;
for (uint16_t line = 0; line < request->height; ++line) {
if (request->cancel_flag != NULL && *request->cancel_flag) {
write_err(err, err_len, "job canceled");
rc = ESP_ERR_INVALID_STATE;
break;
}
if (has_timed_out(deadline_ms)) {
write_err(err, err_len, "print timeout");
rc = ESP_ERR_TIMEOUT;
break;
}
if (!request->ignore_precheck && (line & 0x0F) == 0) {
rc = precheck_before_print_locked(err, err_len);
if (rc != ESP_OK) {
break;
}
}
const uint8_t *line_ptr = &request->raster[(size_t)line * line_bytes];
write_line_bits_locked(line_ptr, line_bytes);
pulse_latch_locked();
fire_strobe_locked(strobe_on_us, strobe_interval_us);
for (uint8_t i = 0; i < steps_per_line; ++i) {
if (request->cancel_flag != NULL && *request->cancel_flag) {
write_err(err, err_len, "job canceled");
rc = ESP_ERR_INVALID_STATE;
break;
}
if (has_timed_out(deadline_ms)) {
write_err(err, err_len, "print timeout");
rc = ESP_ERR_TIMEOUT;
break;
}
motor_step_once_locked(motor_step_us);
}
if (rc != ESP_OK) {
break;
}
// Yield periodically so IDLE task can run and service task WDT on the same CPU.
if (((line + 1u) % k_print_yield_lines) == 0u) {
vTaskDelay(1);
}
}
safe_drive_off_locked(true);
xSemaphoreGive(s_state.lock);
return rc;
#endif
}
esp_err_t platform_direct_printer_gap_move(uint32_t timeout_ms, char *err, size_t err_len) {
#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE
write_err(err, err_len, "direct backend disabled");
return ESP_ERR_NOT_SUPPORTED;
#else
if (!s_state.initialized || s_state.lock == NULL) {
write_err(err, err_len, "direct backend not initialized");
return ESP_ERR_INVALID_STATE;
}
if (timeout_ms == 0) {
timeout_ms = runtime_policy_direct_printer_operation_timeout_ms();
}
if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
write_err(err, err_len, "direct printer lock timeout");
return ESP_ERR_TIMEOUT;
}
if (!s_state.connected) {
xSemaphoreGive(s_state.lock);
write_err(err, err_len, "direct printer not connected");
return ESP_ERR_INVALID_STATE;
}
esp_err_t rc = ESP_OK;
uint16_t steps = runtime_policy_direct_printer_gap_steps();
uint16_t step_delay_us = runtime_policy_direct_printer_motor_step_us();
if (s_state.debug.override_motor_step_us > 0) {
step_delay_us = s_state.debug.override_motor_step_us;
}
platform_printer_sensors_t sensors = {0};
sample_sensors_locked(&sensors);
uint8_t battery_min = runtime_policy_direct_printer_battery_min_percent();
if (sensors.battery_percent < battery_min) {
write_err(err, err_len, "battery too low");
safe_drive_off_locked(true);
xSemaphoreGive(s_state.lock);
return ESP_ERR_INVALID_STATE;
}
// Per JX-2R-01 guidance: out-of-paper feed should run at reduced speed (roughly <=600 PPS).
if (!sensors.has_paper && step_delay_us < 1667) {
step_delay_us = 1667;
}
if (!sensors.has_paper) {
ESP_LOGW(TAG,
"gap move while out-of-paper, gpio_level=%d expect=%u step_us=%u",
sensors.paper_gpio_level,
sensors.paper_present_level,
step_delay_us);
}
int64_t deadline_ms = (int64_t)(esp_timer_get_time() / 1000) + (int64_t)timeout_ms;
for (uint16_t i = 0; i < steps; ++i) {
if (has_timed_out(deadline_ms)) {
write_err(err, err_len, "gap move timeout");
rc = ESP_ERR_TIMEOUT;
break;
}
motor_step_once_locked(step_delay_us);
if (((i + 1u) % k_gap_yield_steps) == 0u) {
vTaskDelay(1);
}
}
safe_drive_off_locked(true);
xSemaphoreGive(s_state.lock);
return rc;
#endif
}