849 lines
33 KiB
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(;;)
|
|
}
|