/* * lcd.c * * Created on: Dec 12, 2025 * Author: Thad */ /* NOTICE: THIS IS A DUMPING GROUND FOR OBSOLETE CODE SINCE WE NO LONGER HAVE AN LCD NONE OF THIS IS TESTED. */ // Debounce & Repeat Settings #define DEBOUNCE_MS 50 #define REPEAT_MS 200 #define REPEAT_START_MS 700 static uint8_t lcd_col = 0; static uint8_t lcd_row = 0; static bool debounced_state[4] = {false}; static bool last_known_state[4] = {false}; static uint64_t last_stable_time[4] = {0}; static uint64_t last_change_time[4] = {0}; static uint8_t claimed_repeats[4] = {0}; // === DELAY HELPERS === static inline void delay_us(uint32_t us) { esp_rom_delay_us(us); } static esp_err_t tca_write_word_16(uint8_t reg, uint16_t value) { uint8_t data[3] = { reg, (uint8_t)(value & 0xFF), (uint8_t)(value >> 8) }; return i2c_master_write_to_device(I2C_PORT, TCA_ADDR, data, 3, pdMS_TO_TICKS(1000)); } // === TCA9555 PORT CONTROL === static esp_err_t tca_set_config_port0(uint16_t config_port0) { return tca_write_word_16(TCA_REG_CONFIG0, config_port0); } static esp_err_t tca_port_write(uint8_t value) { return tca_write_word_8(TCA_REG_OUTPUT1, value); } static esp_err_t tca_port_read(uint16_t *value) { uint16_t low, high; ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT0, &low)); ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT1, &high)); *value = low | (high << 8); return ESP_OK; } // === LCD NIBBLE & COMMAND === static esp_err_t lcd_write_nibble(uint8_t nibble, bool rs) { uint8_t data_state = 0; if (rs) data_state |= (1 << LCD_RS); if (nibble & 0x01) data_state |= (1 << LCD_D4); if (nibble & 0x02) data_state |= (1 << LCD_D5); if (nibble & 0x04) data_state |= (1 << LCD_D6); if (nibble & 0x08) data_state |= (1 << LCD_D7); ESP_ERROR_CHECK(tca_port_write(data_state)); ESP_ERROR_CHECK(tca_port_write(data_state | (1 << LCD_E))); ESP_ERROR_CHECK(tca_port_write(data_state)); return ESP_OK; } static esp_err_t lcd_command(uint8_t cmd) { ESP_ERROR_CHECK(lcd_write_nibble(cmd >> 4, false)); ESP_ERROR_CHECK(lcd_write_nibble(cmd & 0x0F, false)); return ESP_OK; } static esp_err_t lcd_data(uint8_t data) { ESP_ERROR_CHECK(lcd_write_nibble(data >> 4, true)); ESP_ERROR_CHECK(lcd_write_nibble(data & 0x0F, true)); return ESP_OK; } void lcd_set_cursor(uint8_t row, uint8_t col) { uint8_t addr = (row == 0) ? 0x00 : 0x40; addr += col; lcd_row = row; lcd_col = col; lcd_command(0x80 | addr); delay_us(50); } void lcd_printf(const char *fmt, ...) { char buf[64]; va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); lcd_set_cursor(0, 0); for (int i = 0; i < 32 && buf[i]; i++) { if (i == 16) lcd_set_cursor(1, 0); lcd_data((uint8_t)buf[i]); delay_us(50); } } void lcd_print(const char *str) { lcd_set_cursor(0, 0); for (int i = 0; i < 32 && str[i]; i++) { if (i == 16) lcd_set_cursor(1, 0); lcd_data((uint8_t)str[i]); delay_us(50); } } void lcd_off(void) { if (i2c_initted) lcd_command(0x08); } esp_err_t lcd_init_4bit(void) { ESP_LOGI("I2C", "Starting LCD init..."); ESP_ERROR_CHECK(tca_set_config_port0(0xFF)); tca_port_write(0x00); delay_us(50000); ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(4500); ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150); ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150); ESP_ERROR_CHECK(lcd_write_nibble(0x2, false)); delay_us(150); ESP_ERROR_CHECK(lcd_command(0x28)); delay_us(150); ESP_ERROR_CHECK(lcd_command(0x08)); delay_us(150); ESP_ERROR_CHECK(lcd_command(0x01)); delay_us(2000); ESP_ERROR_CHECK(lcd_command(0x06)); delay_us(150); ESP_ERROR_CHECK(lcd_command(0x0C)); delay_us(150); ESP_LOGI("I2C", "LCD init complete."); return ESP_OK; } // === BUTTON DEBOUNCE & REPEAT === void update_buttons(void) { for (uint8_t btn = 0; btn < 4; ++btn) { last_known_state[btn] = debounced_state[btn]; } uint16_t port_val; ESP_ERROR_CHECK(tca_port_read(&port_val)); uint8_t raw_buttons = (uint8_t)(port_val & 0x0F); uint8_t raw_states = ~raw_buttons & 0x0F; uint64_t now = esp_timer_get_time() / 1000; for (uint8_t btn = 0; btn < 4; ++btn) { bool raw_pressed = (raw_states & (1 << btn)) != 0; if (raw_pressed != debounced_state[btn]) { if (now - last_stable_time[btn] >= DEBOUNCE_MS) { debounced_state[btn] = raw_pressed; last_stable_time[btn] = now; last_change_time[btn] = now; claimed_repeats[btn] = 0; } } else { last_stable_time[btn] = now; } } } bool get_button_tripped(uint8_t button) { return (button < 4) && debounced_state[button] && !last_known_state[button]; } bool get_button_released(uint8_t button) { return (button < 4) && !debounced_state[button] && last_known_state[button]; } bool get_button_state(uint8_t button) { return (button < 4) && debounced_state[button]; } bool get_button_repeat(uint8_t btn) { if (btn >= 4 || !debounced_state[btn]) return false; uint64_t now = esp_timer_get_time() / 1000; if (now + DEBOUNCE_MS < last_change_time[btn]) return false; if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) { claimed_repeats[btn]++; return true; } return false; } int8_t get_button_repeats(uint8_t btn) { if (!get_button_state(btn)) return 0; if (btn >= 4 || !debounced_state[btn]) return false; uint64_t now = esp_timer_get_time() / 1000; if (now + DEBOUNCE_MS < last_change_time[btn]) return false; if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) { claimed_repeats[btn]++; if (claimed_repeats[btn] > 100) claimed_repeats[btn] = 100; ESP_LOGI("BTN", "RPT %d", (uint8_t)claimed_repeats[btn]+2); return claimed_repeats[btn]+1; } if (debounced_state[btn] && !last_known_state[btn]) { ESP_LOGI("BTN", "FST %d", 1); return 1; } //ESP_LOGI("BTN", "RPT %d", 0); return 0; } int64_t get_button_ms(uint8_t btn) { if (!get_button_state(btn)) return 0; uint64_t now = esp_timer_get_time() / 1000; return now - last_change_time[btn]; } // Parameter descriptor structure typedef struct { const char key[24]; // NVS key name (null-terminated) uint8_t type_size; // Size in bytes: 1=uint8_t, 2=uint16_t, 4=uint32_t/float, 8=uint64_t/double uint8_t type_flags; // Bitfield: [0:1] signed, [2] float, [3:7] reserved const void *default_val; // Pointer to default value (matches type) } param_desc_t; typedef struct param_group_s param_group_t; typedef struct param_group_s { char* (*formatter)(const param_group_t*, uint8_t idx); const uint8_t num_keys; const uint8_t indices[8][2]; const char keys[8][20]; void (*launch_functions[8])(char* key, int8_t dir); } param_group_t; // temp buffer for formatting stuff onto the LCD static char formatting_buf[LCD_BUFLEN]; /* MENU DIALOG CONFIG */ char* schedule_format(const param_group_t *pg, uint8_t idx); char* dist_format (const param_group_t *pg, uint8_t idx); char* reprog_format (const param_group_t *pg, uint8_t idx); char* override_format(const param_group_t *pg, uint8_t idx); char* status_format (const param_group_t *pg, uint8_t idx); char* cal_format (const param_group_t *pg, uint8_t idx); char* efuse_format (const param_group_t *pg, uint8_t idx); char* ftp_format (const param_group_t *pg, uint8_t idx); // Launch functions (forward declarations) void trigger_move(char* key, int8_t dir); void rf_reprogram_remote(char* key, int8_t dir); void adjust_hour (char* key, int8_t dir); void adjust_i8_0_99 (char* key, int8_t dir); void adjust_generic (int idx, int8_t amt); void dummy_adjuster (char* key, int8_t dir) {}; // do nothing void launch_ftp (char* key, int8_t dir); void adjust_i32_smart_0_99999(char* key, int8_t dir); void adjust_i32_smart_0_999 (char* key, int8_t dir); // Parameter table (legible, declarative) const param_desc_t param_table[] = { { .key = "sched_start", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){0} }, { .key = "sched_end", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){0} }, { .key = "sched_num", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){0} }, { .key = "efuse_drive_A", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){99} },{ .key = "efuse_jack_A", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){99} },{ .key = "efuse_aux_A", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int8_t){99} }, { .key = "drive_dist", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(int16_t){10} },{ .key = "drive_tpdf", .type_size = TYPE_SIZE_4, .type_flags = 0, .default_val = &(int32_t){70} },{ .key = "drive_mspf", .type_size = TYPE_SIZE_4, .type_flags = 0, .default_val = &(int32_t){1000} },{ .key = "jack_mspi", .type_size = TYPE_SIZE_4, .type_flags = 0, .default_val = &(int32_t){1000} },{ .key = "jack_dist", .type_size = TYPE_SIZE_1, .type_flags = TYPE_SIGNED, .default_val = &(uint8_t){7} }, { .key = "keycode0", .type_size = TYPE_SIZE_8, .type_flags = 0, .default_val = &(uint8_t){0} },{ .key = "keycode1", .type_size = TYPE_SIZE_8, .type_flags = 0, .default_val = &(uint8_t){0} },{ .key = "keycode2", .type_size = TYPE_SIZE_8, .type_flags = 0, .default_val = &(uint8_t){0} },{ .key = "keycode3", .type_size = TYPE_SIZE_8, .type_flags = 0, .default_val = &(uint8_t){0} } }; #define PARAM_COUNT (sizeof(param_table)/sizeof(param_table[0])) // Runtime parameter values static param_value_t param_values[PARAM_COUNT]; const param_group_t param_group_table[] = { { .formatter = status_format, .num_keys = 3, .keys = {"","",""}, .launch_functions = {trigger_move, adjust_rtc_hour, adjust_rtc_min} },{ .formatter = schedule_format, .num_keys = 3, .keys = {"sched_start", "sched_end", "sched_num"}, .launch_functions = {adjust_hour, adjust_hour, adjust_i8_0_99} }, { .formatter = dist_format, .num_keys = 2, .keys = {"drive_dist", "jack_dist"}, .launch_functions = {adjust_i8_0_99, adjust_i8_0_99} }, { .formatter = cal_format, .num_keys = 3, .keys = { "jack_mspi", "drive_mspf", "drive_tpdf"}, .launch_functions = {adjust_i32_smart_0_99999, adjust_i32_smart_0_99999, adjust_i32_smart_0_999} }, { .formatter = efuse_format, .num_keys = 3, .keys = { "efuse_aux_A", "efuse_jack_A", "efuse_drive_A"}, .launch_functions = {adjust_i8_0_99, adjust_i8_0_99, adjust_i8_0_99} }, { .formatter = override_format, .num_keys = 3, .keys = {"","",""}, .launch_functions = {dummy_adjuster, dummy_adjuster, dummy_adjuster} }, { .formatter = reprog_format, .num_keys = 1, .keys = {""}, .launch_functions = {rf_reprogram_remote} }, { .formatter = ftp_format, .num_keys = 1, .keys = {""}, .launch_functions = {launch_ftp} } }; #define PARAM_GROUP_RUNMTR 5 #define PARAM_GROUP_FTP 7 #define PARAM_GROUP_COUNT (sizeof(param_group_table)/sizeof(param_group_table[0])) static const char schedule_fmts[3][3][LCD_BUFLEN] = { { "Start/End xTimes [-] - x%-2d ", "Start/End xTimes - [-] x%-2d ", "Start/End xTimes - - [x%-2d]" },{ "Start/End xTimes[%2d%cM] - x%-2d ", "Start/End xTimes %2d%cM [-] x%-2d ", "Start/End xTimes %2d%cM - [x%-2d]" },{ "Start/End xTimes[%2d%cM]-%2d%cM x%-2d", "Start/End xTimes %2d%cM-[%2d%cM] x%-2d", "Start/End xTimes %2d%cM-%2d%cM [x%-2d]" } }; static const char dist_fmts[3][LCD_BUFLEN] = { "Dist. Drive/Jack[%2d ft] / %2d in ", "Dist. Drive/Jack %2d ft / [%2d in]" }; static const char override_fmts[3][LCD_BUFLEN] = { " Run Motors [AUX]JACK DRIVE ", " Run Motors AUX[JACK]DRIVE ", " Run Motors AUX JACK[DRIVE]" }; static const char cal_fmts[3][LCD_BUFLEN] = { "Jack ms/in: [%4ld]%4ld %4ld ", "Drive ms/ft: %4ld[%4ld]%4ld ", "Drive t/10ft: %4ld %4ld[%4ld]" }; static const char efuse_fmts[3][LCD_BUFLEN] = { "E-fuse Aux: [%2dA] %2dA %2dA ", "E-fuse Jack: %2dA [%2dA] %2dA ", "E-fuse Drive: %2dA %2dA [%2dA]" }; /* All function implementations remain unchanged and appear here in original form */ char* schedule_format(const param_group_t *pg, uint8_t idx) { /* pg->keys[0..2] → "sched_start", "sched_end", "sched_num" */ int8_t start = (int8_t)get_param_i8(pg->keys[0]); // helper, see below int8_t end = (int8_t)get_param_i8(pg->keys[1]); int8_t num = (int8_t)get_param_i8(pg->keys[2]); char startAP = start<12 ? 'A':'P'; char endAP = end<12 ? 'A':'P'; start %= 12; end %= 12; if (start == 0) start = 12; if (end == 0) end = 12; if (num == 0) { snprintf(formatting_buf, sizeof(formatting_buf), schedule_fmts[0][idx], num); return formatting_buf; } else if (num == 1) { snprintf(formatting_buf, sizeof(formatting_buf), schedule_fmts[1][idx], start, startAP, num); return formatting_buf; } else { snprintf(formatting_buf, sizeof(formatting_buf), schedule_fmts[2][idx], start, startAP, end, endAP, num); return formatting_buf; } } char* dist_format(const param_group_t *pg, uint8_t idx) { int8_t drive = (int8_t)get_param_i8(pg->keys[0]); // helper, see below int8_t jack = (int8_t)get_param_i8(pg->keys[1]); snprintf(formatting_buf, sizeof(formatting_buf), dist_fmts[idx], drive, jack); return formatting_buf; } char* reprog_format(const param_group_t *pg, uint8_t idx) { return "Reprogram Keyfob [Press ^ / v ] "; } char* override_format(const param_group_t *pg, uint8_t idx) { return override_fmts[idx]; } char* ftp_format(const param_group_t *pg, uint8_t idx) { return " Start Wifi/FTP [Press ^ / v ] "; } char charge_indicators[N_CHARGE_STATES] = { [CHG_STATE_OFF] ='-', [CHG_STATE_FLOAT] ='F', [CHG_STATE_BULK] ='B' }; static const char status_fmts[4][LCD_BUFLEN] = { "%-6s%2dA %2lu.%02luV[MOVE] %2d:%02d %cM", "%-6s%2dA %2lu.%02luV MOVE [%2d]:%02d %cM", "%-6s%2dA %2lu.%02luV MOVE %2d:[%02d]%cM", "%-6s%2dA %2lu.%02luV[ SET TIME ^/v ]", }; char* status_format(const param_group_t *pg, uint8_t idx) { uint32_t vbat = get_battery_mV(); struct tm timeinfo; rtc_get_time(&timeinfo); // --- Build 7-char time: " 9:05PM" or "10:05PM" --- int hour12 = timeinfo.tm_hour % 12; if (hour12 == 0) hour12 = 12; // 12-hour format int current_draw = abs(get_bridge_mA(BRIDGE_DRIVE)/1000) + abs(get_bridge_mA(BRIDGE_JACK)/1000) + abs(get_bridge_mA(BRIDGE_AUX)/1000); if (rtc_is_set()) snprintf(formatting_buf, sizeof(formatting_buf), status_fmts[idx], "Idle", current_draw, (unsigned long)(vbat / 1000), (unsigned long)((vbat % 1000) + 99) / 100, hour12, timeinfo.tm_min, timeinfo.tm_hour < 12 ? 'A':'P' ); else snprintf(formatting_buf, sizeof(formatting_buf), status_fmts[3], "Idle", current_draw, (unsigned long)(vbat / 1000), (unsigned long)((vbat % 1000) + 99) / 100 ); return formatting_buf; } char* cal_format(const param_group_t *pg, uint8_t idx) { int32_t x1 = get_param_i32(pg->keys[0]); int32_t x2 = get_param_i32(pg->keys[1]); int32_t x3 = get_param_i32(pg->keys[2]); snprintf(formatting_buf, sizeof(formatting_buf), cal_fmts[idx], x1, x2, x3); return formatting_buf; } char* efuse_format(const param_group_t *pg, uint8_t idx) { int32_t x1 = get_param_i32(pg->keys[0]); int32_t x2 = get_param_i32(pg->keys[1]); int32_t x3 = get_param_i32(pg->keys[2]); snprintf(formatting_buf, sizeof(formatting_buf), efuse_fmts[idx], x1, x2, x3); return formatting_buf; } // Generic adjustment fallback void adjust_generic(int idx, int8_t amt) { const param_desc_t *p = ¶m_table[idx]; if (p->type_flags & TYPE_FLOAT) { float step = 0.1f; param_values[idx].f32 += amt; } else { switch (p->type_size) { case 1: { int8_t v = (int8_t)param_values[idx].u8; v += amt; param_values[idx].u8 = (int8_t)v; break; } case 2: { int16_t v = (int16_t)param_values[idx].u16; v += amt; param_values[idx].u16 = (int16_t)v; break; } } } params_save(idx); } /** * adjust_time - Shared adjuster for any time parameter (HH:MM format) * @idx: Index in param_table[] * @dir: +1 = increment, -1 = decrement * * Assumes value stored as minutes since 00:00 (0–1439) * Displays as "HH:MM" */ void adjust_hour(char* key, int8_t dir) { int8_t idx = params_find(key); if (idx<0) return; if (dir>0) param_values[idx].i8 += +1; if (dir<0) param_values[idx].i8 += -1; // wraparound if (param_values[idx].i8 > 23) param_values[idx].i8 = 0; if (param_values[idx].i8 < 0) param_values[idx].i8 = 23; params_save(idx); set_next_alarm(); } void adjust_i8_0_99(char* key, int8_t dir) { int8_t idx = params_find(key); if (idx<0) return; if (dir>0) param_values[idx].i8 += +1; if (dir<0) param_values[idx].i8 += -1; // clamp if (param_values[idx].i8 > 99) param_values[idx].i8 = 99; if (param_values[idx].i8 < 0) param_values[idx].i8 = 0; params_save(idx); set_next_alarm(); } void adjust_i16_0_9990_by_10(char* key, int8_t dir) { int8_t idx = params_find(key); if (idx<0) return; if (dir>0) param_values[idx].i16 += +1; if (dir<0) param_values[idx].i16 += -1; // clamp if (param_values[idx].i16 > 9990) param_values[idx].i16 = 9990; if (param_values[idx].i16 < 0) param_values[idx].i16 = 0; params_save(idx); set_next_alarm(); } //inline static int8_t abs(int8_t x) { return x<0?-x:x; } void adjust_i32_smart_0_99999(char* key, int8_t dir) { int8_t idx = params_find(key); if (idx<0) return; int32_t inc = 1; if (abs(dir) > 5) inc = 5; if (abs(dir) > 10) inc = 10; if (abs(dir) > 13) inc = 50; if (abs(dir) > 16) inc = 100; if (abs(dir) > 19) inc = 200; if (abs(dir) > 22) inc = 1000; if (dir>0) param_values[idx].i32 += +inc; if (dir<0) param_values[idx].i32 += -inc; param_values[idx].i32 = (param_values[idx].i32/inc)*inc; ESP_LOGI("ADJ", "P[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32); // clamp if (param_values[idx].i32 > 99999) param_values[idx].i32 = 99999; if (param_values[idx].i32 < 0) param_values[idx].i32 = 0; params_save(idx); set_next_alarm(); } void adjust_i32_smart_0_999(char* key, int8_t dir) { int8_t idx = params_find(key); if (idx<0) return; int32_t inc = 1; if (abs(dir) > 5) inc = 5; if (abs(dir) > 10) inc = 10; if (abs(dir) > 13) inc = 50; if (abs(dir) > 16) inc = 100; if (abs(dir) > 19) inc = 200; if (abs(dir) > 22) inc = 1000; if (dir>0) param_values[idx].i32 += +inc; if (dir<0) param_values[idx].i32 += -inc; param_values[idx].i32 = (param_values[idx].i32/inc)*inc; ESP_LOGI("ADJ", "p[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32); // clamp if (param_values[idx].i32 > 999) param_values[idx].i32 = 999; if (param_values[idx].i32 < 0) param_values[idx].i32 = 0; params_save(idx); set_next_alarm(); } static int8_t group_idx=0, entry_idx=0; void run_parameter_ui() { if (get_button_repeats(BTN_L)) { reset_shutdown_timer(); entry_idx--; if (entry_idx < 0) { group_idx--; if (group_idx < 0) { group_idx = PARAM_GROUP_COUNT-1; } entry_idx = param_group_table[group_idx].num_keys-1; } } if (get_button_repeats(BTN_R)) { reset_shutdown_timer(); entry_idx++; if (entry_idx >= param_group_table[group_idx].num_keys) { group_idx++; if (group_idx >= PARAM_GROUP_COUNT) { group_idx = 0; } entry_idx = 0; } } // Forbid user from doing anything until they set the time if (!rtc_is_set()) { group_idx=0; entry_idx=1; } param_group_t pg = param_group_table[group_idx]; lcd_print(pg.formatter(&pg, entry_idx)); // Formatted with botfmt + values int8_t n; if ((n=get_button_repeats(BTN_U))) { reset_shutdown_timer(); pg.launch_functions[entry_idx]( pg.keys[entry_idx], +n ); } if ((n=get_button_repeats(BTN_D))) { reset_shutdown_timer(); pg.launch_functions[entry_idx]( pg.keys[entry_idx], -n ); } /*int64_t ut = get_button_ms(BTN_U); if (ut) { reset_shutdown_timer(); pg.launch_functions[entry_idx]( pg.keys[entry_idx], +ut ); } int64_t dt = get_button_ms(BTN_D); if (ut) { reset_shutdown_timer(); pg.launch_functions[entry_idx]( pg.keys[entry_idx], -ut ); }*/ } int8_t parameter_ux_in_override() { if(group_idx != PARAM_GROUP_RUNMTR) return -1; return entry_idx; } bool parameter_ux_in_ftp() { return group_idx == PARAM_GROUP_FTP; }