commeownder/main/main.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));
}
}