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" idf_component_register(SRCS "main.c" "draw.c" "ble.c" "game.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
PRIV_REQUIRES driver nvs_flash bt vfs) PRIV_REQUIRES driver nvs_flash bt vfs esp_adc)
if(DEBUG) if(DEBUG)
target_compile_definitions(${COMPONENT_LIB} PUBLIC DEBUG) target_compile_definitions(${COMPONENT_LIB} PUBLIC DEBUG)

@ -12,7 +12,6 @@ volatile int g_ble_scanning;
volatile int g_ble_initialized; volatile int g_ble_initialized;
uint8_t g_own_addr_type; uint8_t g_own_addr_type;
uint8_t g_own_addr[6]; uint8_t g_own_addr[6];
uint8_t g_player_id = 0xFF;
volatile int g_reset_requested; volatile int g_reset_requested;
int g_reset_cmd_ticks; int g_reset_cmd_ticks;
@ -29,10 +28,7 @@ static void ble_adv_start_internal(void)
payload.life = (int16_t)g_life; payload.life = (int16_t)g_life;
payload.poison = (uint8_t)g_counters[0]; payload.poison = (uint8_t)g_counters[0];
payload.eliminated = (uint8_t)g_eliminated; payload.eliminated = (uint8_t)g_eliminated;
payload.player_id = g_player_id;
payload.reset_cmd = (g_reset_cmd_ticks > 0) ? 1 : 0; 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)]; static uint8_t mfr[2 + sizeof(ble_payload_t)];
mfr[0] = 0xFF; mfr[1] = 0xFF; 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); ble_gap_adv_start(g_own_addr_type, NULL, BLE_HS_FOREVER, &params, ble_gap_event_handler, NULL);
#ifdef DEBUG #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.name, (int)payload.life, (unsigned)payload.poison,
payload.game_id[0], payload.game_id[1], (unsigned)payload.eliminated, 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); fflush(stdout);
#endif #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].life = p->life;
g_peers[slot].poison = p->poison; g_peers[slot].poison = p->poison;
g_peers[slot].eliminated = p->eliminated; g_peers[slot].eliminated = p->eliminated;
g_peers[slot].player_id = p->player_id;
g_peers[slot].reset_cmd = p->reset_cmd; 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].last_seen = g_tick;
g_peers[slot].active = 1; g_peers[slot].active = 1;
memcpy(g_peers[slot].name, p->name, PLAYER_NAME_LEN); 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; if (p->reset_cmd && !prev_reset_cmd) g_reset_requested = 1;
#ifdef DEBUG #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, slot,
addr->val[5], addr->val[4], addr->val[3], addr->val[5], addr->val[4], addr->val[3],
addr->val[2], addr->val[1], addr->val[0], addr->val[2], addr->val[1], addr->val[0],
p->name, (int)p->life, (unsigned)p->poison, p->name, (int)p->life, (unsigned)p->poison,
p->game_id[0], p->game_id[1], (unsigned)p->eliminated, p->game_id[0], p->game_id[1], (unsigned)p->eliminated,
(unsigned)p->player_id, (unsigned)p->reset_cmd, (unsigned)p->reset_cmd);
(unsigned)p->cmdr_dmg[0], (unsigned)p->cmdr_dmg[1],
(unsigned)p->cmdr_dmg[2], (unsigned)p->cmdr_dmg[3]);
fflush(stdout); fflush(stdout);
#endif #endif
} }
@ -143,7 +135,6 @@ static void ble_on_sync(void)
{ {
ble_hs_id_infer_auto(0, &g_own_addr_type); ble_hs_id_infer_auto(0, &g_own_addr_type);
ble_hs_id_copy_addr(g_own_addr_type, g_own_addr, NULL); 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; g_ble_initialized = 1;
if (g_ble_enabled) ble_adv_start_internal(); 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) void ble_init(void)
{ {
nimble_port_init(); nimble_port_init();

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

@ -34,21 +34,26 @@
#define BLE_MFR_MAGIC_1 0xDE #define BLE_MFR_MAGIC_1 0xDE
#define BLE_GAME_ID_0 0x42 #define BLE_GAME_ID_0 0x42
#define BLE_GAME_ID_1 0x42 #define BLE_GAME_ID_1 0x42
#define BLE_MAX_PLAYERS 5
#define MAX_BLE_PEERS 4 #define MAX_BLE_PEERS 4
#define BLE_PEER_TIMEOUT 3000 #define BLE_PEER_TIMEOUT 3000
// ── Menus ───────────────────────────────────────────────────────────────────── // ── Menus ─────────────────────────────────────────────────────────────────────
#define NUM_MENUS 4 #define NUM_MENUS 5
#define MENU_LIFE 0 #define MENU_LIFE 0
#define MENU_CMDR 1 #define MENU_CMDR 1
#define MENU_COUNTERS 2 #define MENU_COUNTERS 2
#define MENU_SETTINGS 3 #define MENU_DICE 3
#define MENU_SETTINGS 4
extern const int menu_slot[NUM_MENUS]; 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 ────────────────────────────────────────────────────────────────── // ── Settings ──────────────────────────────────────────────────────────────────
#define NUM_SETTINGS 12 #define NUM_SETTINGS 13
#define SET_BRIGHTNESS 0 #define SET_BRIGHTNESS 0
#define SET_START_LIFE 1 #define SET_START_LIFE 1
#define SET_NUM_OPP 2 #define SET_NUM_OPP 2
@ -61,6 +66,7 @@ extern const int menu_slot[NUM_MENUS];
#define SET_LR_HOLD 9 #define SET_LR_HOLD 9
#define SET_DISPLAY_FLIP 10 #define SET_DISPLAY_FLIP 10
#define SET_DELTA_TIMEOUT 11 #define SET_DELTA_TIMEOUT 11
#define SET_AUTO_SLEEP 12
#define HOLD_MS_MIN 50 #define HOLD_MS_MIN 50
#define HOLD_MS_MAX 2000 #define HOLD_MS_MAX 2000
#define DELTA_TIMEOUT_MIN 100 #define DELTA_TIMEOUT_MIN 100
@ -97,6 +103,14 @@ extern const char *counter_names[NUM_COUNTERS];
#define FONT_BASE 0x20 #define FONT_BASE 0x20
#define FONT_MAX 0x5A #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 ─────────────────────────────────────────────────────────────────────── // ── NVS ───────────────────────────────────────────────────────────────────────
#define NVS_NS "settings" #define NVS_NS "settings"
#define SAVE_DELAY 300 #define SAVE_DELAY 300

@ -9,11 +9,13 @@
#include <string.h> #include <string.h>
#include <stdio.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] = { const char *setting_names[NUM_SETTINGS] = {
"LED BRIGHTNESS", "STARTING LIFE", "NUM OPPONENTS", "BLE", "LED BRIGHTNESS", "STARTING LIFE", "NUM OPPONENTS", "BLE",
"RESET", "RESET ALL", "GAME ID", "PLAYER NAME", "RESET", "RESET ALL", "GAME ID", "PLAYER NAME",
"MENU HOLD MS", "LR HOLD MS", "FLIP DISPLAY", "DELTA TIMEOUT", "MENU HOLD MS", "LR HOLD MS", "FLIP DISPLAY", "DELTA TIMEOUT",
"AUTO SLEEP",
}; };
#define LEDC_TIMER_SEL LEDC_TIMER_0 #define LEDC_TIMER_SEL LEDC_TIMER_0
@ -149,6 +151,53 @@ void oled_draw_header(void)
pages[1][sx+col] = bg ^ icon[1][col]; 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]); 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. // 5×8 mini-icons (vertical-byte, bit0=top), same format as font5x8 columns.
// Teardrop: pointed top, round bottom (for poison) // Teardrop: pointed top, round bottom (for poison)
static const uint8_t icon_mini_drop[5] = {0x3C, 0x7E, 0x7F, 0x7E, 0x3C}; static const uint8_t icon_mini_drop[5] = {0x3C, 0x7E, 0x7F, 0x7E, 0x3C};
// Lightning bolt: zigzag (for storm) // Lightning bolt: thin zigzag from center-top with multiple branches
static const uint8_t icon_mini_bolt[5] = {0x0C, 0x0E, 0x1F, 0x39, 0x70}; 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, static int pxbuf_icon(uint8_t pages[][OLED_WIDTH], int content_h,
int base, int x, const uint8_t *cols, int width, int inv) 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_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_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); 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); 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) void oled_draw_life(void)
{ {
const int content_pages = OLED_PAGES - HEADER_PAGES; 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_players(void);
void oled_draw_settings(void); void oled_draw_settings(void);
void oled_draw_life(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) // Plus (counters)
{ {0x00,0x00,0x00,0xC0,0xC0,0xC0,0xF8,0xF8,0xF8,0xF8,0xC0,0xC0,0xC0,0x00,0x00,0x00}, { {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} }, {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) // Cog (settings)
{ {0x00,0x80,0xDC,0xFC,0xFC,0xB8,0x9C,0xFE,0xFE,0x9C,0xB8,0xFC,0xFC,0xDC,0x80,0x00}, { {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} }, {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_lr_hold_ms = 500;
g_display_flip = 0; g_display_flip = 0;
g_delta_timeout_ms = 1000; g_delta_timeout_ms = 1000;
g_sleep_timeout_min = SLEEP_TIMEOUT_DEF;
} }
void game_reset(void) 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, "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, "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, "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; if (nvs_get_i32(nvs, "life", &val) == ESP_OK) g_life = (int)val;
char key[8]; char key[8];
for (int i = 0; i < MAX_OPPONENTS; i++) { 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, "lr_hold", (int32_t)g_lr_hold_ms);
nvs_set_i32(nvs, "disp_flip", (int32_t)g_display_flip); 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, "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_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); nvs_set_i32(nvs, "life", (int32_t)g_life);
char key[8]; char key[8];
for (int i = 0; i < MAX_OPPONENTS; i++) { for (int i = 0; i < MAX_OPPONENTS; i++) {

@ -10,6 +10,11 @@
#include "nvs_flash.h" #include "nvs_flash.h"
#include "nvs.h" #include "nvs.h"
#include "esp_err.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/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include <stdio.h> #include <stdio.h>
@ -18,6 +23,12 @@
// ── Global state ────────────────────────────────────────────────────────────── // ── Global state ──────────────────────────────────────────────────────────────
int g_sleep_mode = 0; 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 // Game
int g_life; int g_life;
@ -58,14 +69,22 @@ int g_life_delta = 0;
int g_life_delta_tick = 0; int g_life_delta_tick = 0;
int g_delta_timeout_ms = 1000; 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 ─────────────────────────────────────────────────────── // ── Serial command task ───────────────────────────────────────────────────────
static void serial_print_state(void) 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_life, (unsigned)g_counters[0],
g_cmdr_damage[0], g_cmdr_damage[1], g_cmdr_damage[2], g_cmdr_damage[3], 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_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); fflush(stdout);
} }
@ -154,6 +173,21 @@ void app_main(void)
check_elimination(); check_elimination();
g_led_max = (uint8_t)(255 * g_brightness_pct / 100); 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_init();
oled_set_flip(g_display_flip); oled_set_flip(g_display_flip);
oled_clear(); oled_clear();
@ -193,6 +227,13 @@ void app_main(void)
int players_tick=0; int players_tick=0;
int settings_dirty=0, settings_save_tick=0; int settings_dirty=0, settings_save_tick=0;
int sleep_tick=0, sleep_fired=0, ble_pre_sleep=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) #define MARK_DIRTY() do { settings_dirty = 1; settings_save_tick = 0; } while (0)
float breath_phase=0.0f; float breath_phase=0.0f;
@ -321,6 +362,9 @@ void app_main(void)
} else if (g_active_menu == MENU_COUNTERS) { } else if (g_active_menu == MENU_COUNTERS) {
g_active_counter = (g_active_counter + 1) % NUM_COUNTERS; g_active_counter = (g_active_counter + 1) % NUM_COUNTERS;
changed = 1; 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) { } else if (g_active_menu == MENU_LIFE) {
int nc = g_ble_enabled ? 0 : g_num_opponents; int nc = g_ble_enabled ? 0 : g_num_opponents;
if (g_ble_enabled) if (g_ble_enabled)
@ -389,6 +433,9 @@ void app_main(void)
} else if (g_active_menu == MENU_COUNTERS) { } else if (g_active_menu == MENU_COUNTERS) {
g_active_counter = (g_active_counter - 1 + NUM_COUNTERS) % NUM_COUNTERS; g_active_counter = (g_active_counter - 1 + NUM_COUNTERS) % NUM_COUNTERS;
changed = 1; 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) { } else if (g_active_menu == MENU_LIFE) {
int nc = g_ble_enabled ? 0 : g_num_opponents; int nc = g_ble_enabled ? 0 : g_num_opponents;
if (g_ble_enabled) if (g_ble_enabled)
@ -489,6 +536,28 @@ void app_main(void)
g_counters[g_active_counter] += d; g_counters[g_active_counter] += d;
if (g_counters[g_active_counter] < 0) g_counters[g_active_counter] = 0; if (g_counters[g_active_counter] < 0) g_counters[g_active_counter] = 0;
changed = 1; MARK_DIRTY(); check_elimination(); 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) { } else if (g_active_menu == MENU_SETTINGS) {
changed = 1; MARK_DIRTY(); changed = 1; MARK_DIRTY();
switch (g_active_setting) { 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_MIN) g_delta_timeout_ms = DELTA_TIMEOUT_MIN;
if (g_delta_timeout_ms > DELTA_TIMEOUT_MAX) g_delta_timeout_ms = DELTA_TIMEOUT_MAX; if (g_delta_timeout_ms > DELTA_TIMEOUT_MAX) g_delta_timeout_ms = DELTA_TIMEOUT_MAX;
break; 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; g_peers[i].active = 0;
} }
} }
player_id_resolve();
} }
// ── Autosave settings ───────────────────────────────────────────────── // ── Autosave settings ─────────────────────────────────────────────────
@ -639,6 +713,65 @@ void app_main(void)
led_update_for_count(g_life, 255); 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 ────────────────────────────────────────────────── // ── Display dispatch ──────────────────────────────────────────────────
if (changed) { if (changed) {
switch (g_active_menu) { switch (g_active_menu) {
@ -646,29 +779,22 @@ void app_main(void)
oled_draw_life(); oled_draw_life();
break; break;
case MENU_CMDR: case MENU_CMDR:
for (int i = 0; i < g_num_opponents; i++) { if (!g_ble_enabled) {
int found = 0; for (int i = 0; i < g_num_opponents; i++) {
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); snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "CMD %d", i+1);
opponent_label_ptrs[i] = opponent_labels[i]; 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); oled_draw_list(opponent_label_ptrs, g_cmdr_damage, g_num_opponents, g_active_opponent);
else } else {
oled_draw_players(); oled_draw_players();
}
break; break;
case MENU_COUNTERS: case MENU_COUNTERS:
oled_draw_list(counter_names, g_counters, NUM_COUNTERS, g_active_counter); oled_draw_list(counter_names, g_counters, NUM_COUNTERS, g_active_counter);
break; break;
case MENU_DICE:
oled_draw_dice();
break;
case MENU_SETTINGS: case MENU_SETTINGS:
oled_draw_settings(); oled_draw_settings();
break; break;

@ -33,6 +33,10 @@ extern int g_display_flip;
// Peers // Peers
extern ble_peer_t g_peers[MAX_BLE_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 // Timing
extern uint32_t g_tick; extern uint32_t g_tick;
@ -40,3 +44,11 @@ extern uint32_t g_tick;
extern int g_life_delta; extern int g_life_delta;
extern int g_life_delta_tick; extern int g_life_delta_tick;
extern int g_delta_timeout_ms; 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_0 0xC0
#define MAGIC_1 0xDE #define MAGIC_1 0xDE
#define PLAYER_NAME_LEN 8 #define PLAYER_NAME_LEN 8
#define MAX_OPPONENTS 4
#pragma pack(push, 1) #pragma pack(push, 1)
typedef struct { typedef struct {
@ -26,9 +25,7 @@ typedef struct {
int16_t life; int16_t life;
uint8_t poison; uint8_t poison;
uint8_t eliminated; uint8_t eliminated;
uint8_t player_id;
uint8_t reset_cmd; uint8_t reset_cmd;
uint8_t cmdr_dmg[MAX_OPPONENTS];
} ble_payload_t; } ble_payload_t;
#pragma pack(pop) #pragma pack(pop)
@ -42,9 +39,7 @@ static int16_t g_start_life = 40;
static uint8_t g_poison = 0; static uint8_t g_poison = 0;
static uint8_t g_game_id[2] = {0x42, 0x42}; static uint8_t g_game_id[2] = {0x42, 0x42};
static uint8_t g_eliminated = 0; static uint8_t g_eliminated = 0;
static uint8_t g_player_id = 0;
static uint8_t g_reset_cmd = 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 // Monotonic time of last processed reset (microseconds); 0 = never
static gint64 g_last_reset_us = 0; static gint64 g_last_reset_us = 0;
@ -61,9 +56,7 @@ static void start_advertising(void) {
payload.life = g_life; payload.life = g_life;
payload.poison = g_poison; payload.poison = g_poison;
payload.eliminated = g_eliminated; payload.eliminated = g_eliminated;
payload.player_id = g_player_id;
payload.reset_cmd = g_reset_cmd; payload.reset_cmd = g_reset_cmd;
memcpy(payload.cmdr_dmg, g_cmdr_dmg, MAX_OPPONENTS);
GByteArray *data = g_byte_array_new(); GByteArray *data = g_byte_array_new();
g_byte_array_append(data, (const guint8 *)&payload, sizeof(payload)); 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); g_byte_array_free(data, TRUE);
binc_adapter_start_advertising(default_adapter, adv); 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_name, (int)g_life, (unsigned)g_poison,
g_game_id[0], g_game_id[1], (unsigned)g_player_id, (unsigned)g_reset_cmd, g_game_id[0], g_game_id[1], (unsigned)g_reset_cmd);
g_cmdr_dmg[0], g_cmdr_dmg[1], g_cmdr_dmg[2], g_cmdr_dmg[3]);
} }
static void restart_advertising(void) { 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); memcpy(name, p->name, PLAYER_NAME_LEN);
name[PLAYER_NAME_LEN] = '\0'; 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, binc_device_get_address(device), name,
(int)p->life, (unsigned)p->poison, (unsigned)p->player_id, (int)p->life, (unsigned)p->poison,
(unsigned)p->cmdr_dmg[0], (unsigned)p->cmdr_dmg[1],
(unsigned)p->cmdr_dmg[2], (unsigned)p->cmdr_dmg[3],
p->eliminated ? " ELIMINATED" : "", p->eliminated ? " ELIMINATED" : "",
p->reset_cmd ? " RESET_CMD" : ""); p->reset_cmd ? " RESET_CMD" : "");
fflush(stdout); fflush(stdout);
@ -162,15 +152,13 @@ static gboolean cleanup_handler(gpointer user_data) {
static void print_usage(const char *prog) { static void print_usage(const char *prog) {
fprintf(stderr, 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" " --name Player name (max 8 chars, required)\n"
" --life Life total (default: 40)\n" " --life Life total (default: 40)\n"
" --poison Poison counters 0-9 (default: 0)\n" " --poison Poison counters 0-9 (default: 0)\n"
" --game-id 4-hex-digit game ID (default: 4242)\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" " --eliminated Mark player as eliminated\n"
" --reset-cmd Advertise reset_cmd=1 (tells peers to reset counters)\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",
prog); prog);
} }
@ -182,10 +170,8 @@ int main(int argc, char *argv[]) {
{"life", required_argument, NULL, 'l'}, {"life", required_argument, NULL, 'l'},
{"poison", required_argument, NULL, 'p'}, {"poison", required_argument, NULL, 'p'},
{"game-id", required_argument, NULL, 'g'}, {"game-id", required_argument, NULL, 'g'},
{"player-id", required_argument, NULL, 'i'},
{"eliminated", no_argument, NULL, 'e'}, {"eliminated", no_argument, NULL, 'e'},
{"reset-cmd", no_argument, NULL, 'r'}, {"reset-cmd", no_argument, NULL, 'r'},
{"cmdr-dmg", required_argument, NULL, 'd'},
{NULL, 0, NULL, 0} {NULL, 0, NULL, 0}
}; };
@ -214,24 +200,12 @@ int main(int argc, char *argv[]) {
g_game_id[1] = (uint8_t)(id & 0xFF); g_game_id[1] = (uint8_t)(id & 0xFF);
break; break;
} }
case 'i':
g_player_id = (uint8_t)atoi(optarg);
break;
case 'e': case 'e':
g_eliminated = 1; g_eliminated = 1;
break; break;
case 'r': case 'r':
g_reset_cmd = 1; g_reset_cmd = 1;
break; 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: default:
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;

@ -219,26 +219,23 @@ static void sig_cleanup(int sig)
/* ── Simulator subprocess helpers ─────────────────────────────────────────── */ /* ── Simulator subprocess helpers ─────────────────────────────────────────── */
static pid_t sim_start_ex(const char *name, int life, int poison, static pid_t sim_start_ex(const char *name, int life, int poison,
const char *game_id, int eliminated, const char *game_id, int eliminated, int reset_cmd)
int player_id, int reset_cmd)
{ {
pid_t pid = fork(); pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; } if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) { if (pid == 0) {
close(g_serial_fd); close(g_serial_fd);
char lbuf[16], pbuf[16], idbuf[16], n8[9]; char lbuf[16], pbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life); snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison); snprintf(pbuf, sizeof pbuf, "%d", poison);
snprintf(idbuf, sizeof idbuf, "%d", player_id);
strncpy(n8, name, 8); n8[8] = '\0'; strncpy(n8, name, 8); n8[8] = '\0';
const char *args[20]; const char *args[16];
int ai = 0; int ai = 0;
args[ai++] = g_sim_bin; args[ai++] = g_sim_bin;
args[ai++] = "--name"; args[ai++] = n8; args[ai++] = "--name"; args[ai++] = n8;
args[ai++] = "--life"; args[ai++] = lbuf; args[ai++] = "--life"; args[ai++] = lbuf;
args[ai++] = "--poison"; args[ai++] = pbuf; args[ai++] = "--poison"; args[ai++] = pbuf;
args[ai++] = "--game-id"; args[ai++] = game_id; args[ai++] = "--game-id"; args[ai++] = game_id;
args[ai++] = "--player-id"; args[ai++] = idbuf;
if (eliminated) args[ai++] = "--eliminated"; if (eliminated) args[ai++] = "--eliminated";
if (reset_cmd) args[ai++] = "--reset-cmd"; if (reset_cmd) args[ai++] = "--reset-cmd";
args[ai] = NULL; 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, static pid_t sim_start(const char *name, int life, int poison,
const char *game_id, int eliminated) 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) 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, 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]; int pipefd[2];
if (pipe(pipefd) < 0) { perror("pipe"); return -1; } 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]); close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]); close(pipefd[1]);
char lbuf[16], pbuf[16], idbuf[16], n8[9]; char lbuf[16], pbuf[16], n8[9];
snprintf(lbuf, sizeof lbuf, "%d", life); snprintf(lbuf, sizeof lbuf, "%d", life);
snprintf(pbuf, sizeof pbuf, "%d", poison); snprintf(pbuf, sizeof pbuf, "%d", poison);
snprintf(idbuf, sizeof idbuf, "%d", player_id);
strncpy(n8, name, 8); n8[8] = '\0'; strncpy(n8, name, 8); n8[8] = '\0';
const char *args[] = { const char *args[] = {
g_sim_bin, g_sim_bin,
"--name", n8, "--name", n8,
"--life", lbuf, "--life", lbuf,
"--poison", pbuf, "--poison", pbuf,
"--game-id", game_id, "--game-id", game_id,
"--player-id", idbuf,
NULL NULL
}; };
execv(g_sim_bin, (char *const *)args); 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); 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 ───────────────────────────────────────────────────── */ /* ── Key-value parsing ───────────────────────────────────────────────────── */
static int kv_int(const char *line, const char *key, int *out) 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; 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 * 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. * (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]; char line[LBUF];
int cmdr[4], poison, elim; 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 */ /* Wait until firmware logs PEER_RX carrying reset=1 */
int found = wait2("DBG PEER_RX", "reset=1", PEER_DETECT_S, NULL); 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"); serial_send("SET life=19\n");
wait1("DBG STATE", 5, NULL); 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); int found = sim_wait2("PEER", "life=19", PEER_DETECT_S);
sim_stop_tracked(sim); sim_stop_tracked(sim);
serial_send("RESET\n"); serial_send("RESET\n");
@ -765,7 +718,7 @@ static int t_fw_poison_syncs_to_sim(void)
serial_send("SET counter0=3\n"); serial_send("SET counter0=3\n");
wait1("DBG STATE", 5, NULL); 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); int found = sim_wait2("PEER", "poison=3", PEER_DETECT_S);
sim_stop_tracked(sim); sim_stop_tracked(sim);
serial_send("RESET\n"); serial_send("RESET\n");
@ -787,36 +740,6 @@ static int t_sim_life_syncs_to_fw(void)
return 0; 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. */ /* After SET life=0 triggers elimination, simulator sees ELIMINATED in firmware adv. */
static int t_eliminated_syncs_to_adv(void) 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"); serial_send("SET life=0\n");
wait1("DBG STATE", 5, NULL); 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); int found = sim_wait2("PEER", "ELIMINATED", PEER_DETECT_S);
sim_stop_tracked(sim); sim_stop_tracked(sim);
serial_send("RESET\n"); 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; } if (!wait1("DBG STATE", 5, NULL)) { FAIL_MSG("device not responding"); return 1; }
lq_drain(&g_q); 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. */ /* Ramp 0→80 then 80→0, one step at a time. */
static const struct { int from; int to; const char *label; } ramps[] = { 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_wrong_rejected);
RUN(t_game_id_filter_correct_accepted); RUN(t_game_id_filter_correct_accepted);
/* ── Player ID ──────────────────────────────────────────────────────── */
RUN(t_peer_rx_player_id_matches);
/* ── Reset all ──────────────────────────────────────────────────────── */ /* ── Reset all ──────────────────────────────────────────────────────── */
RUN(t_reset_cmd_resets_firmware); RUN(t_reset_cmd_resets_firmware);
@ -998,9 +918,7 @@ int main(int argc, char *argv[])
/* ── Bidirectional sync ─────────────────────────────────────────────── */ /* ── Bidirectional sync ─────────────────────────────────────────────── */
RUN(t_fw_life_syncs_to_sim); RUN(t_fw_life_syncs_to_sim);
RUN(t_fw_poison_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_life_syncs_to_fw);
RUN(t_sim_cmdr_syncs_to_fw);
RUN(t_eliminated_syncs_to_adv); RUN(t_eliminated_syncs_to_adv);
/* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */ /* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */