commeownder/main/main.c

849 lines
33 KiB
C

#include "config.h"
#include "state.h"
#include "game.h"
#include "draw.h"
#include "ble.h"
#include "events.h"
#include "menus.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/uart.h"
#include "esp_vfs_dev.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_err.h"
#include "esp_random.h"
#include "esp_sleep.h"
#include "esp_timer.h"
#include "driver/rtc_io.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
// ── Global state ──────────────────────────────────────────────────────────────
int g_battery_pct = -1;
int g_charging = 0;
int g_batt_full = 0;
int g_sleep_timeout_min = SLEEP_TIMEOUT_DEF;
static adc_oneshot_unit_handle_t s_adc;
static adc_cali_handle_t s_adc_cali;
static bool s_adc_cali_ok;
// Game
int g_life;
int g_cmdr_damage[MAX_OPPONENTS];
int g_counters[NUM_COUNTERS];
int g_eliminated;
// UI
int g_active_menu;
int g_active_opponent;
int g_active_counter;
int g_active_player;
int g_active_setting;
int g_name_cursor;
int g_life_select;
// Settings
int g_brightness_pct = 10;
int g_start_life_index = 2;
int g_num_opponents = 3;
char g_player_name[PLAYER_NAME_LEN + 1] = "PLAYER 1";
int g_ble_enabled = 0;
uint8_t g_led_max = 26;
int g_game_id_cursor = 0;
uint8_t g_game_id[2] = {BLE_GAME_ID_0, BLE_GAME_ID_1};
int g_menu_hold_ms = 500;
int g_lr_hold_ms = 500;
int g_display_flip = 0;
// Peers
ble_peer_t g_peers[MAX_BLE_PEERS];
// Battery blink state toggled by batt timer
bool g_batt_blink;
// Life delta overlay
int g_life_delta = 0;
int g_delta_timeout_ms = 1000;
// Dice
int g_dice_num = 1;
int g_dice_sides = 5;
int g_dice_item = 0;
char g_dice_csv[DICE_CSV_LEN];
int g_dice_rolled = 0;
int g_dice_sum = 0;
// ── Event queue + timer handles ───────────────────────────────────────────────
static QueueHandle_t g_evt_queue;
static esp_timer_handle_t s_hold_timer[4];
static esp_timer_handle_t s_combo_all4_timer;
static esp_timer_handle_t s_combo_fb_timer;
static esp_timer_handle_t s_batt_timer;
static esp_timer_handle_t s_peer_timer;
static esp_timer_handle_t s_autosave_timer;
static esp_timer_handle_t s_life_delta_timer;
static esp_timer_handle_t s_reset_cmd_timer;
static esp_timer_handle_t s_idle_timer;
static esp_timer_handle_t s_led_breathe_timer;
// ── Button ISR state ──────────────────────────────────────────────────────────
static const int BTN_GPIOS[4] = {
BTN_FORWARD_GPIO, BTN_LEFT_GPIO, BTN_RIGHT_GPIO, BTN_BACK_GPIO
};
static int64_t s_press_time_us[4];
static int64_t s_last_event_us[4];
static uint8_t s_pressed_mask;
static bool s_btn_in_combo[4];
// ── LED breathing state ───────────────────────────────────────────────────────
static float s_breath_phase;
static bool s_breathe_active;
// ── Misc ─────────────────────────────────────────────────────────────────────
static bool s_settings_dirty;
static int s_pre_settings_menu = 0;
// ── RTC memory (survives deep sleep) ─────────────────────────────────────────
RTC_DATA_ATTR static bool s_rtc_valid;
RTC_DATA_ATTR static int s_rtc_life, s_rtc_eliminated;
RTC_DATA_ATTR static int s_rtc_cmdr_damage[MAX_OPPONENTS];
RTC_DATA_ATTR static int s_rtc_counters[NUM_COUNTERS];
RTC_DATA_ATTR static int s_rtc_active_menu, s_rtc_active_opponent;
RTC_DATA_ATTR static int s_rtc_active_counter, s_rtc_active_player;
RTC_DATA_ATTR static int s_rtc_active_setting, s_rtc_name_cursor;
RTC_DATA_ATTR static int s_rtc_life_select;
RTC_DATA_ATTR static int s_rtc_sleep_mode;
RTC_DATA_ATTR static int s_rtc_ble_pre_sleep;
#define MARK_DIRTY() do { \
s_settings_dirty = true; \
esp_timer_stop(s_autosave_timer); \
esp_timer_start_once(s_autosave_timer, (int64_t)SAVE_DELAY * 10000LL); \
} while(0)
#define RESTART_LIFE_DELTA() do { \
esp_timer_stop(s_life_delta_timer); \
esp_timer_start_once(s_life_delta_timer, (int64_t)g_delta_timeout_ms * 1000LL); \
} while(0)
void mark_dirty(void) {
s_settings_dirty = true;
esp_timer_stop(s_autosave_timer);
esp_timer_start_once(s_autosave_timer, (int64_t)SAVE_DELAY * 10000LL);
}
void restart_life_delta_timer(void) {
esp_timer_stop(s_life_delta_timer);
esp_timer_start_once(s_life_delta_timer, (int64_t)g_delta_timeout_ms * 1000LL);
}
void start_reset_cmd_timer(void) {
esp_timer_stop(s_reset_cmd_timer);
esp_timer_start_once(s_reset_cmd_timer, 15000000LL);
}
void restart_idle_timer(void) {
if (g_sleep_timeout_min > 0) {
esp_timer_stop(s_idle_timer);
esp_timer_start_once(s_idle_timer,
(int64_t)g_sleep_timeout_min * 60LL * 1000000LL);
}
}
// ── ADC battery helpers ───────────────────────────────────────────────────────
static void sample_battery(void) {
static int raw_buf[100];
static int raw_idx = 0;
static int raw_count = 0;
int raw;
adc_oneshot_read(s_adc, ADC_CHANNEL_6, &raw);
raw_buf[raw_idx] = raw;
raw_idx = (raw_idx + 1) % 100;
if (raw_count < 100) raw_count++;
int sum = 0;
for (int i = 0; i < raw_count; i++) sum += raw_buf[i];
int avg_raw = sum / raw_count;
int voltage_mv;
if (s_adc_cali_ok)
adc_cali_raw_to_voltage(s_adc_cali, avg_raw, &voltage_mv);
else
voltage_mv = avg_raw * 3100 / 4095;
int comp_mv = g_ble_enabled ? BATT_LOAD_COMP_BLE_MV : BATT_LOAD_COMP_MV;
int cell_mv = voltage_mv * 2 + comp_mv;
static const int lipo_mv[] = {3000,3300,3400,3500,3600,3700,3800,3900,4000,4100,4200};
static const int lipo_pct[] = { 0, 3, 8, 18, 34, 50, 62, 72, 81, 92, 100};
static const int N = sizeof(lipo_mv) / sizeof(lipo_mv[0]);
int pct;
if (cell_mv <= lipo_mv[0]) pct = 0;
else if (cell_mv >= lipo_mv[N-1]) pct = 100;
else {
int i = 0;
while (i < N-2 && cell_mv >= lipo_mv[i+1]) i++;
pct = lipo_pct[i] + (cell_mv - lipo_mv[i]) *
(lipo_pct[i+1] - lipo_pct[i]) / (lipo_mv[i+1] - lipo_mv[i]);
}
g_battery_pct = pct;
oled_draw_header();
}
// ── LED breathing sync ────────────────────────────────────────────────────────
void led_sync(bool changed) {
int bthresh = start_life_opts[g_start_life_index] / 4;
bool should = (g_life > 0 && g_life < bthresh);
if (should && !s_breathe_active) {
s_breathe_active = true;
esp_timer_start_periodic(s_led_breathe_timer, 10000);
} else if (!should && s_breathe_active) {
s_breathe_active = false;
s_breath_phase = 0.0f;
esp_timer_stop(s_led_breathe_timer);
if (g_life == 0) led_update_for_count(0, 255);
else if (changed) led_update_for_count(g_life, 255);
} else if (!should && changed) {
led_update_for_count(g_life, 255);
}
}
// ── Button action helpers ─────────────────────────────────────────────────────
static void nav_short(int d) {
menus_nav_short(d);
}
static void nav_long(int d) {
int last = NUM_MENU_SLOTS - 1;
if (g_active_menu == last) return;
int next = g_active_menu;
do {
next = (next + d + last) % last;
} while (menus[next] == NULL);
g_active_menu = next;
esp_timer_stop(s_hold_timer[BTN_IDX_LEFT]);
esp_timer_stop(s_hold_timer[BTN_IDX_RIGHT]);
oled_draw_header();
menus_draw();
}
static void lr_delta(int d) {
menus_lr_delta(d);
}
// ── Deep sleep ────────────────────────────────────────────────────────────────
static void enter_deep_sleep(int manual, int ble_pre) {
s_rtc_valid = true;
s_rtc_life = g_life;
s_rtc_eliminated = g_eliminated;
memcpy(s_rtc_cmdr_damage, g_cmdr_damage, sizeof(g_cmdr_damage));
memcpy(s_rtc_counters, g_counters, sizeof(g_counters));
s_rtc_active_menu = g_active_menu;
s_rtc_active_opponent = g_active_opponent;
s_rtc_active_counter = g_active_counter;
s_rtc_active_player = g_active_player;
s_rtc_active_setting = g_active_setting;
s_rtc_name_cursor = g_name_cursor;
s_rtc_life_select = g_life_select;
s_rtc_sleep_mode = manual;
s_rtc_ble_pre_sleep = ble_pre;
if (s_settings_dirty) settings_save();
for (int i = 0; i < 4; i++) esp_timer_stop(s_hold_timer[i]);
esp_timer_stop(s_combo_all4_timer); esp_timer_stop(s_combo_fb_timer);
esp_timer_stop(s_batt_timer); esp_timer_stop(s_peer_timer);
esp_timer_stop(s_autosave_timer); esp_timer_stop(s_life_delta_timer);
esp_timer_stop(s_reset_cmd_timer); esp_timer_stop(s_idle_timer);
esp_timer_stop(s_led_breathe_timer);
g_ble_enabled = 0;
ble_adv_update();
if (manual) {
oled_draw_sleep();
oled_set_contrast(SLEEP_CONTRAST);
} else {
oled_set_contrast(0);
}
led_off();
if (manual) {
// Wait for all buttons to release before sleeping, otherwise EXT1 ALL_LOW
// triggers immediately (all 4 are still held from the wakeup combo).
while (!gpio_get_level(BTN_FORWARD_GPIO) || !gpio_get_level(BTN_LEFT_GPIO) ||
!gpio_get_level(BTN_RIGHT_GPIO) || !gpio_get_level(BTN_BACK_GPIO))
vTaskDelay(pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(50));
// Manual sleep: wake when all 4 pressed simultaneously (deliberate gesture).
uint64_t wake_mask = (1ULL << BTN_FORWARD_GPIO) | (1ULL << BTN_LEFT_GPIO)
| (1ULL << BTN_RIGHT_GPIO) | (1ULL << BTN_BACK_GPIO);
for (int i = 0; i < 4; i++) {
rtc_gpio_pullup_en(BTN_GPIOS[i]);
rtc_gpio_hold_en(BTN_GPIOS[i]);
}
esp_sleep_enable_ext1_wakeup(wake_mask, ESP_EXT1_WAKEUP_ALL_LOW);
} else {
// Auto sleep: wake on LEFT button press (GPIO2 is the only RTC GPIO
// among our 4 buttons that supports EXT0 single-pin wakeup).
rtc_gpio_pullup_en(BTN_LEFT_GPIO);
rtc_gpio_hold_en(BTN_LEFT_GPIO);
esp_sleep_enable_ext0_wakeup(BTN_LEFT_GPIO, 0);
}
esp_deep_sleep_start();
}
// ── Timer callbacks ───────────────────────────────────────────────────────────
static void cb_hold(void *arg) {
int bi = (int)arg;
app_event_t e = {.type = EVT_HOLD_EXPIRE, .btn_idx = bi};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_combo_all4(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_COMBO_SLEEP};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_combo_fb(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_COMBO_SETTINGS};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_batt(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_BATT_TICK};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_peer(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_PEER_TICK};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_autosave(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_AUTOSAVE};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_life_delta(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_LIFE_DELTA_EXP};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_reset_cmd(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_RESET_CMD_EXP};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_idle(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_IDLE_SLEEP};
xQueueSend(g_evt_queue, &e, 0);
}
static void cb_led_breathe(void *arg) {
(void)arg;
app_event_t e = {.type = EVT_LED_BREATHE};
xQueueSend(g_evt_queue, &e, 0);
}
// ── GPIO ISR ──────────────────────────────────────────────────────────────────
static void IRAM_ATTR btn_isr(void *arg) {
int gpio = (int)arg;
app_event_t e = {
.type = EVT_BUTTON,
.gpio = gpio,
.level = gpio_get_level(gpio),
.ts_us = esp_timer_get_time(),
};
BaseType_t woken = pdFALSE;
xQueueSendFromISR(g_evt_queue, &e, &woken);
portYIELD_FROM_ISR(woken);
}
// ── Button event handler ──────────────────────────────────────────────────────
static int gpio_to_btn(int gpio) {
for (int i = 0; i < 4; i++)
if (BTN_GPIOS[i] == gpio) return i;
return -1;
}
static void handle_button(const app_event_t *e) {
int bi = gpio_to_btn(e->gpio);
if (bi < 0) return;
int64_t ts = e->ts_us;
// Debounce: ignore events within 20ms of last event on same GPIO
if (ts - s_last_event_us[bi] < 20000) return;
s_last_event_us[bi] = ts;
if (e->level == 0) {
// ── FALLING edge (button pressed) ────────────────────────────────────
s_pressed_mask |= (uint8_t)(1u << bi);
s_press_time_us[bi] = ts;
restart_idle_timer();
// Check all-4 combo
if (s_pressed_mask == 0xF) {
for (int i = 0; i < 4; i++) {
s_btn_in_combo[i] = true;
esp_timer_stop(s_hold_timer[i]);
}
esp_timer_stop(s_combo_fb_timer);
esp_timer_stop(s_combo_all4_timer);
esp_timer_start_once(s_combo_all4_timer,
(int64_t)COMBO_HOLD_SLEEP * 10000LL);
return;
}
// Check fwd+back combo
if ((s_pressed_mask & 0x9) == 0x9) { // bits 0 (fwd) and 3 (back)
s_btn_in_combo[BTN_IDX_FWD] = true;
s_btn_in_combo[BTN_IDX_BACK] = true;
esp_timer_stop(s_hold_timer[BTN_IDX_FWD]);
esp_timer_stop(s_hold_timer[BTN_IDX_BACK]);
esp_timer_stop(s_combo_fb_timer);
esp_timer_start_once(s_combo_fb_timer,
(int64_t)COMBO_HOLD_SETTINGS * 10000LL);
return;
}
// Check left+right simultaneous: don't fire either
if ((s_pressed_mask & 0x6) == 0x6) { // bits 1 (left) and 2 (right)
esp_timer_stop(s_hold_timer[BTN_IDX_LEFT]);
esp_timer_stop(s_hold_timer[BTN_IDX_RIGHT]);
return;
}
if (s_btn_in_combo[bi]) return;
if (bi == BTN_IDX_LEFT || bi == BTN_IDX_RIGHT) {
// LR: fire delta immediately on press
lr_delta(bi == BTN_IDX_RIGHT ? 1 : -1);
// Start hold timer for repeat
esp_timer_stop(s_hold_timer[bi]);
esp_timer_start_once(s_hold_timer[bi],
(int64_t)g_lr_hold_ms * 1000LL);
} else {
// FWD/BACK: start hold timer; action dispatched on expire or release
esp_timer_stop(s_hold_timer[bi]);
esp_timer_start_once(s_hold_timer[bi],
(int64_t)g_menu_hold_ms * 1000LL);
}
} else {
// ── RISING edge (button released) ────────────────────────────────────
int64_t hold_ms = (ts - s_press_time_us[bi]) / 1000;
esp_timer_stop(s_hold_timer[bi]);
s_pressed_mask &= (uint8_t)~(1u << bi);
// Cancel combo timers if conditions no longer met
if (s_pressed_mask != 0xF)
esp_timer_stop(s_combo_all4_timer);
if ((s_pressed_mask & 0x9) != 0x9)
esp_timer_stop(s_combo_fb_timer);
bool was_in_combo = s_btn_in_combo[bi];
s_btn_in_combo[bi] = false;
if (was_in_combo) return;
if ((bi == BTN_IDX_FWD || bi == BTN_IDX_BACK) && hold_ms < g_menu_hold_ms)
nav_short(bi == BTN_IDX_FWD ? 1 : -1);
// LR: no action on release (action fires on press and hold-repeat)
}
}
// ── Hold expire handler ───────────────────────────────────────────────────────
static void handle_hold_expire(int bi) {
if (!(s_pressed_mask & (1u << bi))) return; // released before timer fired
if (bi == BTN_IDX_LEFT || bi == BTN_IDX_RIGHT) {
lr_delta(bi == BTN_IDX_RIGHT ? 1 : -1);
// Repeat at HOLD_REPEAT interval
esp_timer_start_once(s_hold_timer[bi], (int64_t)HOLD_REPEAT * 10000LL);
} else {
// FWD or BACK long press: cycle menu
nav_long(bi == BTN_IDX_FWD ? 1 : -1);
esp_timer_start_once(s_hold_timer[bi],
(int64_t)g_menu_hold_ms * 1000LL);
}
}
// ── Serial command task ───────────────────────────────────────────────────────
#ifdef DEBUG
static void serial_print_state(void) {
printf("DBG STATE life=%d poison=%u cmdr=[%d,%d,%d,%d] counters=[%d,%d,%d] menu=%d ble=%d eliminated=%d\n",
g_life, (unsigned)g_counters[0],
g_cmdr_damage[0], g_cmdr_damage[1], g_cmdr_damage[2], g_cmdr_damage[3],
g_counters[0], g_counters[1], g_counters[2],
g_active_menu, g_ble_enabled, g_eliminated);
fflush(stdout);
}
static void serial_cmd_task(void *arg) {
(void)arg;
char buf[64];
int pos = 0;
for (;;) {
int c = fgetc(stdin);
if (c < 0) { vTaskDelay(pdMS_TO_TICKS(10)); continue; }
if (c == '\n' || c == '\r') {
if (pos > 0) {
buf[pos] = '\0';
pos = 0;
int val = 0;
if (strncmp(buf, "SET ", 4) == 0) {
const char *kv = buf + 4;
if (sscanf(kv, "life=%d", &val) == 1) {
g_life = val; check_elimination();
}
if (sscanf(kv, "ble=%d", &val) == 1) {
g_ble_enabled = val ? 1 : 0;
}
{
char fmt[14];
for (int i = 0; i < MAX_OPPONENTS; i++) {
snprintf(fmt, sizeof(fmt), "cmdr%d=%%d", i);
if (sscanf(kv, fmt, &val) == 1) {
g_cmdr_damage[i] = val < 0 ? 0 : val;
check_elimination(); break;
}
}
for (int i = 0; i < NUM_COUNTERS; i++) {
snprintf(fmt, sizeof(fmt), "counter%d=%%d", i);
if (sscanf(kv, fmt, &val) == 1) {
g_counters[i] = val < 0 ? 0 : val;
check_elimination(); break;
}
}
}
ble_adv_update();
serial_print_state();
} else if (strcmp(buf, "RESET") == 0) {
game_reset();
ble_adv_update();
serial_print_state();
} else if (strcmp(buf, "CLEARNVS") == 0) {
nvs_handle_t nvs;
if (nvs_open(NVS_NS, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_erase_all(nvs); nvs_commit(nvs); nvs_close(nvs);
}
settings_reset_defaults();
game_reset();
ble_adv_update();
serial_print_state();
} else if (strcmp(buf, "STATE") == 0) {
serial_print_state();
}
}
} else if (pos < (int)sizeof(buf) - 1) {
buf[pos++] = (char)c;
}
}
}
#endif // DEBUG
// ── app_main ──────────────────────────────────────────────────────────────────
void app_main(void)
{
led_init();
i2c_config_t i2c_cfg = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_SDA_GPIO, .scl_io_num = I2C_SCL_GPIO,
.sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_FREQ_HZ,
};
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &i2c_cfg));
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
esp_err_t nvs_err = nvs_flash_init();
if (nvs_err == ESP_ERR_NVS_NO_FREE_PAGES || nvs_err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase(); nvs_flash_init();
}
g_life = start_life_opts[g_start_life_index];
settings_load();
menus_init();
check_elimination();
g_led_max = (uint8_t)(255 * g_brightness_pct / 100);
// ADC for battery voltage (GPIO34, ADC1 CH6, 100k/100k divider)
adc_oneshot_unit_init_cfg_t adc_cfg = {.unit_id = ADC_UNIT_1};
adc_oneshot_new_unit(&adc_cfg, &s_adc);
adc_oneshot_chan_cfg_t ch_cfg = {
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
adc_oneshot_config_channel(s_adc, ADC_CHANNEL_6, &ch_cfg);
adc_cali_line_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
s_adc_cali_ok = (adc_cali_create_scheme_line_fitting(&cali_cfg, &s_adc_cali) == ESP_OK);
oled_init();
oled_set_flip(g_display_flip);
oled_clear();
// Button GPIO config: ANYEDGE ISRs
gpio_config_t gpio_cfg = {
.pin_bit_mask = (1ULL<<BTN_FORWARD_GPIO)|(1ULL<<BTN_LEFT_GPIO)
|(1ULL<<BTN_RIGHT_GPIO)|(1ULL<<BTN_BACK_GPIO),
.mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_ANYEDGE,
};
gpio_config(&gpio_cfg);
gpio_config_t charge_cfg = {
.pin_bit_mask = (1ULL<<BATT_CHARGE_GPIO) | (1ULL<<BATT_FULL_GPIO),
.mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&charge_cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(BTN_FORWARD_GPIO, btn_isr, (void *)BTN_FORWARD_GPIO);
gpio_isr_handler_add(BTN_LEFT_GPIO, btn_isr, (void *)BTN_LEFT_GPIO);
gpio_isr_handler_add(BTN_RIGHT_GPIO, btn_isr, (void *)BTN_RIGHT_GPIO);
gpio_isr_handler_add(BTN_BACK_GPIO, btn_isr, (void *)BTN_BACK_GPIO);
ble_init();
#ifdef DEBUG
uart_driver_install(UART_NUM_0, 256, 0, 0, NULL, 0);
esp_vfs_dev_uart_use_driver(0);
setvbuf(stdin, NULL, _IONBF, 0);
xTaskCreate(serial_cmd_task, "serial_cmd", 4096, NULL, 3, NULL);
#endif
// ── Create event queue ────────────────────────────────────────────────────
g_evt_queue = xQueueCreate(16, sizeof(app_event_t));
// ── Create timers ─────────────────────────────────────────────────────────
for (int i = 0; i < 4; i++) {
esp_timer_create_args_t a = {
.callback = cb_hold, .arg = (void *)i,
.name = "hold", .dispatch_method = ESP_TIMER_TASK,
};
esp_timer_create(&a, &s_hold_timer[i]);
}
{
esp_timer_create_args_t a = {.callback=cb_combo_all4, .name="all4"};
esp_timer_create(&a, &s_combo_all4_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_combo_fb, .name="fb"};
esp_timer_create(&a, &s_combo_fb_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_batt, .name="batt"};
esp_timer_create(&a, &s_batt_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_peer, .name="peer"};
esp_timer_create(&a, &s_peer_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_autosave, .name="save"};
esp_timer_create(&a, &s_autosave_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_life_delta, .name="delta"};
esp_timer_create(&a, &s_life_delta_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_reset_cmd, .name="rcmd"};
esp_timer_create(&a, &s_reset_cmd_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_idle, .name="idle"};
esp_timer_create(&a, &s_idle_timer);
}
{
esp_timer_create_args_t a = {.callback=cb_led_breathe, .name="breathe"};
esp_timer_create(&a, &s_led_breathe_timer);
}
// Start periodic timers
esp_timer_start_periodic(s_batt_timer, 500000LL);
esp_timer_start_periodic(s_peer_timer, 1000000LL);
restart_idle_timer();
// Restore state after deep sleep wakeup
esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause();
if (s_rtc_valid && (wakeup == ESP_SLEEP_WAKEUP_EXT0 ||
wakeup == ESP_SLEEP_WAKEUP_EXT1)) {
for (int i = 0; i < 4; i++) rtc_gpio_hold_dis(BTN_GPIOS[i]);
g_life = s_rtc_life;
g_eliminated = s_rtc_eliminated;
memcpy(g_cmdr_damage, s_rtc_cmdr_damage, sizeof(g_cmdr_damage));
memcpy(g_counters, s_rtc_counters, sizeof(g_counters));
g_active_menu = s_rtc_active_menu;
g_active_opponent = s_rtc_active_opponent;
g_active_counter = s_rtc_active_counter;
g_active_player = s_rtc_active_player;
g_active_setting = s_rtc_active_setting;
g_name_cursor = s_rtc_name_cursor;
g_life_select = s_rtc_life_select;
g_ble_enabled = s_rtc_ble_pre_sleep;
g_led_max = (uint8_t)(255 * g_brightness_pct / 100);
menus_init();
if (g_active_menu < 0 || g_active_menu >= NUM_MENU_SLOTS ||
(menus[g_active_menu] == NULL && g_active_menu != NUM_MENU_SLOTS - 1))
g_active_menu = 0;
if (g_ble_enabled) ble_adv_update();
}
oled_draw_header();
menus_draw();
led_sync(true);
#ifdef DEBUG
serial_print_state();
#endif
// ── Main event loop ───────────────────────────────────────────────────────
app_event_t evt;
for (;;) {
xQueueReceive(g_evt_queue, &evt, portMAX_DELAY);
switch (evt.type) {
// ── Button edge ───────────────────────────────────────────────────────
case EVT_BUTTON:
handle_button(&evt);
break;
// ── Hold timer expired ────────────────────────────────────────────────
case EVT_HOLD_EXPIRE:
handle_hold_expire(evt.btn_idx);
break;
// ── All-4 sleep combo ─────────────────────────────────────────────────
case EVT_COMBO_SLEEP:
if (s_pressed_mask != 0xF) break;
enter_deep_sleep(1, g_ble_enabled);
break; // not reached
// ── Fwd+back settings combo ───────────────────────────────────────────
case EVT_COMBO_SETTINGS:
if ((s_pressed_mask & 0x9) != 0x9) break;
esp_timer_stop(s_hold_timer[BTN_IDX_LEFT]);
esp_timer_stop(s_hold_timer[BTN_IDX_RIGHT]);
{
int last = NUM_MENU_SLOTS - 1;
if (g_active_menu == last) {
g_active_menu = s_pre_settings_menu;
} else {
s_pre_settings_menu = g_active_menu;
g_active_menu = last;
g_active_setting = 0;
g_name_cursor = 0;
g_game_id_cursor = 0;
}
}
oled_draw_header();
menus_draw();
break;
// ── Battery tick ──────────────────────────────────────────────────────
case EVT_BATT_TICK: {
// Charging/full GPIO state
int charging = !gpio_get_level(BATT_CHARGE_GPIO);
int batt_full = !gpio_get_level(BATT_FULL_GPIO);
if (charging != g_charging || batt_full != g_batt_full) {
g_charging = charging;
g_batt_full = batt_full;
oled_draw_header();
}
// Low battery blink
if (g_battery_pct >= 0 && g_battery_pct < BATT_LOW_PCT) {
g_batt_blink = !g_batt_blink;
oled_draw_header();
}
// ADC sample
sample_battery();
break;
}
// ── Peer expiry + players refresh ─────────────────────────────────────
case EVT_PEER_TICK:
for (int i = 0; i < MAX_BLE_PEERS; i++) {
if (g_peers[i].active &&
esp_timer_get_time() - g_peers[i].last_seen >
(int64_t)BLE_PEER_TIMEOUT * 1000LL) {
#ifdef DEBUG
printf("DBG PEER_EXPIRE slot=%d name=%-8.8s\n", i, g_peers[i].name);
fflush(stdout);
#endif
if (g_active_menu == MENU_LIFE && g_life_select >= 3 && g_ble_enabled) {
int slot = 0;
for (int j = 0; j < i; j++) if (g_peers[j].active) slot++;
if (g_life_select - 3 == slot) { g_life_select = 0; menus_draw(); }
}
g_peers[i].active = 0;
}
}
if (g_active_menu == MENU_CMDR && g_ble_enabled)
oled_draw_players();
// Handle pending remote reset
if (g_reset_requested) {
g_reset_requested = 0;
game_reset();
MARK_DIRTY();
ble_adv_update();
oled_draw_header();
menus_draw();
led_sync(true);
}
break;
// ── Autosave ──────────────────────────────────────────────────────────
case EVT_AUTOSAVE:
if (s_settings_dirty) {
settings_save();
s_settings_dirty = false;
}
break;
// ── Life delta timeout ────────────────────────────────────────────────
case EVT_LIFE_DELTA_EXP:
if (menus[MENU_LIFE] && menus[MENU_LIFE]->on_tick)
menus[MENU_LIFE]->on_tick(&evt);
break;
// ── Reset cmd broadcast end ───────────────────────────────────────────
case EVT_RESET_CMD_EXP:
g_reset_cmd_ticks = 0;
ble_adv_update();
break;
// ── Idle sleep ────────────────────────────────────────────────────────
case EVT_IDLE_SLEEP:
enter_deep_sleep(0, g_ble_enabled);
break; // not reached
// ── LED breathing frame ───────────────────────────────────────────────
case EVT_LED_BREATHE: {
int bthresh = start_life_opts[g_start_life_index] / 4;
if (g_life > 0 && g_life < bthresh) {
float speed = 1.0f +
((float)bthresh - g_life) / (float)(bthresh - 1) * 2.0f;
s_breath_phase += 2.0f * 3.14159265f * speed / 200.0f;
uint8_t sc = (uint8_t)(127.5f + 127.5f * sinf(s_breath_phase));
led_update_for_count(g_life, sc);
}
break;
}
} // switch
} // for(;;)
}