main
noah metz 2026-06-02 19:05:21 -06:00
parent 3b527d1a8c
commit 2ef2e75430
15 changed files with 6842 additions and 208 deletions

@ -1,6 +1,6 @@
idf_component_register(SRCS "main.c" "draw.c" "ble.c" "game.c"
INCLUDE_DIRS "."
PRIV_REQUIRES driver nvs_flash bt vfs)
PRIV_REQUIRES driver nvs_flash bt vfs esp_adc)
if(DEBUG)
target_compile_definitions(${COMPONENT_LIB} PUBLIC DEBUG)

@ -12,7 +12,6 @@ volatile int g_ble_scanning;
volatile int g_ble_initialized;
uint8_t g_own_addr_type;
uint8_t g_own_addr[6];
uint8_t g_player_id = 0xFF;
volatile int g_reset_requested;
int g_reset_cmd_ticks;
@ -29,10 +28,7 @@ static void ble_adv_start_internal(void)
payload.life = (int16_t)g_life;
payload.poison = (uint8_t)g_counters[0];
payload.eliminated = (uint8_t)g_eliminated;
payload.player_id = g_player_id;
payload.reset_cmd = (g_reset_cmd_ticks > 0) ? 1 : 0;
for (int i = 0; i < MAX_OPPONENTS; i++)
payload.cmdr_dmg[i] = (uint8_t)g_cmdr_damage[i];
static uint8_t mfr[2 + sizeof(ble_payload_t)];
mfr[0] = 0xFF; mfr[1] = 0xFF;
@ -51,10 +47,10 @@ static void ble_adv_start_internal(void)
ble_gap_adv_start(g_own_addr_type, NULL, BLE_HS_FOREVER, &params, ble_gap_event_handler, NULL);
#ifdef DEBUG
printf("DBG ADV_TX name=%-8.8s life=%d poison=%u game_id=%02X%02X eliminated=%u pid=%u reset=%u\n",
printf("DBG ADV_TX name=%-8.8s life=%d poison=%u game_id=%02X%02X eliminated=%u reset=%u\n",
payload.name, (int)payload.life, (unsigned)payload.poison,
payload.game_id[0], payload.game_id[1], (unsigned)payload.eliminated,
(unsigned)payload.player_id, (unsigned)payload.reset_cmd);
(unsigned)payload.reset_cmd);
fflush(stdout);
#endif
}
@ -91,9 +87,7 @@ static void ble_update_peer(const ble_addr_t *addr, const ble_payload_t *p)
g_peers[slot].life = p->life;
g_peers[slot].poison = p->poison;
g_peers[slot].eliminated = p->eliminated;
g_peers[slot].player_id = p->player_id;
g_peers[slot].reset_cmd = p->reset_cmd;
memcpy(g_peers[slot].cmdr_dmg, p->cmdr_dmg, MAX_OPPONENTS);
g_peers[slot].last_seen = g_tick;
g_peers[slot].active = 1;
memcpy(g_peers[slot].name, p->name, PLAYER_NAME_LEN);
@ -101,15 +95,13 @@ static void ble_update_peer(const ble_addr_t *addr, const ble_payload_t *p)
if (p->reset_cmd && !prev_reset_cmd) g_reset_requested = 1;
#ifdef DEBUG
printf("DBG PEER_RX slot=%d addr=%02X:%02X:%02X:%02X:%02X:%02X name=%-8.8s life=%d poison=%u game_id=%02X%02X eliminated=%u pid=%u reset=%u cmdr=[%u,%u,%u,%u]\n",
printf("DBG PEER_RX slot=%d addr=%02X:%02X:%02X:%02X:%02X:%02X name=%-8.8s life=%d poison=%u game_id=%02X%02X eliminated=%u reset=%u\n",
slot,
addr->val[5], addr->val[4], addr->val[3],
addr->val[2], addr->val[1], addr->val[0],
p->name, (int)p->life, (unsigned)p->poison,
p->game_id[0], p->game_id[1], (unsigned)p->eliminated,
(unsigned)p->player_id, (unsigned)p->reset_cmd,
(unsigned)p->cmdr_dmg[0], (unsigned)p->cmdr_dmg[1],
(unsigned)p->cmdr_dmg[2], (unsigned)p->cmdr_dmg[3]);
(unsigned)p->reset_cmd);
fflush(stdout);
#endif
}
@ -143,7 +135,6 @@ static void ble_on_sync(void)
{
ble_hs_id_infer_auto(0, &g_own_addr_type);
ble_hs_id_copy_addr(g_own_addr_type, g_own_addr, NULL);
if (g_player_id == 0xFF) g_player_id = 0;
g_ble_initialized = 1;
if (g_ble_enabled) ble_adv_start_internal();
}
@ -182,29 +173,6 @@ static void ble_manager_task(void *arg)
}
}
void player_id_resolve(void)
{
if (!g_ble_initialized || !g_ble_enabled) return;
for (int i = 0; i < MAX_BLE_PEERS; i++) {
if (!g_peers[i].active || g_peers[i].player_id != g_player_id) continue;
int we_lose = 0;
for (int b = 5; b >= 0; b--) {
if (g_peers[i].addr.val[b] < g_own_addr[b]) { we_lose = 1; break; }
if (g_peers[i].addr.val[b] > g_own_addr[b]) break;
}
if (!we_lose) continue;
uint8_t used[BLE_MAX_PLAYERS] = {0};
for (int j = 0; j < MAX_BLE_PEERS; j++)
if (g_peers[j].active && g_peers[j].player_id < BLE_MAX_PLAYERS)
used[g_peers[j].player_id] = 1;
for (int id = 0; id < BLE_MAX_PLAYERS; id++) {
if (!used[id]) { g_player_id = (uint8_t)id; break; }
}
ble_adv_update();
break;
}
}
void ble_init(void)
{
nimble_port_init();

@ -10,9 +10,7 @@ typedef struct __attribute__((packed)) {
int16_t life;
uint8_t poison;
uint8_t eliminated;
uint8_t player_id;
uint8_t reset_cmd;
uint8_t cmdr_dmg[MAX_OPPONENTS];
} ble_payload_t;
typedef struct {
@ -21,9 +19,7 @@ typedef struct {
int16_t life;
uint8_t poison;
uint8_t eliminated;
uint8_t player_id;
uint8_t reset_cmd;
uint8_t cmdr_dmg[MAX_OPPONENTS];
uint32_t last_seen;
int active;
} ble_peer_t;
@ -32,10 +28,8 @@ extern volatile int g_ble_scanning;
extern volatile int g_ble_initialized;
extern uint8_t g_own_addr_type;
extern uint8_t g_own_addr[6];
extern uint8_t g_player_id;
extern volatile int g_reset_requested;
extern int g_reset_cmd_ticks;
void ble_init(void);
void ble_adv_update(void);
void player_id_resolve(void);

@ -34,21 +34,26 @@
#define BLE_MFR_MAGIC_1 0xDE
#define BLE_GAME_ID_0 0x42
#define BLE_GAME_ID_1 0x42
#define BLE_MAX_PLAYERS 5
#define MAX_BLE_PEERS 4
#define BLE_PEER_TIMEOUT 3000
// ── Menus ─────────────────────────────────────────────────────────────────────
#define NUM_MENUS 4
#define NUM_MENUS 5
#define MENU_LIFE 0
#define MENU_CMDR 1
#define MENU_COUNTERS 2
#define MENU_SETTINGS 3
#define MENU_DICE 3
#define MENU_SETTINGS 4
extern const int menu_slot[NUM_MENUS];
// ── Dice ──────────────────────────────────────────────────────────────────────
#define DICE_CSV_LEN 128
#define NUM_DICE_SIDES 7
extern const int die_sides[NUM_DICE_SIDES];
// ── Settings ──────────────────────────────────────────────────────────────────
#define NUM_SETTINGS 12
#define NUM_SETTINGS 13
#define SET_BRIGHTNESS 0
#define SET_START_LIFE 1
#define SET_NUM_OPP 2
@ -61,6 +66,7 @@ extern const int menu_slot[NUM_MENUS];
#define SET_LR_HOLD 9
#define SET_DISPLAY_FLIP 10
#define SET_DELTA_TIMEOUT 11
#define SET_AUTO_SLEEP 12
#define HOLD_MS_MIN 50
#define HOLD_MS_MAX 2000
#define DELTA_TIMEOUT_MIN 100
@ -97,6 +103,14 @@ extern const char *counter_names[NUM_COUNTERS];
#define FONT_BASE 0x20
#define FONT_MAX 0x5A
// ── Battery ADC (GPIO34, ADC1 Channel 6) ──────────────────────────────────────
#define BATT_LOW_PCT 10 // flash battery indicator below this
#define BATT_SAMPLE_TICKS 500 // sample every 500 × 10ms = 5s
// ── Auto-sleep ────────────────────────────────────────────────────────────────
#define SLEEP_TIMEOUT_DEF 60 // default auto-sleep timeout (minutes)
#define SLEEP_TIMEOUT_MAX 120 // maximum timeout (minutes)
// ── NVS ───────────────────────────────────────────────────────────────────────
#define NVS_NS "settings"
#define SAVE_DELAY 300

@ -9,11 +9,13 @@
#include <string.h>
#include <stdio.h>
const int menu_slot[NUM_MENUS] = {0, 1, 2, 7};
const int menu_slot[NUM_MENUS] = {0, 1, 2, 3, 7};
const int die_sides[NUM_DICE_SIDES] = {4, 6, 8, 10, 12, 20, 100};
const char *setting_names[NUM_SETTINGS] = {
"LED BRIGHTNESS", "STARTING LIFE", "NUM OPPONENTS", "BLE",
"RESET", "RESET ALL", "GAME ID", "PLAYER NAME",
"MENU HOLD MS", "LR HOLD MS", "FLIP DISPLAY", "DELTA TIMEOUT",
"AUTO SLEEP",
};
#define LEDC_TIMER_SEL LEDC_TIMER_0
@ -149,6 +151,53 @@ void oled_draw_header(void)
pages[1][sx+col] = bg ^ icon[1][col];
}
}
// Slot 5 (cols 80-95): percentage text, page 0
// Slot 6 (cols 96-111): vertical battery icon, nub at top, fill from bottom
if (g_battery_pct >= 0) {
int show = (g_battery_pct >= BATT_LOW_PCT || (g_tick / 50) % 2 == 0);
if (show) {
// ── Percentage text, vertically centered (rows 4-11) ─────────────
char pct_str[5];
int plen = snprintf(pct_str, sizeof(pct_str), "%d", g_battery_pct);
int tx = 5 * SEG_W + (SEG_W - plen * 5) / 2;
for (int ci = 0; ci < plen; ci++) {
uint8_t c = (uint8_t)pct_str[ci];
if (c < FONT_BASE || c > FONT_MAX) continue;
const uint8_t *glyph = font5x8[c - FONT_BASE];
for (int sc = 0; sc < 5; sc++, tx++) {
if (tx < 0 || tx >= OLED_WIDTH) continue;
pages[0][tx] &= ~(uint8_t)(glyph[sc] << 4); // font bits 0-3 → rows 4-7
pages[1][tx] &= ~(uint8_t)(glyph[sc] >> 4); // font bits 4-7 → rows 8-11
}
}
// ── Vertical battery icon ─────────────────────────────────────────
// 1px inset from slot edges: body cols 2-13, rows 3-14.
// Nub: cols 5-10, rows 1-2.
// Inner: cols 3-12, rows 4-13 (10 rows). Fill rises from bottom.
int fill_rows = g_battery_pct * 10 / 100;
int fill_start = 14 - fill_rows; // first filled inner row
uint8_t p0_fill = 0, p1_fill = 0;
for (int r = 4; r <= 7; r++) if (r >= fill_start) p0_fill |= (1 << r);
for (int r = 8; r <= 13; r++) if (r >= fill_start) p1_fill |= (1 << (r-8));
int bbase = 6 * SEG_W;
for (int x = 0; x < 16; x++) {
uint8_t p0 = 0, p1 = 0;
if (x == 2 || x == 13) {
p0 = 0xF8; p1 = 0x7F; // side borders rows 3-14
} else if (x >= 3 && x <= 12) {
p0 = 0x08 | p0_fill; // top border (row 3) + fill
p1 = 0x40 | p1_fill; // bottom border (row 14) + fill
}
if (x >= 5 && x <= 10) p0 |= 0x06; // nub rows 1-2
pages[0][bbase + x] &= ~p0;
pages[1][bbase + x] &= ~p1;
}
}
}
for (int p = 0; p < HEADER_PAGES; p++) oled_write_page(p, pages[p]);
}
@ -237,8 +286,8 @@ static void fill_cols(uint8_t pages[][OLED_WIDTH], int content_h,
// 5×8 mini-icons (vertical-byte, bit0=top), same format as font5x8 columns.
// Teardrop: pointed top, round bottom (for poison)
static const uint8_t icon_mini_drop[5] = {0x3C, 0x7E, 0x7F, 0x7E, 0x3C};
// Lightning bolt: zigzag (for storm)
static const uint8_t icon_mini_bolt[5] = {0x0C, 0x0E, 0x1F, 0x39, 0x70};
// Lightning bolt: thin zigzag from center-top with multiple branches
static const uint8_t icon_mini_bolt[5] = {0x44, 0x2A, 0x51, 0x0C, 0x10};
static int pxbuf_icon(uint8_t pages[][OLED_WIDTH], int content_h,
int base, int x, const uint8_t *cols, int width, int inv)
@ -374,9 +423,73 @@ void oled_draw_settings(void)
snprintf(rows[SET_LR_HOLD].value, sizeof(rows[0].value), "%d", g_lr_hold_ms);
snprintf(rows[SET_DISPLAY_FLIP].value, sizeof(rows[0].value), "%s", g_display_flip ? "ON" : "OFF");
snprintf(rows[SET_DELTA_TIMEOUT].value, sizeof(rows[0].value), "%dms", g_delta_timeout_ms);
if (g_sleep_timeout_min == 0)
snprintf(rows[SET_AUTO_SLEEP].value, sizeof(rows[0].value), "OFF");
else
snprintf(rows[SET_AUTO_SLEEP].value, sizeof(rows[0].value), "%dmin", g_sleep_timeout_min);
oled_draw_rows(rows, NUM_SETTINGS, g_active_setting);
}
void oled_draw_dice(void)
{
// 128px wide, labels start at x=2, 6px/char → 21 chars per row
static const int WRAP_COLS = 21;
static const int MAX_WRAP = 8;
char wrap_lines[8][24];
int wrap_count = 0;
if (g_dice_rolled) {
const char *src = g_dice_csv;
while (*src && wrap_count < MAX_WRAP) {
int len = (int)strlen(src);
if (len <= WRAP_COLS) {
strncpy(wrap_lines[wrap_count], src, WRAP_COLS);
wrap_lines[wrap_count][WRAP_COLS] = '\0';
wrap_count++;
break;
}
// find last comma within WRAP_COLS chars to avoid mid-number breaks
int cut = WRAP_COLS;
for (int j = WRAP_COLS - 1; j > 0; j--) {
if (src[j] == ',') { cut = j; break; }
}
strncpy(wrap_lines[wrap_count], src, cut);
wrap_lines[wrap_count][cut] = '\0';
wrap_count++;
src += cut;
if (*src == ',') src++;
}
}
oled_row_t rows[3 + 8];
int n = 3;
rows[0].label = "NUM";
rows[0].cursor = -1;
snprintf(rows[0].value, sizeof(rows[0].value), "%d", g_dice_num);
rows[1].label = "SIDES";
rows[1].cursor = -1;
snprintf(rows[1].value, sizeof(rows[1].value), "D%d", die_sides[g_dice_sides]);
rows[2].label = "ROLL";
rows[2].cursor = -1;
if (g_dice_rolled)
snprintf(rows[2].value, sizeof(rows[2].value), "%d", g_dice_sum);
else
snprintf(rows[2].value, sizeof(rows[2].value), "---");
for (int i = 0; i < wrap_count; i++) {
rows[n].label = wrap_lines[i];
rows[n].cursor = -1;
rows[n].value[0] = '\0';
n++;
}
oled_draw_rows(rows, n, g_dice_item);
}
void oled_draw_life(void)
{
const int content_pages = OLED_PAGES - HEADER_PAGES;

@ -22,3 +22,4 @@ void oled_draw_list(const char **labels, const int *values, int count, int activ
void oled_draw_players(void);
void oled_draw_settings(void);
void oled_draw_life(void);
void oled_draw_dice(void);

@ -74,6 +74,9 @@ static const uint8_t icons[NUM_MENUS][2][ICON_W] = {
// Plus (counters)
{ {0x00,0x00,0x00,0xC0,0xC0,0xC0,0xF8,0xF8,0xF8,0xF8,0xC0,0xC0,0xC0,0x00,0x00,0x00},
{0x00,0x00,0x00,0x03,0x03,0x03,0x1F,0x1F,0x1F,0x1F,0x03,0x03,0x03,0x00,0x00,0x00} },
// D6 showing 6-face: square outline + 6 dots in 2x3 arrangement
{ {0x00,0xFE,0x02,0x02,0x9A,0x9A,0x02,0x02,0x02,0x02,0x9A,0x9A,0x02,0x02,0xFE,0x00},
{0x00,0x7F,0x40,0x40,0x59,0x59,0x40,0x40,0x40,0x40,0x59,0x59,0x40,0x40,0x7F,0x00} },
// Cog (settings)
{ {0x00,0x80,0xDC,0xFC,0xFC,0xB8,0x9C,0xFE,0xFE,0x9C,0xB8,0xFC,0xFC,0xDC,0x80,0x00},
{0x00,0x01,0x3B,0x3F,0x3F,0x1D,0x39,0x7F,0x7F,0x39,0x1D,0x3F,0x3F,0x3B,0x01,0x00} },

@ -26,6 +26,7 @@ void settings_reset_defaults(void)
g_lr_hold_ms = 500;
g_display_flip = 0;
g_delta_timeout_ms = 1000;
g_sleep_timeout_min = SLEEP_TIMEOUT_DEF;
}
void game_reset(void)
@ -63,7 +64,7 @@ void settings_load(void)
if (nvs_get_i32(nvs, "lr_hold", &val) == ESP_OK) g_lr_hold_ms = (int)val;
if (nvs_get_i32(nvs, "disp_flip", &val) == ESP_OK) g_display_flip = (int)val;
if (nvs_get_i32(nvs, "delta_to", &val) == ESP_OK) g_delta_timeout_ms = (int)val;
if (nvs_get_i32(nvs, "player_id", &val) == ESP_OK) g_player_id = (uint8_t)val;
if (nvs_get_i32(nvs, "sleep_to", &val) == ESP_OK) g_sleep_timeout_min = (int)val;
if (nvs_get_i32(nvs, "life", &val) == ESP_OK) g_life = (int)val;
char key[8];
for (int i = 0; i < MAX_OPPONENTS; i++) {
@ -90,8 +91,8 @@ void settings_save(void)
nvs_set_i32(nvs, "lr_hold", (int32_t)g_lr_hold_ms);
nvs_set_i32(nvs, "disp_flip", (int32_t)g_display_flip);
nvs_set_i32(nvs, "delta_to", (int32_t)g_delta_timeout_ms);
nvs_set_i32(nvs, "sleep_to", (int32_t)g_sleep_timeout_min);
nvs_set_str(nvs, "pname", g_player_name);
nvs_set_i32(nvs, "player_id", (int32_t)g_player_id);
nvs_set_i32(nvs, "life", (int32_t)g_life);
char key[8];
for (int i = 0; i < MAX_OPPONENTS; i++) {

@ -10,6 +10,11 @@
#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>
@ -18,6 +23,12 @@
// ── 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;
@ -58,14 +69,22 @@ 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 pid=%u eliminated=%d\n",
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, (unsigned)g_player_id, g_eliminated);
g_active_menu, g_ble_enabled, g_eliminated);
fflush(stdout);
}
@ -154,6 +173,21 @@ void app_main(void)
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();
@ -193,6 +227,13 @@ void app_main(void)
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;
@ -321,6 +362,9 @@ void app_main(void)
} 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)
@ -389,6 +433,9 @@ void app_main(void)
} 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)
@ -489,6 +536,28 @@ void app_main(void)
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) {
@ -564,6 +633,12 @@ void app_main(void)
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;
}
}
}
@ -596,7 +671,6 @@ void app_main(void)
g_peers[i].active = 0;
}
}
player_id_resolve();
}
// ── Autosave settings ─────────────────────────────────────────────────
@ -639,6 +713,65 @@ void app_main(void)
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) {
@ -646,29 +779,22 @@ void app_main(void)
oled_draw_life();
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)
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];
}
if (!g_ble_enabled)
opponent_label_ptrs[i] = opponent_labels[i];
}
oled_draw_list(opponent_label_ptrs, g_cmdr_damage, g_num_opponents, g_active_opponent);
else
} 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;

@ -33,6 +33,10 @@ extern int g_display_flip;
// Peers
extern ble_peer_t g_peers[MAX_BLE_PEERS];
// Battery / power
extern int g_battery_pct; // -1 = not yet sampled
extern int g_sleep_timeout_min; // 0 = disabled
// Timing
extern uint32_t g_tick;
@ -40,3 +44,11 @@ extern uint32_t g_tick;
extern int g_life_delta;
extern int g_life_delta_tick;
extern int g_delta_timeout_ms;
// Dice
extern int g_dice_num;
extern int g_dice_sides;
extern int g_dice_item;
extern char g_dice_csv[DICE_CSV_LEN];
extern int g_dice_rolled;
extern int g_dice_sum;

@ -0,0 +1,105 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"prototype_zone_fills": false,
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"drc_exclusions",
"locked_item_shadows",
"conflict_shadows",
"shapes",
"board_outline_area",
"ly_points"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"integration_disabled": false,
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "commeownder.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [],
"col_order": [],
"col_widths": [],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_time_domain_details": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": -1
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"hierarchy_collapsed": [],
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"ruleAreas": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

@ -1 +1,465 @@
{}
{
"board": {
"3dviewports": [],
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"field_name_whitespace": "warning",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "commeownder.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
},
{
"group_by": false,
"label": "Sim.Pins",
"name": "Sim.Pins",
"show": false
},
{
"group_by": false,
"label": "Sim.Type",
"name": "Sim.Type",
"show": false
},
{
"group_by": false,
"label": "Sim.Device",
"name": "Sim.Device",
"show": false
},
{
"group_by": false,
"label": "Description",
"name": "Description",
"show": false
},
{
"group_by": false,
"label": "#",
"name": "${ITEM_NUMBER}",
"show": false
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "",
"sort_asc": true,
"sort_field": "Reference"
},
"bus_aliases": {},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"top_level_sheets": [
{
"filename": "commeownder.kicad_sch",
"name": "Root",
"uuid": "4f1ddaab-5ed8-4280-ae9f-5da3efc5b3b5"
}
],
"used_designators": "P1,BT1,SW1-4,D1-2,R1-3,Q1,U1-6",
"variants": []
},
"sheets": [
[
"4f1ddaab-5ed8-4280-ae9f-5da3efc5b3b5",
"Root"
]
],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}

File diff suppressed because it is too large Load Diff

@ -16,7 +16,6 @@
#define MAGIC_0 0xC0
#define MAGIC_1 0xDE
#define PLAYER_NAME_LEN 8
#define MAX_OPPONENTS 4
#pragma pack(push, 1)
typedef struct {
@ -26,9 +25,7 @@ typedef struct {
int16_t life;
uint8_t poison;
uint8_t eliminated;
uint8_t player_id;
uint8_t reset_cmd;
uint8_t cmdr_dmg[MAX_OPPONENTS];
} ble_payload_t;
#pragma pack(pop)
@ -42,9 +39,7 @@ static int16_t g_start_life = 40;
static uint8_t g_poison = 0;
static uint8_t g_game_id[2] = {0x42, 0x42};
static uint8_t g_eliminated = 0;
static uint8_t g_player_id = 0;
static uint8_t g_reset_cmd = 0;
static uint8_t g_cmdr_dmg[MAX_OPPONENTS] = {0};
// Monotonic time of last processed reset (microseconds); 0 = never
static gint64 g_last_reset_us = 0;
@ -61,9 +56,7 @@ static void start_advertising(void) {
payload.life = g_life;
payload.poison = g_poison;
payload.eliminated = g_eliminated;
payload.player_id = g_player_id;
payload.reset_cmd = g_reset_cmd;
memcpy(payload.cmdr_dmg, g_cmdr_dmg, MAX_OPPONENTS);
GByteArray *data = g_byte_array_new();
g_byte_array_append(data, (const guint8 *)&payload, sizeof(payload));
@ -75,10 +68,9 @@ static void start_advertising(void) {
g_byte_array_free(data, TRUE);
binc_adapter_start_advertising(default_adapter, adv);
fprintf(stderr, "advertising: name='%s' life=%d poison=%u game_id=%02X%02X pid=%u reset=%u cmdr=[%u,%u,%u,%u]\n",
fprintf(stderr, "advertising: name='%s' life=%d poison=%u game_id=%02X%02X reset=%u\n",
g_name, (int)g_life, (unsigned)g_poison,
g_game_id[0], g_game_id[1], (unsigned)g_player_id, (unsigned)g_reset_cmd,
g_cmdr_dmg[0], g_cmdr_dmg[1], g_cmdr_dmg[2], g_cmdr_dmg[3]);
g_game_id[0], g_game_id[1], (unsigned)g_reset_cmd);
}
static void restart_advertising(void) {
@ -107,11 +99,9 @@ static void on_scan_result(Adapter *adapter, Device *device) {
memcpy(name, p->name, PLAYER_NAME_LEN);
name[PLAYER_NAME_LEN] = '\0';
printf("PEER addr=%s name=%-8s life=%-5d poison=%u pid=%u cmdr=[%u,%u,%u,%u]%s%s\n",
printf("PEER addr=%s name=%-8s life=%-5d poison=%u%s%s\n",
binc_device_get_address(device), name,
(int)p->life, (unsigned)p->poison, (unsigned)p->player_id,
(unsigned)p->cmdr_dmg[0], (unsigned)p->cmdr_dmg[1],
(unsigned)p->cmdr_dmg[2], (unsigned)p->cmdr_dmg[3],
(int)p->life, (unsigned)p->poison,
p->eliminated ? " ELIMINATED" : "",
p->reset_cmd ? " RESET_CMD" : "");
fflush(stdout);
@ -162,15 +152,13 @@ static gboolean cleanup_handler(gpointer user_data) {
static void print_usage(const char *prog) {
fprintf(stderr,
"Usage: %s --name <name> [--life <n>] [--poison <n>] [--game-id <hex4>] [--player-id <n>] [--eliminated] [--reset-cmd] [--cmdr-dmg <idx>:<val>]...\n"
"Usage: %s --name <name> [--life <n>] [--poison <n>] [--game-id <hex4>] [--eliminated] [--reset-cmd]\n"
" --name Player name (max 8 chars, required)\n"
" --life Life total (default: 40)\n"
" --poison Poison counters 0-9 (default: 0)\n"
" --game-id 4-hex-digit game ID (default: 4242)\n"
" --player-id Player slot 0-4 (default: 0)\n"
" --eliminated Mark player as eliminated\n"
" --reset-cmd Advertise reset_cmd=1 (tells peers to reset counters)\n"
" --cmdr-dmg Commander damage from opponent slot idx (0-3), e.g. --cmdr-dmg 0:7 (repeatable)\n",
" --reset-cmd Advertise reset_cmd=1 (tells peers to reset counters)\n",
prog);
}
@ -182,10 +170,8 @@ int main(int argc, char *argv[]) {
{"life", required_argument, NULL, 'l'},
{"poison", required_argument, NULL, 'p'},
{"game-id", required_argument, NULL, 'g'},
{"player-id", required_argument, NULL, 'i'},
{"eliminated", no_argument, NULL, 'e'},
{"reset-cmd", no_argument, NULL, 'r'},
{"cmdr-dmg", required_argument, NULL, 'd'},
{NULL, 0, NULL, 0}
};
@ -214,24 +200,12 @@ int main(int argc, char *argv[]) {
g_game_id[1] = (uint8_t)(id & 0xFF);
break;
}
case 'i':
g_player_id = (uint8_t)atoi(optarg);
break;
case 'e':
g_eliminated = 1;
break;
case 'r':
g_reset_cmd = 1;
break;
case 'd': {
int idx, val;
if (sscanf(optarg, "%d:%d", &idx, &val) != 2 || idx < 0 || idx >= MAX_OPPONENTS || val < 0 || val > 255) {
fprintf(stderr, "invalid cmdr-dmg '%s' (expected <idx>:<val>, idx 0-%d, val 0-255)\n", optarg, MAX_OPPONENTS - 1);
return 1;
}
g_cmdr_dmg[idx] = (uint8_t)val;
break;
}
default:
print_usage(argv[0]);
return 1;

@ -219,26 +219,23 @@ static void sig_cleanup(int sig)
/* ── Simulator subprocess helpers ─────────────────────────────────────────── */
static pid_t sim_start_ex(const char *name, int life, int poison,
const char *game_id, int eliminated,
int player_id, int reset_cmd)
const char *game_id, int eliminated, int reset_cmd)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
close(g_serial_fd);
char lbuf[16], pbuf[16], idbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison);
snprintf(idbuf, sizeof idbuf, "%d", player_id);
char lbuf[16], pbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison);
strncpy(n8, name, 8); n8[8] = '\0';
const char *args[20];
const char *args[16];
int ai = 0;
args[ai++] = g_sim_bin;
args[ai++] = "--name"; args[ai++] = n8;
args[ai++] = "--life"; args[ai++] = lbuf;
args[ai++] = "--poison"; args[ai++] = pbuf;
args[ai++] = "--game-id"; args[ai++] = game_id;
args[ai++] = "--player-id"; args[ai++] = idbuf;
if (eliminated) args[ai++] = "--eliminated";
if (reset_cmd) args[ai++] = "--reset-cmd";
args[ai] = NULL;
@ -252,7 +249,7 @@ static pid_t sim_start_ex(const char *name, int life, int poison,
static pid_t sim_start(const char *name, int life, int poison,
const char *game_id, int eliminated)
{
return sim_start_ex(name, life, poison, game_id, eliminated, 0, 0);
return sim_start_ex(name, life, poison, game_id, eliminated, 0);
}
static void sim_stop(pid_t pid)
@ -290,7 +287,7 @@ static void *sim_pipe_reader(void *arg)
}
static pid_t sim_start_tracked(const char *name, int life, int poison,
const char *game_id, int player_id)
const char *game_id)
{
int pipefd[2];
if (pipe(pipefd) < 0) { perror("pipe"); return -1; }
@ -303,18 +300,16 @@ static pid_t sim_start_tracked(const char *name, int life, int poison,
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
char lbuf[16], pbuf[16], idbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison);
snprintf(idbuf, sizeof idbuf, "%d", player_id);
char lbuf[16], pbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison);
strncpy(n8, name, 8); n8[8] = '\0';
const char *args[] = {
g_sim_bin,
"--name", n8,
"--life", lbuf,
"--poison", pbuf,
"--game-id", game_id,
"--player-id", idbuf,
"--name", n8,
"--life", lbuf,
"--poison", pbuf,
"--game-id", game_id,
NULL
};
execv(g_sim_bin, (char *const *)args);
@ -347,34 +342,6 @@ static int sim_wait2(const char *p1, const char *p2, int secs)
return lq_wait(&g_sim_q, pats, secs, NULL);
}
/* Variant of sim_start_ex with one cmdr-dmg entry, no pipe capture. */
static pid_t sim_start_ex_cmdr(const char *name, int life,
const char *game_id, int cmdr_idx, int cmdr_val)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
close(g_serial_fd);
char lbuf[16], cdmg[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(cdmg, sizeof cdmg, "%d:%d", cmdr_idx, cmdr_val);
strncpy(n8, name, 8); n8[8] = '\0';
const char *args[] = {
g_sim_bin,
"--name", n8,
"--life", lbuf,
"--poison", "0",
"--game-id", game_id,
"--player-id", "0",
"--cmdr-dmg", cdmg,
NULL
};
execv(g_sim_bin, (char *const *)args);
_exit(1);
}
register_child(pid);
return pid;
}
/* ── Key-value parsing ───────────────────────────────────────────────────── */
static int kv_int(const char *line, const char *key, int *out)
@ -671,20 +638,6 @@ static int t_active_peer_not_expired(void)
return 0;
}
/* PEER_RX player_id must match the simulator's --player-id argument. */
static int t_peer_rx_player_id_matches(void)
{
char line[LBUF];
int pid_val;
pid_t sim = sim_start_ex("PIDCHK1", 40, 0, DEFAULT_GAME_ID, 0, 3, 0);
int found = wait2("DBG PEER_RX", "PIDCHK1", PEER_DETECT_S, line);
sim_stop(sim);
CHECK(found);
CHECK(kv_int(line, "pid", &pid_val));
CHECK_EQ(pid_val, 3);
return 0;
}
/*
* When a peer advertises reset_cmd=1, the firmware must reset its counters
* (poison=0, eliminated=0, cmdr all 0) and emit a DBG STATE reflecting that.
@ -694,7 +647,7 @@ static int t_reset_cmd_resets_firmware(void)
char line[LBUF];
int cmdr[4], poison, elim;
pid_t sim = sim_start_ex("RSTALL1", 40, 0, DEFAULT_GAME_ID, 0, 0, 1);
pid_t sim = sim_start_ex("RSTALL1", 40, 0, DEFAULT_GAME_ID, 0, 1);
/* Wait until firmware logs PEER_RX carrying reset=1 */
int found = wait2("DBG PEER_RX", "reset=1", PEER_DETECT_S, NULL);
@ -748,7 +701,7 @@ static int t_fw_life_syncs_to_sim(void)
serial_send("SET life=19\n");
wait1("DBG STATE", 5, NULL);
pid_t sim = sim_start_tracked("SYNCL1", 40, 0, DEFAULT_GAME_ID, 1);
pid_t sim = sim_start_tracked("SYNCL1", 40, 0, DEFAULT_GAME_ID);
int found = sim_wait2("PEER", "life=19", PEER_DETECT_S);
sim_stop_tracked(sim);
serial_send("RESET\n");
@ -765,7 +718,7 @@ static int t_fw_poison_syncs_to_sim(void)
serial_send("SET counter0=3\n");
wait1("DBG STATE", 5, NULL);
pid_t sim = sim_start_tracked("SYNCP1", 40, 0, DEFAULT_GAME_ID, 1);
pid_t sim = sim_start_tracked("SYNCP1", 40, 0, DEFAULT_GAME_ID);
int found = sim_wait2("PEER", "poison=3", PEER_DETECT_S);
sim_stop_tracked(sim);
serial_send("RESET\n");
@ -787,36 +740,6 @@ static int t_sim_life_syncs_to_fw(void)
return 0;
}
/* Firmware logs correct cmdr_dmg from simulator PEER_RX. */
static int t_sim_cmdr_syncs_to_fw(void)
{
char line[LBUF]; int cmdr[4];
pid_t sim = sim_start_ex_cmdr("SCMDR1", 40, DEFAULT_GAME_ID, 0, 9);
int found = wait2("DBG PEER_RX", "SCMDR1", PEER_DETECT_S, line);
sim_stop(sim);
CHECK(found);
CHECK(kv_intlist(line, "cmdr", cmdr, 4));
CHECK_EQ(cmdr[0], 9);
return 0;
}
/* After SET cmdr0 via serial, simulator sees updated cmdr in firmware adv. */
static int t_fw_cmdr_syncs_to_sim(void)
{
serial_send("SET ble=1\n");
wait1("DBG STATE", 5, NULL);
serial_send("SET cmdr0=11\n");
wait1("DBG STATE", 5, NULL);
pid_t sim = sim_start_tracked("CMDR4", 40, 0, DEFAULT_GAME_ID, 1);
int found = sim_wait2("PEER", "cmdr=[11,", PEER_DETECT_S);
sim_stop_tracked(sim);
serial_send("RESET\n");
wait1("DBG STATE", 5, NULL);
CHECK(found);
return 0;
}
/* After SET life=0 triggers elimination, simulator sees ELIMINATED in firmware adv. */
static int t_eliminated_syncs_to_adv(void)
{
@ -825,7 +748,7 @@ static int t_eliminated_syncs_to_adv(void)
serial_send("SET life=0\n");
wait1("DBG STATE", 5, NULL);
pid_t sim = sim_start_tracked("ELIMSYN1", 40, 0, DEFAULT_GAME_ID, 1);
pid_t sim = sim_start_tracked("ELIMSYN1", 40, 0, DEFAULT_GAME_ID);
int found = sim_wait2("PEER", "ELIMINATED", PEER_DETECT_S);
sim_stop_tracked(sim);
serial_send("RESET\n");
@ -853,7 +776,7 @@ static int t_life_cycle_visual(void)
if (!wait1("DBG STATE", 5, NULL)) { FAIL_MSG("device not responding"); return 1; }
lq_drain(&g_q);
pid_t sim = sim_start_tracked("VISLIFE1", 40, 0, DEFAULT_GAME_ID, 1);
pid_t sim = sim_start_tracked("VISLIFE1", 40, 0, DEFAULT_GAME_ID);
/* Ramp 0→80 then 80→0, one step at a time. */
static const struct { int from; int to; const char *label; } ramps[] = {
@ -981,9 +904,6 @@ int main(int argc, char *argv[])
RUN(t_game_id_filter_wrong_rejected);
RUN(t_game_id_filter_correct_accepted);
/* ── Player ID ──────────────────────────────────────────────────────── */
RUN(t_peer_rx_player_id_matches);
/* ── Reset all ──────────────────────────────────────────────────────── */
RUN(t_reset_cmd_resets_firmware);
@ -998,9 +918,7 @@ int main(int argc, char *argv[])
/* ── Bidirectional sync ─────────────────────────────────────────────── */
RUN(t_fw_life_syncs_to_sim);
RUN(t_fw_poison_syncs_to_sim);
RUN(t_fw_cmdr_syncs_to_sim);
RUN(t_sim_life_syncs_to_fw);
RUN(t_sim_cmdr_syncs_to_fw);
RUN(t_eliminated_syncs_to_adv);
/* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */