1021 lines
34 KiB
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
|
|
}
|