#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 #include #include // ── 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< 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)); } }