465 lines
19 KiB
C
465 lines
19 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 "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
// ── 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<<BTN_RIGHT_GPIO)|(1ULL<<BTN_LEFT_GPIO)|(1ULL<<BTN_MID_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();
|
|
{ char ibuf[12]; snprintf(ibuf, sizeof(ibuf), "%d", g_life); oled_draw_centered(ibuf); }
|
|
led_update_for_count(g_life, 255);
|
|
|
|
#ifdef DEBUG
|
|
serial_print_state();
|
|
#endif
|
|
|
|
int prev_right=1, prev_left=1, prev_mid=1;
|
|
int hold_right=0, hold_left=0, hold_mid=0, mid_fired_hold=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;
|
|
#define MARK_DIRTY() do { settings_dirty = 1; settings_save_tick = 0; } while (0)
|
|
float breath_phase=0.0f;
|
|
char nbuf[12];
|
|
|
|
char opponent_labels[MAX_OPPONENTS][PLAYER_NAME_LEN+1];
|
|
const char *opponent_label_ptrs[MAX_OPPONENTS];
|
|
|
|
while (1) {
|
|
g_tick++;
|
|
int right = gpio_get_level(BTN_RIGHT_GPIO);
|
|
int left = gpio_get_level(BTN_LEFT_GPIO);
|
|
int mid = gpio_get_level(BTN_MID_GPIO);
|
|
int changed = 0;
|
|
|
|
// ── Middle button ────────────────────────────────────────────────────
|
|
if (mid == 0) {
|
|
hold_mid++;
|
|
if (!mid_fired_hold && hold_mid == MID_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;
|
|
}
|
|
mid_fired_hold = 1;
|
|
hold_right = hold_left = players_tick = 0;
|
|
oled_draw_header();
|
|
changed = 1;
|
|
}
|
|
} else {
|
|
if (prev_mid == 0 && !mid_fired_hold) {
|
|
if (hold_mid >= 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));
|
|
}
|
|
}
|