#include "platform.h" #include #include #include #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 }