#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 "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include #include // ── Global state ────────────────────────────────────────────────────────────── // 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; // 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}; // Peers ble_peer_t g_peers[MAX_BLE_PEERS]; // Timing uint32_t g_tick; // ── 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 pid=%u 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, (unsigned)g_player_id, 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(); g_led_max = (uint8_t)(255 * g_brightness_pct / 100); oled_init(); oled_clear(); gpio_config_t gpio_cfg = { .pin_bit_mask = (1ULL<= MID_HOLD_MENU) { if (g_active_menu == MENU_SETTINGS) { g_active_menu = 0; } else { g_active_menu = (g_active_menu + 1) % (NUM_MENUS - 1); } hold_right = hold_left = players_tick = 0; oled_draw_header(); changed = 1; } else 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; } } hold_mid = 0; mid_fired_hold = 0; } // ── Left / Right ───────────────────────────────────────────────────── int delta_r=0, delta_l=0; 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>=HOLD_DELAY && (hold_right-HOLD_DELAY)%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>=HOLD_DELAY && (hold_left-HOLD_DELAY)%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) { g_life += d; changed = 1; MARK_DIRTY(); check_elimination(); } else if (g_active_menu == MENU_CMDR) { if (g_ble_enabled) { if (g_active_player == 0) { g_life += d; 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_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; } } } // ── 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 g_peers[i].active = 0; } } player_id_resolve(); } // ── Autosave settings ───────────────────────────────────────────────── if (settings_dirty && ++settings_save_tick >= SAVE_DELAY) { settings_save(); settings_dirty = 0; settings_save_tick = 0; } // ── 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 ─────────────────────────────────────────────────────────────── if (g_life == 0) { led_update_for_count(0, 255); } else if (g_life < 10) { float speed = 1.0f + (10.0f - g_life) / 9.0f * 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); } // ── Display dispatch ────────────────────────────────────────────────── if (changed) { switch (g_active_menu) { case MENU_LIFE: snprintf(nbuf, sizeof(nbuf), "%d", g_life); oled_draw_centered(nbuf); break; case MENU_CMDR: for (int i = 0; i < g_num_opponents; i++) { int found = 0; if (g_ble_enabled) { for (int j = 0; j < MAX_BLE_PEERS; j++) { if (g_peers[j].active && g_peers[j].player_id == (uint8_t)i) { snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "%.4s", g_peers[j].name); found = 1; break; } } } if (!found) snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "CMD %d", i+1); opponent_label_ptrs[i] = opponent_labels[i]; } if (!g_ble_enabled) 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_SETTINGS: oled_draw_settings(); break; } } #ifdef DEBUG if (changed || g_tick % 1000 == 0) serial_print_state(); #endif prev_right = right; prev_left = left; prev_mid = mid; vTaskDelay(pdMS_TO_TICKS(10)); } }