813 lines
36 KiB
C
813 lines
36 KiB
C
#include "config.h"
|
|
#include "state.h"
|
|
#include "game.h"
|
|
#include "draw.h"
|
|
#include "ble.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_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 <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
// ── Global state ──────────────────────────────────────────────────────────────
|
|
int g_sleep_mode = 0;
|
|
int g_battery_pct = -1;
|
|
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; // 40
|
|
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];
|
|
|
|
// Timing
|
|
uint32_t g_tick;
|
|
|
|
// Life delta overlay
|
|
int g_life_delta = 0;
|
|
int g_life_delta_tick = 0;
|
|
int g_delta_timeout_ms = 1000;
|
|
|
|
// Dice
|
|
int g_dice_num = 1;
|
|
int g_dice_sides = 5; // index into die_sides: d20
|
|
int g_dice_item = 0;
|
|
char g_dice_csv[DICE_CSV_LEN];
|
|
int g_dice_rolled = 0;
|
|
int g_dice_sum = 0;
|
|
|
|
// ── Serial command task ───────────────────────────────────────────────────────
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 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();
|
|
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();
|
|
|
|
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_DISABLE,
|
|
};
|
|
gpio_config(&gpio_cfg);
|
|
|
|
ble_init();
|
|
|
|
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);
|
|
|
|
oled_draw_header();
|
|
oled_draw_life();
|
|
led_update_for_count(g_life, 255);
|
|
|
|
#ifdef DEBUG
|
|
serial_print_state();
|
|
#endif
|
|
|
|
int prev_fwd=1, prev_right=1, prev_left=1, prev_back=1;
|
|
int hold_fwd=0, hold_back=0, hold_right=0, hold_left=0;
|
|
int combo_tick=0, combo_fired_hold=0;
|
|
int fwd_in_combo=0, back_in_combo=0;
|
|
int fwd_menu_fired=0, back_menu_fired=0;
|
|
// Require 2 consecutive released ticks before arming hold; prevents accidental
|
|
// hold fires when rapid taps have sub-10ms releases that the sampler misses.
|
|
int release_r=2, release_l=2;
|
|
int hold_armed_r=1, hold_armed_l=1;
|
|
int players_tick=0;
|
|
int settings_dirty=0, settings_save_tick=0;
|
|
int sleep_tick=0, sleep_fired=0, ble_pre_sleep=0;
|
|
uint32_t idle_ticks=0, batt_tick=0;
|
|
|
|
gpio_wakeup_enable(BTN_FORWARD_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
gpio_wakeup_enable(BTN_LEFT_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
gpio_wakeup_enable(BTN_RIGHT_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
gpio_wakeup_enable(BTN_BACK_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
esp_sleep_enable_gpio_wakeup();
|
|
#define MARK_DIRTY() do { settings_dirty = 1; settings_save_tick = 0; } while (0)
|
|
float breath_phase=0.0f;
|
|
|
|
char opponent_labels[MAX_OPPONENTS][PLAYER_NAME_LEN+1];
|
|
const char *opponent_label_ptrs[MAX_OPPONENTS];
|
|
|
|
while (1) {
|
|
g_tick++;
|
|
int menu_ticks = g_menu_hold_ms / 10;
|
|
int lr_ticks = g_lr_hold_ms / 10;
|
|
int fwd = gpio_get_level(BTN_FORWARD_GPIO);
|
|
int right = gpio_get_level(BTN_RIGHT_GPIO);
|
|
int left = gpio_get_level(BTN_LEFT_GPIO);
|
|
int back = gpio_get_level(BTN_BACK_GPIO);
|
|
int changed = 0;
|
|
|
|
// ── Combo: all 4 buttons → sleep toggle ──────────────────────────────
|
|
int all4 = (fwd==0 && back==0 && left==0 && right==0);
|
|
if (all4) {
|
|
sleep_tick++;
|
|
if (!sleep_fired && sleep_tick == COMBO_HOLD_SLEEP) {
|
|
sleep_fired = 1;
|
|
hold_fwd = hold_back = hold_left = hold_right = 0;
|
|
combo_tick = 0; combo_fired_hold = 0;
|
|
if (!g_sleep_mode) {
|
|
g_sleep_mode = 1;
|
|
ble_pre_sleep = g_ble_enabled;
|
|
oled_draw_sleep();
|
|
oled_set_contrast(SLEEP_CONTRAST);
|
|
led_off();
|
|
if (g_ble_enabled) { g_ble_enabled = 0; ble_adv_update(); }
|
|
} else {
|
|
g_sleep_mode = 0;
|
|
oled_set_contrast(0xCF);
|
|
g_ble_enabled = ble_pre_sleep;
|
|
if (g_ble_enabled) ble_adv_update();
|
|
oled_draw_header();
|
|
breath_phase = 0.0f;
|
|
led_update_for_count(g_life, 255);
|
|
changed = 1;
|
|
}
|
|
}
|
|
} else {
|
|
sleep_tick = 0; sleep_fired = 0;
|
|
}
|
|
|
|
// ── Sleep mode: skip all further processing ───────────────────────────
|
|
if (g_sleep_mode) {
|
|
prev_fwd = fwd; prev_right = right; prev_left = left; prev_back = back;
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
continue;
|
|
}
|
|
|
|
// ── Combo: FORWARD + BACK simultaneously (settings toggle) ──────────
|
|
int both = (fwd == 0 && back == 0);
|
|
|
|
if (both) {
|
|
hold_fwd = hold_back = 0;
|
|
fwd_in_combo = back_in_combo = 1;
|
|
fwd_menu_fired = back_menu_fired = 0;
|
|
combo_tick++;
|
|
if (!combo_fired_hold && combo_tick == COMBO_HOLD_SETTINGS) {
|
|
if (g_active_menu == MENU_SETTINGS) {
|
|
g_active_menu = 0;
|
|
} else {
|
|
g_active_menu = MENU_SETTINGS;
|
|
g_active_setting = 0;
|
|
g_name_cursor = 0;
|
|
g_game_id_cursor = 0;
|
|
}
|
|
combo_fired_hold = 1;
|
|
hold_right = hold_left = players_tick = 0;
|
|
oled_draw_header();
|
|
changed = 1;
|
|
}
|
|
} else {
|
|
combo_tick = 0;
|
|
combo_fired_hold = 0;
|
|
|
|
// ── FORWARD: long press = next menu (+ repeat), short press = sub-item fwd ──
|
|
if (fwd == 0) {
|
|
hold_fwd++;
|
|
if (g_active_menu != MENU_SETTINGS && !fwd_in_combo) {
|
|
if (hold_fwd == menu_ticks ||
|
|
(hold_fwd > menu_ticks && (hold_fwd - menu_ticks) % menu_ticks == 0)) {
|
|
g_active_menu = (g_active_menu + 1) % (NUM_MENUS - 1);
|
|
hold_right = hold_left = players_tick = 0;
|
|
oled_draw_header();
|
|
changed = 1;
|
|
fwd_menu_fired = 1;
|
|
}
|
|
}
|
|
} else {
|
|
if (prev_fwd == 0 && !fwd_in_combo && !fwd_menu_fired) {
|
|
if (hold_fwd < menu_ticks) {
|
|
if (g_active_menu == MENU_SETTINGS) {
|
|
if (g_active_setting == SET_PLAYER_NAME) {
|
|
g_name_cursor++;
|
|
if (g_name_cursor >= PLAYER_NAME_LEN) {
|
|
g_name_cursor = 0;
|
|
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
|
g_game_id_cursor = 0;
|
|
}
|
|
} else if (g_active_setting == SET_GAME_ID) {
|
|
g_game_id_cursor++;
|
|
if (g_game_id_cursor >= GAME_ID_DIGITS) {
|
|
g_game_id_cursor = 0;
|
|
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
|
}
|
|
} else {
|
|
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
|
g_name_cursor = 0;
|
|
g_game_id_cursor = 0;
|
|
}
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_CMDR) {
|
|
if (g_ble_enabled) {
|
|
int next = (g_active_player + 1) % (MAX_BLE_PEERS + 1);
|
|
while (next != g_active_player && next != 0 && !g_peers[next - 1].active)
|
|
next = (next + 1) % (MAX_BLE_PEERS + 1);
|
|
g_active_player = next;
|
|
} else {
|
|
g_active_opponent = (g_active_opponent + 1) % g_num_opponents;
|
|
}
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_COUNTERS) {
|
|
g_active_counter = (g_active_counter + 1) % NUM_COUNTERS;
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_DICE) {
|
|
g_dice_item = (g_dice_item + 1) % 3;
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_LIFE) {
|
|
int nc = g_ble_enabled ? 0 : g_num_opponents;
|
|
if (g_ble_enabled)
|
|
for (int i = 0; i < MAX_BLE_PEERS; i++)
|
|
if (g_peers[i].active) nc++;
|
|
g_life_select = (g_life_select + 1) % (3 + nc);
|
|
changed = 1;
|
|
}
|
|
}
|
|
}
|
|
hold_fwd = 0;
|
|
fwd_in_combo = 0;
|
|
fwd_menu_fired = 0;
|
|
}
|
|
|
|
// ── BACK: long press = prev menu (+ repeat), short press = sub-item back ──
|
|
if (back == 0) {
|
|
hold_back++;
|
|
if (g_active_menu != MENU_SETTINGS && !back_in_combo) {
|
|
if (hold_back == menu_ticks ||
|
|
(hold_back > menu_ticks && (hold_back - menu_ticks) % menu_ticks == 0)) {
|
|
g_active_menu = (g_active_menu - 1 + (NUM_MENUS - 1)) % (NUM_MENUS - 1);
|
|
hold_right = hold_left = players_tick = 0;
|
|
oled_draw_header();
|
|
changed = 1;
|
|
back_menu_fired = 1;
|
|
}
|
|
}
|
|
} else {
|
|
if (prev_back == 0 && !back_in_combo && !back_menu_fired) {
|
|
if (hold_back < menu_ticks) {
|
|
if (g_active_menu == MENU_SETTINGS) {
|
|
if (g_active_setting == SET_PLAYER_NAME) {
|
|
if (g_name_cursor > 0) {
|
|
g_name_cursor--;
|
|
} else {
|
|
g_active_setting = (g_active_setting - 1 + NUM_SETTINGS) % NUM_SETTINGS;
|
|
g_name_cursor = (g_active_setting == SET_PLAYER_NAME) ? PLAYER_NAME_LEN - 1 : 0;
|
|
g_game_id_cursor = (g_active_setting == SET_GAME_ID) ? GAME_ID_DIGITS - 1 : 0;
|
|
}
|
|
} else if (g_active_setting == SET_GAME_ID) {
|
|
if (g_game_id_cursor > 0) {
|
|
g_game_id_cursor--;
|
|
} else {
|
|
g_active_setting = (g_active_setting - 1 + NUM_SETTINGS) % NUM_SETTINGS;
|
|
g_name_cursor = (g_active_setting == SET_PLAYER_NAME) ? PLAYER_NAME_LEN - 1 : 0;
|
|
g_game_id_cursor = (g_active_setting == SET_GAME_ID) ? GAME_ID_DIGITS - 1 : 0;
|
|
}
|
|
} else {
|
|
g_active_setting = (g_active_setting - 1 + NUM_SETTINGS) % NUM_SETTINGS;
|
|
g_name_cursor = (g_active_setting == SET_PLAYER_NAME) ? PLAYER_NAME_LEN - 1 : 0;
|
|
g_game_id_cursor = (g_active_setting == SET_GAME_ID) ? GAME_ID_DIGITS - 1 : 0;
|
|
}
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_CMDR) {
|
|
if (g_ble_enabled) {
|
|
int total = MAX_BLE_PEERS + 1;
|
|
int next = (g_active_player - 1 + total) % total;
|
|
while (next != g_active_player && next != 0 && !g_peers[next - 1].active)
|
|
next = (next - 1 + total) % total;
|
|
g_active_player = next;
|
|
} else {
|
|
g_active_opponent = (g_active_opponent - 1 + g_num_opponents) % g_num_opponents;
|
|
}
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_COUNTERS) {
|
|
g_active_counter = (g_active_counter - 1 + NUM_COUNTERS) % NUM_COUNTERS;
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_DICE) {
|
|
g_dice_item = (g_dice_item - 1 + 3) % 3;
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_LIFE) {
|
|
int nc = g_ble_enabled ? 0 : g_num_opponents;
|
|
if (g_ble_enabled)
|
|
for (int i = 0; i < MAX_BLE_PEERS; i++)
|
|
if (g_peers[i].active) nc++;
|
|
int total = 3 + nc;
|
|
g_life_select = (g_life_select - 1 + total) % total;
|
|
changed = 1;
|
|
}
|
|
}
|
|
}
|
|
hold_back = 0;
|
|
back_in_combo = 0;
|
|
back_menu_fired = 0;
|
|
}
|
|
}
|
|
|
|
// ── delta[0]: value change (right / left) ────────────────────────────
|
|
int delta_r=0, delta_l=0;
|
|
if (right==0 && left==0) {
|
|
// both held: reset counters so they stay in sync and neither fires
|
|
hold_right=0; hold_left=0; release_r=0; release_l=0;
|
|
} else {
|
|
if (right==0) {
|
|
if (prev_right==1) {
|
|
delta_r=1; hold_right=0;
|
|
hold_armed_r = (release_r >= 2); release_r=0;
|
|
} else if (hold_armed_r && ++hold_right>=lr_ticks
|
|
&& (hold_right-lr_ticks)%HOLD_REPEAT==0) {
|
|
delta_r=1;
|
|
}
|
|
} else { hold_right=0; if (release_r<2) release_r++; }
|
|
if (left==0) {
|
|
if (prev_left==1) {
|
|
delta_l=1; hold_left=0;
|
|
hold_armed_l = (release_l >= 2); release_l=0;
|
|
} else if (hold_armed_l && ++hold_left>=lr_ticks
|
|
&& (hold_left-lr_ticks)%HOLD_REPEAT==0) {
|
|
delta_l=1;
|
|
}
|
|
} else { hold_left=0; if (release_l<2) release_l++; }
|
|
}
|
|
|
|
if (delta_r || delta_l) {
|
|
int d = delta_r - delta_l;
|
|
if (g_active_menu == MENU_LIFE) {
|
|
if (g_life_select == 0) {
|
|
g_life += d;
|
|
g_life_delta += d;
|
|
g_life_delta_tick = 0;
|
|
check_elimination();
|
|
} else if (g_life_select == 1) {
|
|
g_counters[COUNTER_STORM] += d;
|
|
if (g_counters[COUNTER_STORM] < 0) g_counters[COUNTER_STORM] = 0;
|
|
} else if (g_life_select == 2) {
|
|
g_counters[COUNTER_POISON] += d;
|
|
if (g_counters[COUNTER_POISON] < 0) g_counters[COUNTER_POISON] = 0;
|
|
check_elimination();
|
|
} else {
|
|
int si = g_life_select - 3;
|
|
if (g_ble_enabled) {
|
|
int found = 0;
|
|
for (int i = 0; i < MAX_BLE_PEERS; i++) {
|
|
if (g_peers[i].active && found++ == si) {
|
|
g_cmdr_damage[i] += d;
|
|
if (g_cmdr_damage[i] < 0) g_cmdr_damage[i] = 0;
|
|
check_elimination();
|
|
break;
|
|
}
|
|
}
|
|
} else if (si < g_num_opponents) {
|
|
g_cmdr_damage[si] += d;
|
|
if (g_cmdr_damage[si] < 0) g_cmdr_damage[si] = 0;
|
|
check_elimination();
|
|
}
|
|
}
|
|
changed = 1; MARK_DIRTY();
|
|
} else if (g_active_menu == MENU_CMDR) {
|
|
if (g_ble_enabled) {
|
|
if (g_active_player == 0) {
|
|
g_life += d;
|
|
g_life_delta += d;
|
|
g_life_delta_tick = 0;
|
|
check_elimination();
|
|
} else {
|
|
int slot = g_active_player - 1;
|
|
g_cmdr_damage[slot] += d;
|
|
if (g_cmdr_damage[slot] < 0) g_cmdr_damage[slot] = 0;
|
|
check_elimination();
|
|
}
|
|
} else {
|
|
g_cmdr_damage[g_active_opponent] += d;
|
|
if (g_cmdr_damage[g_active_opponent] < 0) g_cmdr_damage[g_active_opponent] = 0;
|
|
check_elimination();
|
|
}
|
|
changed = 1; MARK_DIRTY();
|
|
} else if (g_active_menu == MENU_COUNTERS) {
|
|
g_counters[g_active_counter] += d;
|
|
if (g_counters[g_active_counter] < 0) g_counters[g_active_counter] = 0;
|
|
changed = 1; MARK_DIRTY(); check_elimination();
|
|
} else if (g_active_menu == MENU_DICE) {
|
|
if (g_dice_item == 0) {
|
|
g_dice_num += d;
|
|
if (g_dice_num < 1) g_dice_num = 1;
|
|
} else if (g_dice_item == 1) {
|
|
g_dice_sides = (g_dice_sides + d + NUM_DICE_SIDES) % NUM_DICE_SIDES;
|
|
} else {
|
|
int sides = die_sides[g_dice_sides];
|
|
g_dice_sum = 0;
|
|
int pos = 0;
|
|
g_dice_csv[0] = '\0';
|
|
for (int i = 0; i < g_dice_num; i++) {
|
|
int roll = (int)(esp_random() % (uint32_t)sides) + 1;
|
|
g_dice_sum += roll;
|
|
if (pos < DICE_CSV_LEN - 5) {
|
|
if (i > 0) g_dice_csv[pos++] = ',';
|
|
pos += snprintf(g_dice_csv + pos, DICE_CSV_LEN - pos, "%d", roll);
|
|
}
|
|
}
|
|
g_dice_rolled = 1;
|
|
}
|
|
changed = 1;
|
|
} else if (g_active_menu == MENU_SETTINGS) {
|
|
changed = 1; MARK_DIRTY();
|
|
switch (g_active_setting) {
|
|
case SET_BRIGHTNESS:
|
|
g_brightness_pct += d;
|
|
if (g_brightness_pct < BRIGHTNESS_MIN) g_brightness_pct = BRIGHTNESS_MIN;
|
|
if (g_brightness_pct > BRIGHTNESS_MAX) g_brightness_pct = BRIGHTNESS_MAX;
|
|
g_led_max = (uint8_t)(255 * g_brightness_pct / 100);
|
|
break;
|
|
case SET_START_LIFE:
|
|
g_start_life_index += d;
|
|
if (g_start_life_index < 0) g_start_life_index = 0;
|
|
if (g_start_life_index >= NUM_LIFE_OPTS) g_start_life_index = NUM_LIFE_OPTS-1;
|
|
break;
|
|
case SET_NUM_OPP:
|
|
g_num_opponents += d;
|
|
if (g_num_opponents < 1) g_num_opponents = 1;
|
|
if (g_num_opponents > MAX_OPPONENTS) g_num_opponents = MAX_OPPONENTS;
|
|
if (g_active_opponent >= g_num_opponents) g_active_opponent = 0;
|
|
break;
|
|
case SET_PLAYER_NAME: {
|
|
char curr = g_player_name[g_name_cursor];
|
|
const char *pos = strchr(NAME_CHARS, curr);
|
|
int idx = pos ? (int)(pos - NAME_CHARS) : 0;
|
|
idx = (idx + d + NUM_NAME_CHARS) % NUM_NAME_CHARS;
|
|
g_player_name[g_name_cursor] = NAME_CHARS[idx];
|
|
break;
|
|
}
|
|
case SET_BLE:
|
|
g_ble_enabled += d;
|
|
if (g_ble_enabled < 0) g_ble_enabled = 0;
|
|
if (g_ble_enabled > 1) g_ble_enabled = 1;
|
|
ble_adv_update();
|
|
oled_draw_header();
|
|
break;
|
|
case SET_GAME_ID: {
|
|
int byte = g_game_id_cursor / 2;
|
|
int nibble = g_game_id_cursor % 2;
|
|
int val = nibble == 0 ? (g_game_id[byte] >> 4) & 0xF : g_game_id[byte] & 0xF;
|
|
val = (val + d + NUM_HEX_CHARS) % NUM_HEX_CHARS;
|
|
if (nibble == 0)
|
|
g_game_id[byte] = (g_game_id[byte] & 0x0F) | ((uint8_t)val << 4);
|
|
else
|
|
g_game_id[byte] = (g_game_id[byte] & 0xF0) | (uint8_t)val;
|
|
ble_adv_update();
|
|
break;
|
|
}
|
|
case SET_RESET:
|
|
game_reset();
|
|
ble_adv_update();
|
|
break;
|
|
case SET_RESET_ALL:
|
|
game_reset();
|
|
g_reset_cmd_ticks = 1500;
|
|
ble_adv_update();
|
|
break;
|
|
case SET_MENU_HOLD:
|
|
g_menu_hold_ms += d * 10;
|
|
if (g_menu_hold_ms < HOLD_MS_MIN) g_menu_hold_ms = HOLD_MS_MIN;
|
|
if (g_menu_hold_ms > HOLD_MS_MAX) g_menu_hold_ms = HOLD_MS_MAX;
|
|
break;
|
|
case SET_LR_HOLD:
|
|
g_lr_hold_ms += d * 10;
|
|
if (g_lr_hold_ms < HOLD_MS_MIN) g_lr_hold_ms = HOLD_MS_MIN;
|
|
if (g_lr_hold_ms > HOLD_MS_MAX) g_lr_hold_ms = HOLD_MS_MAX;
|
|
break;
|
|
case SET_DISPLAY_FLIP:
|
|
g_display_flip ^= 1;
|
|
oled_set_flip(g_display_flip);
|
|
break;
|
|
case SET_DELTA_TIMEOUT:
|
|
g_delta_timeout_ms += d * 100;
|
|
if (g_delta_timeout_ms < DELTA_TIMEOUT_MIN) g_delta_timeout_ms = DELTA_TIMEOUT_MIN;
|
|
if (g_delta_timeout_ms > DELTA_TIMEOUT_MAX) g_delta_timeout_ms = DELTA_TIMEOUT_MAX;
|
|
break;
|
|
case SET_AUTO_SLEEP:
|
|
g_sleep_timeout_min += d;
|
|
if (g_sleep_timeout_min < 0) g_sleep_timeout_min = 0;
|
|
if (g_sleep_timeout_min > SLEEP_TIMEOUT_MAX) g_sleep_timeout_min = SLEEP_TIMEOUT_MAX;
|
|
idle_ticks = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Remote reset command ──────────────────────────────────────────────
|
|
if (g_reset_requested) {
|
|
g_reset_requested = 0;
|
|
game_reset();
|
|
MARK_DIRTY();
|
|
changed = 1;
|
|
ble_adv_update();
|
|
}
|
|
|
|
// ── reset_cmd broadcast countdown ────────────────────────────────────
|
|
if (g_reset_cmd_ticks > 0 && --g_reset_cmd_ticks == 0) ble_adv_update();
|
|
|
|
// ── Peer expiry + player ID resolution ───────────────────────────────
|
|
if (g_tick % 100 == 0) {
|
|
for (int i = 0; i < MAX_BLE_PEERS; i++) {
|
|
if (g_peers[i].active && g_tick - g_peers[i].last_seen > BLE_PEER_TIMEOUT) {
|
|
#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; changed = 1; }
|
|
}
|
|
g_peers[i].active = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Autosave settings ─────────────────────────────────────────────────
|
|
if (settings_dirty && ++settings_save_tick >= SAVE_DELAY) {
|
|
settings_save(); settings_dirty = 0; settings_save_tick = 0;
|
|
}
|
|
|
|
// ── Life delta timeout ────────────────────────────────────────────────
|
|
if (g_life_delta != 0) {
|
|
if (++g_life_delta_tick >= g_delta_timeout_ms / 10) {
|
|
g_life_delta = 0;
|
|
g_life_delta_tick = 0;
|
|
if (g_active_menu == MENU_LIFE) changed = 1;
|
|
}
|
|
}
|
|
|
|
// ── BLE adv update on game state change ───────────────────────────────
|
|
if (changed && (g_active_menu==MENU_LIFE || g_active_menu==MENU_COUNTERS ||
|
|
g_active_menu==MENU_CMDR)) {
|
|
ble_adv_update();
|
|
}
|
|
|
|
// ── Players periodic refresh ──────────────────────────────────────────
|
|
if (g_active_menu == MENU_CMDR && g_ble_enabled) {
|
|
if (++players_tick >= 100) {
|
|
players_tick = 0; oled_draw_players();
|
|
}
|
|
} else { players_tick = 0; }
|
|
|
|
// ── LED ───────────────────────────────────────────────────────────────
|
|
int bthresh = start_life_opts[g_start_life_index] / 4;
|
|
if (g_life == 0) {
|
|
led_update_for_count(0, 255);
|
|
} else if (g_life < bthresh) {
|
|
float speed = 1.0f + ((float)bthresh - g_life) / (float)(bthresh - 1) * 2.0f;
|
|
breath_phase += 2.0f * 3.14159265f * speed / 200.0f;
|
|
uint8_t sc = (uint8_t)(127.5f + 127.5f * sinf(breath_phase));
|
|
led_update_for_count(g_life, sc);
|
|
} else if (changed) {
|
|
led_update_for_count(g_life, 255);
|
|
}
|
|
|
|
// ── Battery voltage sampling ──────────────────────────────────────────
|
|
if (g_battery_pct >= 0 && g_battery_pct < BATT_LOW_PCT && g_tick % 50 == 0)
|
|
oled_draw_header();
|
|
|
|
if (++batt_tick >= BATT_SAMPLE_TICKS) {
|
|
batt_tick = 0;
|
|
int raw, voltage_mv;
|
|
adc_oneshot_read(s_adc, ADC_CHANNEL_6, &raw);
|
|
if (s_adc_cali_ok)
|
|
adc_cali_raw_to_voltage(s_adc_cali, raw, &voltage_mv);
|
|
else
|
|
voltage_mv = raw * 3100 / 4095;
|
|
int cell_mv = voltage_mv * 2; // 100k/100k voltage divider
|
|
// LiPo discharge curve: piecewise linear interpolation.
|
|
// Voltage stays flat ~3.7-4.2V through most of charge; linear
|
|
// mapping reads too high. Table derived from typical LiPo OCV curve.
|
|
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();
|
|
}
|
|
|
|
// ── Idle tracking and auto-sleep ──────────────────────────────────────
|
|
if (fwd == 0 || back == 0 || left == 0 || right == 0)
|
|
idle_ticks = 0;
|
|
else
|
|
idle_ticks++;
|
|
|
|
if (!g_sleep_mode && g_sleep_timeout_min > 0 &&
|
|
idle_ticks >= (uint32_t)g_sleep_timeout_min * 60 * 100) {
|
|
idle_ticks = 0;
|
|
if (settings_dirty) { settings_save(); settings_dirty = 0; settings_save_tick = 0; }
|
|
oled_set_contrast(SLEEP_CONTRAST);
|
|
led_off();
|
|
int ble_pre_auto = g_ble_enabled;
|
|
if (g_ble_enabled) { g_ble_enabled = 0; ble_adv_update(); }
|
|
esp_light_sleep_start();
|
|
// woke up — restore
|
|
oled_set_contrast(0xCF);
|
|
g_ble_enabled = ble_pre_auto;
|
|
if (g_ble_enabled) ble_adv_update();
|
|
breath_phase = 0.0f;
|
|
led_update_for_count(g_life, 255);
|
|
oled_draw_header();
|
|
changed = 1;
|
|
}
|
|
|
|
// ── Display dispatch ──────────────────────────────────────────────────
|
|
if (changed) {
|
|
switch (g_active_menu) {
|
|
case MENU_LIFE:
|
|
oled_draw_life();
|
|
break;
|
|
case MENU_CMDR:
|
|
if (!g_ble_enabled) {
|
|
for (int i = 0; i < g_num_opponents; i++) {
|
|
snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "CMD %d", i+1);
|
|
opponent_label_ptrs[i] = opponent_labels[i];
|
|
}
|
|
oled_draw_list(opponent_label_ptrs, g_cmdr_damage, g_num_opponents, g_active_opponent);
|
|
} else {
|
|
oled_draw_players();
|
|
}
|
|
break;
|
|
case MENU_COUNTERS:
|
|
oled_draw_list(counter_names, g_counters, NUM_COUNTERS, g_active_counter);
|
|
break;
|
|
case MENU_DICE:
|
|
oled_draw_dice();
|
|
break;
|
|
case MENU_SETTINGS:
|
|
oled_draw_settings();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
if (changed || g_tick % 1000 == 0)
|
|
serial_print_state();
|
|
#endif
|
|
|
|
prev_fwd = fwd; prev_right = right; prev_left = left; prev_back = back;
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
}
|