Initial commit
parent
2f9ec49e72
commit
11e5953ccd
@ -0,0 +1,3 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: build
|
||||
Remove: [-mlongcalls, -fstrict-volatile-bitfields, -fno-tree-switch-conversion]
|
||||
@ -0,0 +1,2 @@
|
||||
case/venv/
|
||||
case/output/*.stl
|
||||
@ -0,0 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(commeownder)
|
||||
|
||||
if(DEBUG)
|
||||
add_compile_definitions(DEBUG)
|
||||
endif()
|
||||
@ -0,0 +1,62 @@
|
||||
SHELL := /bin/bash
|
||||
.ONESHELL:
|
||||
|
||||
ESP_ENV := . ~/scripts/esp_env.sh
|
||||
SIM_BUILD := simulator/build
|
||||
PORT ?= /dev/ttyUSB0
|
||||
|
||||
# All firmware targets build with DEBUG=1 so serial commands and DBG lines
|
||||
# are always available for testing and monitoring.
|
||||
IDF := $(ESP_ENV) && idf.py -DDEBUG=1
|
||||
|
||||
.PHONY: all firmware sim test flash monitor flash-monitor clean firmware-clean sim-clean case case_live case_clean
|
||||
|
||||
all: firmware sim
|
||||
|
||||
firmware:
|
||||
$(IDF) build
|
||||
|
||||
flash:
|
||||
$(IDF) flash
|
||||
|
||||
monitor:
|
||||
$(IDF) monitor
|
||||
|
||||
flash-monitor:
|
||||
$(IDF) flash monitor
|
||||
|
||||
sim:
|
||||
cmake --build $(SIM_BUILD) --parallel
|
||||
|
||||
test: sim flash
|
||||
sleep 5
|
||||
$(SIM_BUILD)/test_ble --port $(PORT)
|
||||
|
||||
clean: firmware-clean sim-clean case_clean
|
||||
|
||||
firmware-clean:
|
||||
$(IDF) fullclean
|
||||
|
||||
sim-clean:
|
||||
cmake --build $(SIM_BUILD) --target clean
|
||||
|
||||
# --- case (build123d) ---
|
||||
CASE_VENV := case/venv
|
||||
CASE_PYTHON := $(CASE_VENV)/bin/python
|
||||
CASE_PIP := $(CASE_VENV)/bin/pip
|
||||
CASE_FW := $(CASE_VENV)/bin/fw123d
|
||||
|
||||
$(CASE_VENV)/.installed: case/requirements.txt
|
||||
python3.13 -m venv $(CASE_VENV)
|
||||
$(CASE_PIP) install --upgrade pip
|
||||
$(CASE_PIP) install -r case/requirements.txt
|
||||
touch $@
|
||||
|
||||
case: $(CASE_VENV)/.installed
|
||||
$(CASE_PYTHON) case/case.py
|
||||
|
||||
case_live: $(CASE_VENV)/.installed
|
||||
$(CASE_FW) case/case.py
|
||||
|
||||
case_clean:
|
||||
rm -rf $(CASE_VENV) case/output/*.stl
|
||||
@ -0,0 +1,61 @@
|
||||
from build123d import *
|
||||
|
||||
# --- Facade panel ---
|
||||
PANEL = 1.6 # plate thickness (Cherry MX standard ~1.5 mm)
|
||||
|
||||
# --- Component footprints ---
|
||||
SW_SIZE = 14.0 # Cherry MX plate cutout (square)
|
||||
SW_PITCH = 19.0 # switch centre-to-centre
|
||||
N_SW = 3
|
||||
|
||||
OLED_W = 25.0 # 0.96" SSD1306 visible window width
|
||||
OLED_H = 13.0 # visible window height
|
||||
|
||||
# --- OLED alignment pegs (solid 2 mm, 24 mm square pattern) ---
|
||||
POST_D = 2.0 # peg diameter
|
||||
POST_SPACING = 24.0 # hole centre-to-centre
|
||||
POST_H = 12.0 # peg length behind panel
|
||||
|
||||
# --- Layout ---
|
||||
# MARGIN_Y=7 and OLED_SW_GAP=9 push outer_h to 50 mm, giving the top pegs
|
||||
# 1.5 mm clearance from the panel edge and the bottom pegs 2.5 mm clearance
|
||||
# above the switch cutout tops.
|
||||
MARGIN_X = 3.0
|
||||
MARGIN_Y = 7.0
|
||||
OLED_SW_GAP = 9.0
|
||||
|
||||
outer_w = (N_SW - 1) * SW_PITCH + SW_SIZE + 2 * MARGIN_X # 58 mm
|
||||
outer_h = 2 * MARGIN_Y + OLED_H + OLED_SW_GAP + SW_SIZE # 50 mm
|
||||
|
||||
oled_cy = outer_h / 2 - MARGIN_Y - OLED_H / 2 # 11.5 mm
|
||||
sw_cy = -outer_h / 2 + MARGIN_Y + SW_SIZE / 2 # -11.0 mm
|
||||
|
||||
with BuildPart() as case:
|
||||
Box(outer_w, outer_h, PANEL)
|
||||
|
||||
with BuildSketch(Plane.XY.offset(PANEL / 2)):
|
||||
with Locations((0, oled_cy)):
|
||||
Rectangle(OLED_W, OLED_H)
|
||||
with Locations((-SW_PITCH, sw_cy), (0, sw_cy), (SW_PITCH, sw_cy)):
|
||||
RectangleRounded(SW_SIZE, SW_SIZE, 0.3)
|
||||
extrude(amount=-PANEL, mode=Mode.SUBTRACT)
|
||||
|
||||
# Solid 2 mm alignment pegs extending from the back face
|
||||
half_s = POST_SPACING / 2
|
||||
post_z = -PANEL / 2 - POST_H / 2
|
||||
post_xy = [
|
||||
(-half_s, oled_cy - half_s),
|
||||
( half_s, oled_cy - half_s),
|
||||
(-half_s, oled_cy + half_s),
|
||||
( half_s, oled_cy + half_s),
|
||||
]
|
||||
with Locations([(x, y, post_z) for x, y in post_xy]):
|
||||
Cylinder(POST_D / 2, POST_H)
|
||||
|
||||
export_stl(case.part, "case/output/case.stl")
|
||||
|
||||
try:
|
||||
from ocp_vscode import show
|
||||
show(case)
|
||||
except Exception:
|
||||
pass
|
||||
@ -0,0 +1,4 @@
|
||||
build123d>=0.10.0
|
||||
cadquery-ocp>=7.8,<7.9
|
||||
ocp-vscode
|
||||
git+https://github.com/jdegenstein/filewatcher123d
|
||||
@ -0,0 +1,7 @@
|
||||
idf_component_register(SRCS "main.c" "draw.c" "ble.c" "game.c"
|
||||
INCLUDE_DIRS "."
|
||||
PRIV_REQUIRES driver nvs_flash bt vfs)
|
||||
|
||||
if(DEBUG)
|
||||
target_compile_definitions(${COMPONENT_LIB} PUBLIC DEBUG)
|
||||
endif()
|
||||
@ -0,0 +1,216 @@
|
||||
#include "ble.h"
|
||||
#include "state.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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;
|
||||
|
||||
static int ble_gap_event_handler(struct ble_gap_event *event, void *arg);
|
||||
|
||||
static void ble_adv_start_internal(void)
|
||||
{
|
||||
ble_payload_t payload;
|
||||
payload.magic[0] = BLE_MFR_MAGIC_0;
|
||||
payload.magic[1] = BLE_MFR_MAGIC_1;
|
||||
payload.game_id[0] = g_game_id[0];
|
||||
payload.game_id[1] = g_game_id[1];
|
||||
memcpy(payload.name, g_player_name, PLAYER_NAME_LEN);
|
||||
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;
|
||||
memcpy(mfr + 2, &payload, sizeof(payload));
|
||||
|
||||
struct ble_hs_adv_fields fields = {0};
|
||||
fields.mfg_data = mfr;
|
||||
fields.mfg_data_len = sizeof(mfr);
|
||||
ble_gap_adv_set_fields(&fields);
|
||||
|
||||
struct ble_gap_adv_params params = {0};
|
||||
params.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
params.disc_mode = BLE_GAP_DISC_MODE_NON;
|
||||
params.itvl_min = 0x0320; // 500ms
|
||||
params.itvl_max = 0x0320;
|
||||
ble_gap_adv_start(g_own_addr_type, NULL, BLE_HS_FOREVER, ¶ms, 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",
|
||||
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);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ble_adv_update(void)
|
||||
{
|
||||
if (!g_ble_initialized || g_ble_scanning) return;
|
||||
ble_gap_adv_stop();
|
||||
if (g_ble_enabled) ble_adv_start_internal();
|
||||
}
|
||||
|
||||
static void ble_update_peer(const ble_addr_t *addr, const ble_payload_t *p)
|
||||
{
|
||||
if (p->game_id[0] != g_game_id[0] || p->game_id[1] != g_game_id[1]) return;
|
||||
|
||||
int slot = -1;
|
||||
for (int i = 0; i < MAX_BLE_PEERS; i++) {
|
||||
if (g_peers[i].active && memcmp(&g_peers[i].addr, addr, sizeof(ble_addr_t)) == 0) {
|
||||
slot = i; break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
for (int i = 0; i < MAX_BLE_PEERS; i++) {
|
||||
if (!g_peers[i].active) { slot = i; break; }
|
||||
}
|
||||
if (slot < 0) {
|
||||
slot = 0;
|
||||
for (int i = 1; i < MAX_BLE_PEERS; i++)
|
||||
if (g_peers[i].last_seen < g_peers[slot].last_seen) slot = i;
|
||||
}
|
||||
}
|
||||
uint8_t prev_reset_cmd = g_peers[slot].reset_cmd;
|
||||
g_peers[slot].addr = *addr;
|
||||
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);
|
||||
g_peers[slot].name[PLAYER_NAME_LEN] = '\0';
|
||||
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",
|
||||
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]);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int ble_gap_event_handler(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
if (event->type == BLE_GAP_EVENT_DISC) {
|
||||
const uint8_t *adv_data = event->disc.data;
|
||||
int len = event->disc.length_data;
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
uint8_t adlen = adv_data[i];
|
||||
if (adlen == 0 || i + adlen >= len) break;
|
||||
uint8_t adtype = adv_data[i+1];
|
||||
if (adtype == 0xFF && adlen >= 1 + 2 + (int)sizeof(ble_payload_t)) {
|
||||
const uint8_t *payload_ptr = adv_data + i + 4;
|
||||
if (payload_ptr[0] == BLE_MFR_MAGIC_0 && payload_ptr[1] == BLE_MFR_MAGIC_1) {
|
||||
ble_update_peer(&event->disc.addr, (const ble_payload_t *)payload_ptr);
|
||||
}
|
||||
}
|
||||
i += 1 + adlen;
|
||||
}
|
||||
} else if (event->type == BLE_GAP_EVENT_DISC_COMPLETE) {
|
||||
g_ble_scanning = 0;
|
||||
if (g_ble_enabled) ble_adv_start_internal();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
static void ble_on_reset(int reason) { (void)reason; }
|
||||
|
||||
static void ble_host_task(void *arg)
|
||||
{
|
||||
nimble_port_run();
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
static void ble_manager_task(void *arg)
|
||||
{
|
||||
while (!g_ble_initialized) vTaskDelay(pdMS_TO_TICKS(100));
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
if (!g_ble_enabled || g_ble_scanning) continue;
|
||||
|
||||
g_ble_scanning = 1;
|
||||
ble_gap_adv_stop();
|
||||
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.itvl = 0x0100; disc_params.window = 0x0080; disc_params.passive = 1;
|
||||
int rc = ble_gap_disc(g_own_addr_type, 5000, &disc_params, ble_gap_event_handler, NULL);
|
||||
if (rc != 0) {
|
||||
g_ble_scanning = 0;
|
||||
if (g_ble_enabled) ble_adv_start_internal();
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(6000));
|
||||
if (g_ble_scanning) {
|
||||
ble_gap_disc_cancel();
|
||||
g_ble_scanning = 0;
|
||||
if (g_ble_enabled) ble_adv_start_internal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
ble_hs_cfg.sync_cb = ble_on_sync;
|
||||
ble_hs_cfg.reset_cb = ble_on_reset;
|
||||
ble_svc_gap_init();
|
||||
nimble_port_freertos_init(ble_host_task);
|
||||
xTaskCreate(ble_manager_task, "ble_mgr", 4096, NULL, 5, NULL);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "config.h"
|
||||
#include "host/ble_hs.h"
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t magic[2];
|
||||
uint8_t game_id[2];
|
||||
char name[PLAYER_NAME_LEN];
|
||||
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 {
|
||||
ble_addr_t addr;
|
||||
char name[PLAYER_NAME_LEN + 1];
|
||||
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;
|
||||
|
||||
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);
|
||||
@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────────────
|
||||
#define BTN_RIGHT_GPIO 23
|
||||
#define BTN_LEFT_GPIO 4
|
||||
#define BTN_MID_GPIO 5
|
||||
|
||||
// ── RGB LED ───────────────────────────────────────────────────────────────────
|
||||
#define LED_R_GPIO 13
|
||||
#define LED_G_GPIO 14
|
||||
#define LED_B_GPIO 27
|
||||
|
||||
// ── I2C / OLED ────────────────────────────────────────────────────────────────
|
||||
#define I2C_PORT I2C_NUM_0
|
||||
#define I2C_SDA_GPIO 21
|
||||
#define I2C_SCL_GPIO 22
|
||||
#define I2C_FREQ_HZ 400000
|
||||
#define OLED_ADDR 0x3C
|
||||
#define OLED_WIDTH 128
|
||||
#define OLED_PAGES 8
|
||||
#define HEADER_PAGES 2
|
||||
|
||||
// ── Button timing ─────────────────────────────────────────────────────────────
|
||||
#define HOLD_DELAY 50
|
||||
#define HOLD_REPEAT 10
|
||||
#define MID_HOLD_MENU 15
|
||||
#define MID_HOLD_SETTINGS 100
|
||||
|
||||
// ── BLE ───────────────────────────────────────────────────────────────────────
|
||||
#define BLE_MFR_MAGIC_0 0xC0
|
||||
#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 MENU_LIFE 0
|
||||
#define MENU_CMDR 1
|
||||
#define MENU_COUNTERS 2
|
||||
#define MENU_SETTINGS 3
|
||||
|
||||
extern const int menu_slot[NUM_MENUS];
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────────────
|
||||
#define NUM_SETTINGS 8
|
||||
#define SET_BRIGHTNESS 0
|
||||
#define SET_START_LIFE 1
|
||||
#define SET_NUM_OPP 2
|
||||
#define SET_BLE 3
|
||||
#define SET_RESET 4
|
||||
#define SET_RESET_ALL 5
|
||||
#define SET_PLAYER_NAME 7
|
||||
#define SET_GAME_ID 6
|
||||
#define BRIGHTNESS_MIN 1
|
||||
#define BRIGHTNESS_MAX 100
|
||||
#define PLAYER_NAME_LEN 8
|
||||
#define GAME_ID_DIGITS 4
|
||||
|
||||
extern const char *setting_names[NUM_SETTINGS];
|
||||
extern const int start_life_opts[];
|
||||
#define NUM_LIFE_OPTS 3
|
||||
|
||||
extern const char NAME_CHARS[];
|
||||
#define NUM_NAME_CHARS 37
|
||||
#define NUM_HEX_CHARS 16
|
||||
|
||||
// ── Counters ──────────────────────────────────────────────────────────────────
|
||||
#define NUM_COUNTERS 4
|
||||
extern const char *counter_names[NUM_COUNTERS];
|
||||
|
||||
// ── Opponents ─────────────────────────────────────────────────────────────────
|
||||
#define MAX_OPPONENTS 4
|
||||
|
||||
// ── Font / display ────────────────────────────────────────────────────────────
|
||||
#define SCALE 5
|
||||
#define CHAR_WIDTH (5 * SCALE + SCALE)
|
||||
#define CHAR_PAGES 5
|
||||
#define START_PAGE (HEADER_PAGES + (OLED_PAGES - HEADER_PAGES - CHAR_PAGES + 1) / 2)
|
||||
#define SEG_W 16
|
||||
#define ICON_W 16
|
||||
#define FONT_BASE 0x20
|
||||
#define FONT_MAX 0x5A
|
||||
|
||||
// ── NVS ───────────────────────────────────────────────────────────────────────
|
||||
#define NVS_NS "settings"
|
||||
#define SAVE_DELAY 300
|
||||
@ -0,0 +1,294 @@
|
||||
#include "draw.h"
|
||||
#include "config.h"
|
||||
#include "game.h"
|
||||
#include "font.h"
|
||||
#include "state.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
const int menu_slot[NUM_MENUS] = {0, 1, 2, 7};
|
||||
const char *setting_names[NUM_SETTINGS] = {
|
||||
"LED BRIGHTNESS", "STARTING LIFE", "NUM OPPONENTS", "BLE",
|
||||
"RESET", "RESET ALL", "GAME ID", "PLAYER NAME",
|
||||
};
|
||||
|
||||
#define LEDC_TIMER_SEL LEDC_TIMER_0
|
||||
#define LEDC_MODE_ LEDC_LOW_SPEED_MODE
|
||||
#define LEDC_DUTY_RES_ LEDC_TIMER_8_BIT
|
||||
#define LEDC_FREQ_HZ_ 5000
|
||||
|
||||
// ── LED ───────────────────────────────────────────────────────────────────────
|
||||
void led_init(void)
|
||||
{
|
||||
ledc_timer_config_t timer = {
|
||||
.speed_mode = LEDC_MODE_,
|
||||
.timer_num = LEDC_TIMER_SEL,
|
||||
.duty_resolution = LEDC_DUTY_RES_,
|
||||
.freq_hz = LEDC_FREQ_HZ_,
|
||||
.clk_cfg = LEDC_AUTO_CLK,
|
||||
};
|
||||
ledc_timer_config(&timer);
|
||||
ledc_channel_config_t ch[] = {
|
||||
{.gpio_num=LED_R_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_0,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
||||
{.gpio_num=LED_G_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_1,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
||||
{.gpio_num=LED_B_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_2,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
||||
};
|
||||
for (int i = 0; i < 3; i++) ledc_channel_config(&ch[i]);
|
||||
}
|
||||
|
||||
void led_update_for_count(int count, uint8_t scale)
|
||||
{
|
||||
int c = count < 0 ? 0 : count > 80 ? 80 : count;
|
||||
uint8_t r, g, b;
|
||||
if (c <= 40) {
|
||||
int cc = c < 10 ? 10 : c;
|
||||
r = (uint8_t)((uint32_t)(40-cc)*scale*g_led_max/30/255);
|
||||
g = (uint8_t)((uint32_t)(cc-10)*scale*g_led_max/30/255);
|
||||
b = 0;
|
||||
} else {
|
||||
r = 0;
|
||||
g = (uint8_t)((uint32_t)(80-c)*g_led_max/40);
|
||||
b = (uint8_t)((uint32_t)(c-40)*g_led_max/40);
|
||||
}
|
||||
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_0, r); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_0);
|
||||
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_1, g); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_1);
|
||||
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_2, b); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_2);
|
||||
}
|
||||
|
||||
// ── OLED ──────────────────────────────────────────────────────────────────────
|
||||
static void oled_write(const uint8_t *buf, size_t len)
|
||||
{
|
||||
i2c_master_write_to_device(I2C_PORT, OLED_ADDR, buf, len, pdMS_TO_TICKS(100));
|
||||
}
|
||||
static void oled_cmd(uint8_t c) { uint8_t b[2]={0x00,c}; oled_write(b,2); }
|
||||
static void oled_write_page(int p, const uint8_t *row);
|
||||
|
||||
void oled_init(void)
|
||||
{
|
||||
static const uint8_t seq[] = {
|
||||
0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0x20,0x00,
|
||||
0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF,
|
||||
};
|
||||
for (int i = 0; i < (int)sizeof(seq); i++) oled_cmd(seq[i]);
|
||||
}
|
||||
|
||||
void oled_clear(void)
|
||||
{
|
||||
uint8_t row[OLED_WIDTH]; memset(row, 0, sizeof(row));
|
||||
for (int p = 0; p < OLED_PAGES; p++) oled_write_page(p, row);
|
||||
}
|
||||
|
||||
static void oled_write_page(int p, const uint8_t *row)
|
||||
{
|
||||
oled_cmd(0x21); oled_cmd(0); oled_cmd(127);
|
||||
oled_cmd(0x22); oled_cmd(p); oled_cmd(p);
|
||||
uint8_t buf[OLED_WIDTH+1]; buf[0]=0x40;
|
||||
memcpy(buf+1, row, OLED_WIDTH); oled_write(buf, sizeof(buf));
|
||||
}
|
||||
|
||||
void oled_draw_header(void)
|
||||
{
|
||||
uint8_t pages[HEADER_PAGES][OLED_WIDTH];
|
||||
memset(pages, 0, sizeof(pages));
|
||||
for (int seg = 0; seg < NUM_MENUS; seg++) {
|
||||
uint8_t bg = (seg == g_active_menu) ? 0x00 : 0xFF;
|
||||
int sx = menu_slot[seg] * SEG_W;
|
||||
const uint8_t (*icon)[ICON_W] = (seg == MENU_CMDR && g_ble_enabled) ? icon_net : icons[seg];
|
||||
for (int col = 0; col < ICON_W; col++) {
|
||||
pages[0][sx+col] = bg ^ icon[0][col];
|
||||
pages[1][sx+col] = bg ^ icon[1][col];
|
||||
}
|
||||
}
|
||||
for (int p = 0; p < HEADER_PAGES; p++) oled_write_page(p, pages[p]);
|
||||
}
|
||||
|
||||
void oled_draw_centered(const char *str)
|
||||
{
|
||||
int len = strlen(str);
|
||||
int start = (OLED_WIDTH - len * CHAR_WIDTH) / 2;
|
||||
if (start < 0) start = 0;
|
||||
for (int p = HEADER_PAGES; p < OLED_PAGES; p++) {
|
||||
uint8_t row[OLED_WIDTH]; memset(row, 0, sizeof(row));
|
||||
if (p >= START_PAGE && p < START_PAGE + CHAR_PAGES) {
|
||||
int char_page = p - START_PAGE;
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = str[i];
|
||||
if (c < FONT_BASE || c > FONT_MAX) c = ' ';
|
||||
const uint8_t *glyph = font5x8[(uint8_t)c - FONT_BASE];
|
||||
int col = start + i * CHAR_WIDTH;
|
||||
for (int sc = 0; sc < 5*SCALE; sc++) {
|
||||
int x = col + sc;
|
||||
if (x < 0 || x >= OLED_WIDTH) continue;
|
||||
uint8_t font_col = glyph[sc/SCALE];
|
||||
uint8_t pixel_byte = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
int src_row = (char_page*8+b)/SCALE;
|
||||
if (src_row < 8 && ((font_col >> src_row) & 1)) pixel_byte |= (1<<b);
|
||||
}
|
||||
row[x] = pixel_byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
oled_write_page(p, row);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pixel framebuffer helpers ─────────────────────────────────────────────────
|
||||
|
||||
static uint8_t row_mask(int p, int base, int h)
|
||||
{
|
||||
uint8_t mask = 0;
|
||||
for (int r = 0; r < 8; r++)
|
||||
if (8*p+r >= base && 8*p+r < base+h) mask |= (1<<r);
|
||||
return mask;
|
||||
}
|
||||
|
||||
static void pxbuf_fill(uint8_t pages[][OLED_WIDTH], int content_h, int base, int entry_h)
|
||||
{
|
||||
for (int p = base/8; p <= (base+entry_h-1)/8 && p < content_h/8; p++) {
|
||||
uint8_t mask = row_mask(p, base, entry_h);
|
||||
for (int x = 0; x < OLED_WIDTH; x++) pages[p][x] |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
static int pxbuf_str(uint8_t pages[][OLED_WIDTH], int content_h,
|
||||
int base, int x, const char *str, int inv)
|
||||
{
|
||||
for (; *str && x+5 <= OLED_WIDTH; str++) {
|
||||
uint8_t c = (uint8_t)*str;
|
||||
if (c < FONT_BASE || c > FONT_MAX) c = ' ';
|
||||
const uint8_t *glyph = font5x8[c - FONT_BASE];
|
||||
for (int sc = 0; sc < 5; sc++, x++) {
|
||||
for (int g = 0; g < 8; g++) {
|
||||
if ((glyph[sc] >> g) & 1) {
|
||||
int row = base + 1 + g;
|
||||
if (row < content_h) {
|
||||
if (inv) pages[row/8][x] &= ~(1 << (row%8));
|
||||
else pages[row/8][x] |= (1 << (row%8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
x++;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// ── Core list renderer ────────────────────────────────────────────────────────
|
||||
|
||||
void oled_draw_rows(const oled_row_t *rows, int count, int active)
|
||||
{
|
||||
const int content_h = (OLED_PAGES - HEADER_PAGES) * 8;
|
||||
const int entry_h = 9;
|
||||
const int max_full = content_h / entry_h;
|
||||
|
||||
int start = active - max_full / 2;
|
||||
if (start < 0) start = 0;
|
||||
if (start + max_full > count) start = count - max_full;
|
||||
if (start < 0) start = 0;
|
||||
|
||||
uint8_t pages[OLED_PAGES - HEADER_PAGES][OLED_WIDTH];
|
||||
memset(pages, 0, sizeof(pages));
|
||||
|
||||
for (int ei = 0; ei < max_full+1 && (start+ei) < count; ei++) {
|
||||
int entry = start + ei;
|
||||
int inv = (entry == active);
|
||||
int base = ei * entry_h;
|
||||
|
||||
if (inv) pxbuf_fill(pages, content_h, base, entry_h);
|
||||
|
||||
if (rows[entry].label && rows[entry].label[0])
|
||||
pxbuf_str(pages, content_h, base, 2, rows[entry].label, inv);
|
||||
|
||||
if (rows[entry].value[0]) {
|
||||
int vlen = (int)strlen(rows[entry].value);
|
||||
int rx = OLED_WIDTH - 2 - vlen*6;
|
||||
if (rx < 0) rx = 0;
|
||||
pxbuf_str(pages, content_h, base, rx, rows[entry].value, inv);
|
||||
|
||||
if (inv && rows[entry].cursor >= 0 && rows[entry].cursor < vlen) {
|
||||
int cursor_x = rx + rows[entry].cursor * 6;
|
||||
for (int p = base/8; p <= (base+entry_h-1)/8 && p < content_h/8; p++) {
|
||||
uint8_t mask = row_mask(p, base, entry_h);
|
||||
for (int j = 0; j < 6 && cursor_x+j < OLED_WIDTH; j++)
|
||||
pages[p][cursor_x+j] &= ~mask;
|
||||
}
|
||||
char one[2] = { rows[entry].value[rows[entry].cursor], 0 };
|
||||
pxbuf_str(pages, content_h, base, cursor_x, one, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int p = 0; p < OLED_PAGES - HEADER_PAGES; p++)
|
||||
oled_write_page(HEADER_PAGES + p, pages[p]);
|
||||
}
|
||||
|
||||
// ── Menu draw functions (thin wrappers that build rows and call oled_draw_rows) ──
|
||||
|
||||
void oled_draw_list(const char **labels, const int *values, int count, int active)
|
||||
{
|
||||
oled_row_t rows[8];
|
||||
int n = count < 8 ? count : 8;
|
||||
for (int i = 0; i < n; i++) {
|
||||
rows[i].label = labels[i];
|
||||
rows[i].cursor = -1;
|
||||
snprintf(rows[i].value, sizeof(rows[i].value), "%d", values[i]);
|
||||
}
|
||||
oled_draw_rows(rows, n, active);
|
||||
}
|
||||
|
||||
void oled_draw_players(void)
|
||||
{
|
||||
int total = MAX_BLE_PEERS + 1 + (!g_ble_enabled ? 1 : 0);
|
||||
oled_row_t rows[MAX_BLE_PEERS + 2];
|
||||
|
||||
rows[0].label = g_player_name;
|
||||
rows[0].cursor = -1;
|
||||
snprintf(rows[0].value, sizeof(rows[0].value),
|
||||
g_eliminated ? "ELIM" : "%d/%d", g_life, g_counters[0]);
|
||||
|
||||
for (int pi = 0; pi < MAX_BLE_PEERS; pi++) {
|
||||
rows[pi+1].cursor = -1;
|
||||
if (g_peers[pi].active) {
|
||||
rows[pi+1].label = g_peers[pi].name;
|
||||
snprintf(rows[pi+1].value, sizeof(rows[pi+1].value),
|
||||
g_peers[pi].eliminated ? "ELIM" : "%d/%d/%d",
|
||||
g_peers[pi].life, g_peers[pi].poison, g_cmdr_damage[pi]);
|
||||
} else {
|
||||
rows[pi+1].label = "";
|
||||
rows[pi+1].value[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_ble_enabled) {
|
||||
rows[MAX_BLE_PEERS+1].label = "BLE OFF";
|
||||
rows[MAX_BLE_PEERS+1].value[0] = 0;
|
||||
rows[MAX_BLE_PEERS+1].cursor = -1;
|
||||
}
|
||||
|
||||
oled_draw_rows(rows, total, g_active_player);
|
||||
}
|
||||
|
||||
void oled_draw_settings(void)
|
||||
{
|
||||
oled_row_t rows[NUM_SETTINGS];
|
||||
for (int i = 0; i < NUM_SETTINGS; i++) {
|
||||
rows[i].label = setting_names[i];
|
||||
rows[i].cursor = -1;
|
||||
}
|
||||
snprintf(rows[SET_BRIGHTNESS].value, sizeof(rows[0].value), "%d%%", g_brightness_pct);
|
||||
snprintf(rows[SET_START_LIFE].value, sizeof(rows[0].value), "%d", start_life_opts[g_start_life_index]);
|
||||
snprintf(rows[SET_NUM_OPP].value, sizeof(rows[0].value), "%d", g_num_opponents);
|
||||
memcpy(rows[SET_PLAYER_NAME].value, g_player_name, PLAYER_NAME_LEN);
|
||||
rows[SET_PLAYER_NAME].value[PLAYER_NAME_LEN] = 0;
|
||||
rows[SET_PLAYER_NAME].cursor = g_name_cursor;
|
||||
snprintf(rows[SET_BLE].value, sizeof(rows[0].value), "%s", g_ble_enabled ? "ON" : "OFF");
|
||||
snprintf(rows[SET_GAME_ID].value, sizeof(rows[0].value), "%02X%02X", g_game_id[0], g_game_id[1]);
|
||||
rows[SET_GAME_ID].cursor = g_game_id_cursor;
|
||||
snprintf(rows[SET_RESET].value, sizeof(rows[0].value), "---");
|
||||
snprintf(rows[SET_RESET_ALL].value, sizeof(rows[0].value), "---");
|
||||
oled_draw_rows(rows, NUM_SETTINGS, g_active_setting);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
const char *label; // left-aligned; NULL or empty = blank row
|
||||
char value[16]; // right-aligned formatted string; empty = omit
|
||||
int cursor; // index into value for edit cursor indicator; -1 = none
|
||||
} oled_row_t;
|
||||
|
||||
void led_init(void);
|
||||
void led_update_for_count(int count, uint8_t scale);
|
||||
void oled_init(void);
|
||||
void oled_clear(void);
|
||||
void oled_draw_header(void);
|
||||
void oled_draw_centered(const char *str);
|
||||
void oled_draw_rows(const oled_row_t *rows, int count, int active);
|
||||
void oled_draw_list(const char **labels, const int *values, int count, int active);
|
||||
void oled_draw_players(void);
|
||||
void oled_draw_settings(void);
|
||||
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "config.h"
|
||||
|
||||
static const uint8_t font5x8[][5] = {
|
||||
{0x00,0x00,0x00,0x00,0x00}, // ' '
|
||||
{0x00,0x00,0x5F,0x00,0x00}, // '!'
|
||||
{0x00,0x07,0x00,0x07,0x00}, // '"'
|
||||
{0x14,0x7F,0x14,0x7F,0x14}, // '#'
|
||||
{0x24,0x2A,0x7F,0x2A,0x12}, // '$'
|
||||
{0x23,0x13,0x08,0x64,0x62}, // '%'
|
||||
{0x36,0x49,0x55,0x22,0x50}, // '&'
|
||||
{0x00,0x05,0x03,0x00,0x00}, // '\''
|
||||
{0x00,0x1C,0x22,0x41,0x00}, // '('
|
||||
{0x00,0x41,0x22,0x1C,0x00}, // ')'
|
||||
{0x14,0x08,0x3E,0x08,0x14}, // '*'
|
||||
{0x08,0x08,0x3E,0x08,0x08}, // '+'
|
||||
{0x00,0x50,0x30,0x00,0x00}, // ','
|
||||
{0x08,0x08,0x08,0x08,0x08}, // '-'
|
||||
{0x00,0x60,0x60,0x00,0x00}, // '.'
|
||||
{0x20,0x10,0x08,0x04,0x02}, // '/'
|
||||
{0x3E,0x51,0x49,0x45,0x3E}, // '0'
|
||||
{0x00,0x42,0x7F,0x40,0x00}, // '1'
|
||||
{0x72,0x49,0x49,0x49,0x46}, // '2'
|
||||
{0x21,0x41,0x49,0x4D,0x33}, // '3'
|
||||
{0x18,0x14,0x12,0x7F,0x10}, // '4'
|
||||
{0x27,0x45,0x45,0x45,0x39}, // '5'
|
||||
{0x3C,0x4A,0x49,0x49,0x31}, // '6'
|
||||
{0x41,0x21,0x11,0x09,0x07}, // '7'
|
||||
{0x36,0x49,0x49,0x49,0x36}, // '8'
|
||||
{0x46,0x49,0x49,0x29,0x1E}, // '9'
|
||||
{0x00,0x36,0x36,0x00,0x00}, // ':'
|
||||
{0x00,0x56,0x36,0x00,0x00}, // ';'
|
||||
{0x08,0x14,0x22,0x41,0x00}, // '<'
|
||||
{0x14,0x14,0x14,0x14,0x14}, // '='
|
||||
{0x00,0x41,0x22,0x14,0x08}, // '>'
|
||||
{0x02,0x01,0x51,0x09,0x06}, // '?'
|
||||
{0x32,0x49,0x79,0x41,0x3E}, // '@'
|
||||
{0x7E,0x11,0x11,0x11,0x7E}, // 'A'
|
||||
{0x7F,0x49,0x49,0x49,0x36}, // 'B'
|
||||
{0x3E,0x41,0x41,0x41,0x22}, // 'C'
|
||||
{0x7F,0x41,0x41,0x22,0x1C}, // 'D'
|
||||
{0x7F,0x49,0x49,0x49,0x41}, // 'E'
|
||||
{0x7F,0x09,0x09,0x09,0x01}, // 'F'
|
||||
{0x3E,0x41,0x49,0x49,0x7A}, // 'G'
|
||||
{0x7F,0x08,0x08,0x08,0x7F}, // 'H'
|
||||
{0x00,0x41,0x7F,0x41,0x00}, // 'I'
|
||||
{0x20,0x40,0x41,0x3F,0x01}, // 'J'
|
||||
{0x7F,0x08,0x14,0x22,0x41}, // 'K'
|
||||
{0x7F,0x40,0x40,0x40,0x40}, // 'L'
|
||||
{0x7F,0x02,0x04,0x02,0x7F}, // 'M'
|
||||
{0x7F,0x04,0x08,0x10,0x7F}, // 'N'
|
||||
{0x3E,0x41,0x41,0x41,0x3E}, // 'O'
|
||||
{0x7F,0x09,0x09,0x09,0x06}, // 'P'
|
||||
{0x3E,0x41,0x51,0x21,0x5E}, // 'Q'
|
||||
{0x7F,0x09,0x19,0x29,0x46}, // 'R'
|
||||
{0x46,0x49,0x49,0x49,0x31}, // 'S'
|
||||
{0x01,0x01,0x7F,0x01,0x01}, // 'T'
|
||||
{0x3F,0x40,0x40,0x40,0x3F}, // 'U'
|
||||
{0x1F,0x20,0x40,0x20,0x1F}, // 'V'
|
||||
{0x3F,0x40,0x38,0x40,0x3F}, // 'W'
|
||||
{0x63,0x14,0x08,0x14,0x63}, // 'X'
|
||||
{0x07,0x08,0x70,0x08,0x07}, // 'Y'
|
||||
{0x61,0x51,0x49,0x45,0x43}, // 'Z'
|
||||
};
|
||||
|
||||
static const uint8_t icons[NUM_MENUS][2][ICON_W] = {
|
||||
// Heart (life)
|
||||
{ {0x00,0x70,0xF8,0xFC,0xFC,0xFC,0xF8,0xF0,0xF0,0xF8,0xFC,0xFC,0xFC,0xF8,0x70,0x00},
|
||||
{0x00,0x00,0x00,0x01,0x03,0x07,0x0F,0x3F,0x3F,0x0F,0x07,0x03,0x01,0x00,0x00,0x00} },
|
||||
// Shield (commander damage)
|
||||
{ {0x00,0xFC,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFC,0x00,0x00},
|
||||
{0x00,0x07,0x0F,0x1F,0x3F,0x3F,0x3F,0x7F,0x7F,0x3F,0x3F,0x1F,0x0F,0x07,0x00,0x00} },
|
||||
// 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} },
|
||||
// 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} },
|
||||
};
|
||||
|
||||
// Signal bars shown on MENU_CMDR slot when BLE is enabled
|
||||
static const uint8_t icon_net[2][ICON_W] = {
|
||||
{0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x00,0xF8,0xF8,0x00,0xFE,0xFE,0x00,0x00},
|
||||
{0x00,0x00,0x00,0x38,0x38,0x00,0x3F,0x3F,0x00,0x3F,0x3F,0x00,0x3F,0x3F,0x00,0x00},
|
||||
};
|
||||
@ -0,0 +1,97 @@
|
||||
#include "game.h"
|
||||
#include "config.h"
|
||||
#include "state.h"
|
||||
#include "ble.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_err.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
const int start_life_opts[] = {20, 30, 40};
|
||||
const char NAME_CHARS[] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const char *counter_names[NUM_COUNTERS] = {"POISON", "EXPERIENCE", "ENERGY", "LORE"};
|
||||
|
||||
void settings_reset_defaults(void)
|
||||
{
|
||||
g_brightness_pct = 10;
|
||||
g_start_life_index = 2;
|
||||
g_num_opponents = 3;
|
||||
strncpy(g_player_name, "PLAYER 1", PLAYER_NAME_LEN);
|
||||
g_player_name[PLAYER_NAME_LEN] = '\0';
|
||||
g_ble_enabled = 0;
|
||||
g_led_max = 26;
|
||||
g_game_id[0] = BLE_GAME_ID_0;
|
||||
g_game_id[1] = BLE_GAME_ID_1;
|
||||
}
|
||||
|
||||
void game_reset(void)
|
||||
{
|
||||
g_life = start_life_opts[g_start_life_index];
|
||||
g_eliminated = 0;
|
||||
memset(g_cmdr_damage, 0, sizeof(g_cmdr_damage));
|
||||
memset(g_counters, 0, sizeof(g_counters));
|
||||
}
|
||||
|
||||
void check_elimination(void)
|
||||
{
|
||||
if (g_eliminated) return;
|
||||
if (g_life <= 0 || g_counters[0] >= 10) { g_eliminated = 1; return; }
|
||||
for (int i = 0; i < g_num_opponents; i++)
|
||||
if (g_cmdr_damage[i] >= 21) { g_eliminated = 1; return; }
|
||||
}
|
||||
|
||||
void settings_load(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &nvs) != ESP_OK) return;
|
||||
int32_t val;
|
||||
if (nvs_get_i32(nvs, "brightness", &val) == ESP_OK) g_brightness_pct = (int)val;
|
||||
if (nvs_get_i32(nvs, "start_life", &val) == ESP_OK) g_start_life_index = (int)val;
|
||||
if (nvs_get_i32(nvs, "num_opp", &val) == ESP_OK) g_num_opponents = (int)val;
|
||||
if (nvs_get_i32(nvs, "ble_en", &val) == ESP_OK) g_ble_enabled = (int)val;
|
||||
if (nvs_get_i32(nvs, "game_id", &val) == ESP_OK) {
|
||||
g_game_id[0] = (uint8_t)((val >> 8) & 0xFF);
|
||||
g_game_id[1] = (uint8_t)(val & 0xFF);
|
||||
}
|
||||
size_t len = sizeof(g_player_name);
|
||||
nvs_get_str(nvs, "pname", g_player_name, &len);
|
||||
if (nvs_get_i32(nvs, "player_id", &val) == ESP_OK) g_player_id = (uint8_t)val;
|
||||
if (nvs_get_i32(nvs, "life", &val) == ESP_OK) g_life = (int)val;
|
||||
if (nvs_get_i32(nvs, "elim", &val) == ESP_OK) g_eliminated = (int)val;
|
||||
char key[8];
|
||||
for (int i = 0; i < MAX_OPPONENTS; i++) {
|
||||
snprintf(key, sizeof(key), "cmdr%d", i);
|
||||
if (nvs_get_i32(nvs, key, &val) == ESP_OK) g_cmdr_damage[i] = (int)val;
|
||||
}
|
||||
for (int i = 0; i < NUM_COUNTERS; i++) {
|
||||
snprintf(key, sizeof(key), "cnt%d", i);
|
||||
if (nvs_get_i32(nvs, key, &val) == ESP_OK) g_counters[i] = (int)val;
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
|
||||
void settings_save(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &nvs) != ESP_OK) return;
|
||||
nvs_set_i32(nvs, "brightness", (int32_t)g_brightness_pct);
|
||||
nvs_set_i32(nvs, "start_life", (int32_t)g_start_life_index);
|
||||
nvs_set_i32(nvs, "num_opp", (int32_t)g_num_opponents);
|
||||
nvs_set_i32(nvs, "ble_en", (int32_t)g_ble_enabled);
|
||||
nvs_set_i32(nvs, "game_id", (int32_t)((g_game_id[0] << 8) | g_game_id[1]));
|
||||
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, "elim", (int32_t)g_eliminated);
|
||||
char key[8];
|
||||
for (int i = 0; i < MAX_OPPONENTS; i++) {
|
||||
snprintf(key, sizeof(key), "cmdr%d", i);
|
||||
nvs_set_i32(nvs, key, (int32_t)g_cmdr_damage[i]);
|
||||
}
|
||||
for (int i = 0; i < NUM_COUNTERS; i++) {
|
||||
snprintf(key, sizeof(key), "cnt%d", i);
|
||||
nvs_set_i32(nvs, key, (int32_t)g_counters[i]);
|
||||
}
|
||||
nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "config.h"
|
||||
|
||||
extern const int start_life_opts[];
|
||||
extern const char NAME_CHARS[];
|
||||
extern const char *counter_names[NUM_COUNTERS];
|
||||
|
||||
void settings_reset_defaults(void);
|
||||
void game_reset(void);
|
||||
void check_elimination(void);
|
||||
void settings_load(void);
|
||||
void settings_save(void);
|
||||
@ -0,0 +1,464 @@
|
||||
#include "config.h"
|
||||
#include "state.h"
|
||||
#include "game.h"
|
||||
#include "draw.h"
|
||||
#include "ble.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_vfs_dev.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
// ── Global state ──────────────────────────────────────────────────────────────
|
||||
// Game
|
||||
int g_life;
|
||||
int g_cmdr_damage[MAX_OPPONENTS];
|
||||
int g_counters[NUM_COUNTERS];
|
||||
int g_eliminated;
|
||||
|
||||
// UI
|
||||
int g_active_menu;
|
||||
int g_active_opponent;
|
||||
int g_active_counter;
|
||||
int g_active_player;
|
||||
int g_active_setting;
|
||||
int g_name_cursor;
|
||||
|
||||
// Settings
|
||||
int g_brightness_pct = 10;
|
||||
int g_start_life_index = 2; // 40
|
||||
int g_num_opponents = 3;
|
||||
char g_player_name[PLAYER_NAME_LEN + 1] = "PLAYER 1";
|
||||
int g_ble_enabled = 0;
|
||||
uint8_t g_led_max = 26;
|
||||
int g_game_id_cursor = 0;
|
||||
uint8_t g_game_id[2] = {BLE_GAME_ID_0, BLE_GAME_ID_1};
|
||||
|
||||
// Peers
|
||||
ble_peer_t g_peers[MAX_BLE_PEERS];
|
||||
|
||||
// Timing
|
||||
uint32_t g_tick;
|
||||
|
||||
// ── Serial command task ───────────────────────────────────────────────────────
|
||||
static void serial_print_state(void)
|
||||
{
|
||||
printf("DBG STATE life=%d poison=%u cmdr=[%d,%d,%d,%d] counters=[%d,%d,%d] menu=%d ble=%d pid=%u eliminated=%d\n",
|
||||
g_life, (unsigned)g_counters[0],
|
||||
g_cmdr_damage[0], g_cmdr_damage[1], g_cmdr_damage[2], g_cmdr_damage[3],
|
||||
g_counters[0], g_counters[1], g_counters[2],
|
||||
g_active_menu, g_ble_enabled, (unsigned)g_player_id, g_eliminated);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void serial_cmd_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char buf[64];
|
||||
int pos = 0;
|
||||
for (;;) {
|
||||
int c = fgetc(stdin);
|
||||
if (c < 0) { vTaskDelay(pdMS_TO_TICKS(10)); continue; }
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (pos > 0) {
|
||||
buf[pos] = '\0';
|
||||
pos = 0;
|
||||
int val = 0;
|
||||
if (strncmp(buf, "SET ", 4) == 0) {
|
||||
const char *kv = buf + 4;
|
||||
if (sscanf(kv, "life=%d", &val) == 1) {
|
||||
g_life = val; check_elimination();
|
||||
}
|
||||
if (sscanf(kv, "ble=%d", &val) == 1) {
|
||||
g_ble_enabled = val ? 1 : 0;
|
||||
}
|
||||
{
|
||||
char fmt[14];
|
||||
for (int i = 0; i < MAX_OPPONENTS; i++) {
|
||||
snprintf(fmt, sizeof(fmt), "cmdr%d=%%d", i);
|
||||
if (sscanf(kv, fmt, &val) == 1) {
|
||||
g_cmdr_damage[i] = val < 0 ? 0 : val; check_elimination(); break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < NUM_COUNTERS; i++) {
|
||||
snprintf(fmt, sizeof(fmt), "counter%d=%%d", i);
|
||||
if (sscanf(kv, fmt, &val) == 1) {
|
||||
g_counters[i] = val < 0 ? 0 : val; check_elimination(); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ble_adv_update();
|
||||
serial_print_state();
|
||||
} else if (strcmp(buf, "RESET") == 0) {
|
||||
game_reset();
|
||||
ble_adv_update();
|
||||
serial_print_state();
|
||||
} else if (strcmp(buf, "CLEARNVS") == 0) {
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &nvs) == ESP_OK) {
|
||||
nvs_erase_all(nvs);
|
||||
nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
}
|
||||
settings_reset_defaults();
|
||||
game_reset();
|
||||
ble_adv_update();
|
||||
serial_print_state();
|
||||
} else if (strcmp(buf, "STATE") == 0) {
|
||||
serial_print_state();
|
||||
}
|
||||
}
|
||||
} else if (pos < (int)sizeof(buf) - 1) {
|
||||
buf[pos++] = (char)c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── app_main ──────────────────────────────────────────────────────────────────
|
||||
void app_main(void)
|
||||
{
|
||||
led_init();
|
||||
|
||||
i2c_config_t i2c_cfg = {
|
||||
.mode=I2C_MODE_MASTER, .sda_io_num=I2C_SDA_GPIO, .scl_io_num=I2C_SCL_GPIO,
|
||||
.sda_pullup_en=GPIO_PULLUP_ENABLE, .scl_pullup_en=GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed=I2C_FREQ_HZ,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &i2c_cfg));
|
||||
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
|
||||
|
||||
esp_err_t nvs_err = nvs_flash_init();
|
||||
if (nvs_err == ESP_ERR_NVS_NO_FREE_PAGES || nvs_err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
nvs_flash_erase(); nvs_flash_init();
|
||||
}
|
||||
g_life = start_life_opts[g_start_life_index];
|
||||
settings_load();
|
||||
g_led_max = (uint8_t)(255 * g_brightness_pct / 100);
|
||||
|
||||
oled_init();
|
||||
oled_clear();
|
||||
|
||||
gpio_config_t gpio_cfg = {
|
||||
.pin_bit_mask = (1ULL<<BTN_RIGHT_GPIO)|(1ULL<<BTN_LEFT_GPIO)|(1ULL<<BTN_MID_GPIO),
|
||||
.mode=GPIO_MODE_INPUT, .pull_up_en=GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en=GPIO_PULLDOWN_DISABLE, .intr_type=GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&gpio_cfg);
|
||||
|
||||
ble_init();
|
||||
|
||||
uart_driver_install(UART_NUM_0, 256, 0, 0, NULL, 0);
|
||||
esp_vfs_dev_uart_use_driver(0);
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
xTaskCreate(serial_cmd_task, "serial_cmd", 4096, NULL, 3, NULL);
|
||||
|
||||
oled_draw_header();
|
||||
{ char ibuf[12]; snprintf(ibuf, sizeof(ibuf), "%d", g_life); oled_draw_centered(ibuf); }
|
||||
led_update_for_count(g_life, 255);
|
||||
|
||||
#ifdef DEBUG
|
||||
serial_print_state();
|
||||
#endif
|
||||
|
||||
int prev_right=1, prev_left=1, prev_mid=1;
|
||||
int hold_right=0, hold_left=0, hold_mid=0, mid_fired_hold=0;
|
||||
// Require 2 consecutive released ticks before arming hold; prevents accidental
|
||||
// hold fires when rapid taps have sub-10ms releases that the sampler misses.
|
||||
int release_r=2, release_l=2;
|
||||
int hold_armed_r=1, hold_armed_l=1;
|
||||
int players_tick=0;
|
||||
int settings_dirty=0, settings_save_tick=0;
|
||||
#define MARK_DIRTY() do { settings_dirty = 1; settings_save_tick = 0; } while (0)
|
||||
float breath_phase=0.0f;
|
||||
char nbuf[12];
|
||||
|
||||
char opponent_labels[MAX_OPPONENTS][PLAYER_NAME_LEN+1];
|
||||
const char *opponent_label_ptrs[MAX_OPPONENTS];
|
||||
|
||||
while (1) {
|
||||
g_tick++;
|
||||
int right = gpio_get_level(BTN_RIGHT_GPIO);
|
||||
int left = gpio_get_level(BTN_LEFT_GPIO);
|
||||
int mid = gpio_get_level(BTN_MID_GPIO);
|
||||
int changed = 0;
|
||||
|
||||
// ── Middle button ────────────────────────────────────────────────────
|
||||
if (mid == 0) {
|
||||
hold_mid++;
|
||||
if (!mid_fired_hold && hold_mid == MID_HOLD_SETTINGS) {
|
||||
if (g_active_menu == MENU_SETTINGS) {
|
||||
g_active_menu = 0;
|
||||
} else {
|
||||
g_active_menu = MENU_SETTINGS;
|
||||
g_active_setting = 0;
|
||||
g_name_cursor = 0;
|
||||
g_game_id_cursor = 0;
|
||||
}
|
||||
mid_fired_hold = 1;
|
||||
hold_right = hold_left = players_tick = 0;
|
||||
oled_draw_header();
|
||||
changed = 1;
|
||||
}
|
||||
} else {
|
||||
if (prev_mid == 0 && !mid_fired_hold) {
|
||||
if (hold_mid >= MID_HOLD_MENU) {
|
||||
if (g_active_menu == MENU_SETTINGS) {
|
||||
g_active_menu = 0;
|
||||
} else {
|
||||
g_active_menu = (g_active_menu + 1) % (NUM_MENUS - 1);
|
||||
}
|
||||
hold_right = hold_left = players_tick = 0;
|
||||
oled_draw_header();
|
||||
changed = 1;
|
||||
} else if (g_active_menu == MENU_SETTINGS) {
|
||||
if (g_active_setting == SET_PLAYER_NAME) {
|
||||
g_name_cursor++;
|
||||
if (g_name_cursor >= PLAYER_NAME_LEN) {
|
||||
g_name_cursor = 0;
|
||||
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
||||
g_game_id_cursor = 0;
|
||||
}
|
||||
} else if (g_active_setting == SET_GAME_ID) {
|
||||
g_game_id_cursor++;
|
||||
if (g_game_id_cursor >= GAME_ID_DIGITS) {
|
||||
g_game_id_cursor = 0;
|
||||
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
||||
}
|
||||
} else {
|
||||
g_active_setting = (g_active_setting + 1) % NUM_SETTINGS;
|
||||
g_name_cursor = 0;
|
||||
g_game_id_cursor = 0;
|
||||
}
|
||||
changed = 1;
|
||||
} else if (g_active_menu == MENU_CMDR) {
|
||||
if (g_ble_enabled) {
|
||||
int next = (g_active_player + 1) % (MAX_BLE_PEERS + 1);
|
||||
while (next != g_active_player && next != 0 && !g_peers[next - 1].active)
|
||||
next = (next + 1) % (MAX_BLE_PEERS + 1);
|
||||
g_active_player = next;
|
||||
} else {
|
||||
g_active_opponent = (g_active_opponent + 1) % g_num_opponents;
|
||||
}
|
||||
changed = 1;
|
||||
} else if (g_active_menu == MENU_COUNTERS) {
|
||||
g_active_counter = (g_active_counter + 1) % NUM_COUNTERS;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
hold_mid = 0; mid_fired_hold = 0;
|
||||
}
|
||||
|
||||
// ── Left / Right ─────────────────────────────────────────────────────
|
||||
int delta_r=0, delta_l=0;
|
||||
if (right==0) {
|
||||
if (prev_right==1) {
|
||||
delta_r=1; hold_right=0;
|
||||
hold_armed_r = (release_r >= 2);
|
||||
release_r = 0;
|
||||
} else if (hold_armed_r && ++hold_right>=HOLD_DELAY && (hold_right-HOLD_DELAY)%HOLD_REPEAT==0) {
|
||||
delta_r=1;
|
||||
}
|
||||
} else { hold_right=0; if (release_r < 2) release_r++; }
|
||||
if (left==0) {
|
||||
if (prev_left==1) {
|
||||
delta_l=1; hold_left=0;
|
||||
hold_armed_l = (release_l >= 2);
|
||||
release_l = 0;
|
||||
} else if (hold_armed_l && ++hold_left>=HOLD_DELAY && (hold_left-HOLD_DELAY)%HOLD_REPEAT==0) {
|
||||
delta_l=1;
|
||||
}
|
||||
} else { hold_left=0; if (release_l < 2) release_l++; }
|
||||
|
||||
if (delta_r || delta_l) {
|
||||
int d = delta_r - delta_l;
|
||||
if (g_active_menu == MENU_LIFE) {
|
||||
g_life += d; changed = 1; MARK_DIRTY(); check_elimination();
|
||||
} else if (g_active_menu == MENU_CMDR) {
|
||||
if (g_ble_enabled) {
|
||||
if (g_active_player == 0) {
|
||||
g_life += d; check_elimination();
|
||||
} else {
|
||||
int slot = g_active_player - 1;
|
||||
g_cmdr_damage[slot] += d;
|
||||
if (g_cmdr_damage[slot] < 0) g_cmdr_damage[slot] = 0;
|
||||
check_elimination();
|
||||
}
|
||||
} else {
|
||||
g_cmdr_damage[g_active_opponent] += d;
|
||||
if (g_cmdr_damage[g_active_opponent] < 0) g_cmdr_damage[g_active_opponent] = 0;
|
||||
check_elimination();
|
||||
}
|
||||
changed = 1; MARK_DIRTY();
|
||||
} else if (g_active_menu == MENU_COUNTERS) {
|
||||
g_counters[g_active_counter] += d;
|
||||
if (g_counters[g_active_counter] < 0) g_counters[g_active_counter] = 0;
|
||||
changed = 1; MARK_DIRTY(); check_elimination();
|
||||
} else if (g_active_menu == MENU_SETTINGS) {
|
||||
changed = 1; MARK_DIRTY();
|
||||
switch (g_active_setting) {
|
||||
case SET_BRIGHTNESS:
|
||||
g_brightness_pct += d;
|
||||
if (g_brightness_pct < BRIGHTNESS_MIN) g_brightness_pct = BRIGHTNESS_MIN;
|
||||
if (g_brightness_pct > BRIGHTNESS_MAX) g_brightness_pct = BRIGHTNESS_MAX;
|
||||
g_led_max = (uint8_t)(255 * g_brightness_pct / 100);
|
||||
break;
|
||||
case SET_START_LIFE:
|
||||
g_start_life_index += d;
|
||||
if (g_start_life_index < 0) g_start_life_index = 0;
|
||||
if (g_start_life_index >= NUM_LIFE_OPTS) g_start_life_index = NUM_LIFE_OPTS-1;
|
||||
break;
|
||||
case SET_NUM_OPP:
|
||||
g_num_opponents += d;
|
||||
if (g_num_opponents < 1) g_num_opponents = 1;
|
||||
if (g_num_opponents > MAX_OPPONENTS) g_num_opponents = MAX_OPPONENTS;
|
||||
if (g_active_opponent >= g_num_opponents) g_active_opponent = 0;
|
||||
break;
|
||||
case SET_PLAYER_NAME: {
|
||||
char curr = g_player_name[g_name_cursor];
|
||||
const char *pos = strchr(NAME_CHARS, curr);
|
||||
int idx = pos ? (int)(pos - NAME_CHARS) : 0;
|
||||
idx = (idx + d + NUM_NAME_CHARS) % NUM_NAME_CHARS;
|
||||
g_player_name[g_name_cursor] = NAME_CHARS[idx];
|
||||
break;
|
||||
}
|
||||
case SET_BLE:
|
||||
g_ble_enabled += d;
|
||||
if (g_ble_enabled < 0) g_ble_enabled = 0;
|
||||
if (g_ble_enabled > 1) g_ble_enabled = 1;
|
||||
ble_adv_update();
|
||||
oled_draw_header();
|
||||
break;
|
||||
case SET_GAME_ID: {
|
||||
int byte = g_game_id_cursor / 2;
|
||||
int nibble = g_game_id_cursor % 2;
|
||||
int val = nibble == 0 ? (g_game_id[byte] >> 4) & 0xF : g_game_id[byte] & 0xF;
|
||||
val = (val + d + NUM_HEX_CHARS) % NUM_HEX_CHARS;
|
||||
if (nibble == 0)
|
||||
g_game_id[byte] = (g_game_id[byte] & 0x0F) | ((uint8_t)val << 4);
|
||||
else
|
||||
g_game_id[byte] = (g_game_id[byte] & 0xF0) | (uint8_t)val;
|
||||
ble_adv_update();
|
||||
break;
|
||||
}
|
||||
case SET_RESET:
|
||||
game_reset();
|
||||
ble_adv_update();
|
||||
break;
|
||||
case SET_RESET_ALL:
|
||||
game_reset();
|
||||
g_reset_cmd_ticks = 1500;
|
||||
ble_adv_update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remote reset command ──────────────────────────────────────────────
|
||||
if (g_reset_requested) {
|
||||
g_reset_requested = 0;
|
||||
game_reset();
|
||||
MARK_DIRTY();
|
||||
changed = 1;
|
||||
ble_adv_update();
|
||||
}
|
||||
|
||||
// ── reset_cmd broadcast countdown ────────────────────────────────────
|
||||
if (g_reset_cmd_ticks > 0 && --g_reset_cmd_ticks == 0) ble_adv_update();
|
||||
|
||||
// ── Peer expiry + player ID resolution ───────────────────────────────
|
||||
if (g_tick % 100 == 0) {
|
||||
for (int i = 0; i < MAX_BLE_PEERS; i++) {
|
||||
if (g_peers[i].active && g_tick - g_peers[i].last_seen > BLE_PEER_TIMEOUT) {
|
||||
#ifdef DEBUG
|
||||
printf("DBG PEER_EXPIRE slot=%d name=%-8.8s\n", i, g_peers[i].name);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
g_peers[i].active = 0;
|
||||
}
|
||||
}
|
||||
player_id_resolve();
|
||||
}
|
||||
|
||||
// ── Autosave settings ─────────────────────────────────────────────────
|
||||
if (settings_dirty && ++settings_save_tick >= SAVE_DELAY) {
|
||||
settings_save(); settings_dirty = 0; settings_save_tick = 0;
|
||||
}
|
||||
|
||||
// ── BLE adv update on game state change ───────────────────────────────
|
||||
if (changed && (g_active_menu==MENU_LIFE || g_active_menu==MENU_COUNTERS ||
|
||||
g_active_menu==MENU_CMDR)) {
|
||||
ble_adv_update();
|
||||
}
|
||||
|
||||
// ── Players periodic refresh ──────────────────────────────────────────
|
||||
if (g_active_menu == MENU_CMDR && g_ble_enabled) {
|
||||
if (++players_tick >= 100) {
|
||||
players_tick = 0; oled_draw_players();
|
||||
}
|
||||
} else { players_tick = 0; }
|
||||
|
||||
// ── LED ───────────────────────────────────────────────────────────────
|
||||
if (g_life == 0) {
|
||||
led_update_for_count(0, 255);
|
||||
} else if (g_life < 10) {
|
||||
float speed = 1.0f + (10.0f - g_life) / 9.0f * 2.0f;
|
||||
breath_phase += 2.0f * 3.14159265f * speed / 200.0f;
|
||||
uint8_t sc = (uint8_t)(127.5f + 127.5f * sinf(breath_phase));
|
||||
led_update_for_count(g_life, sc);
|
||||
} else if (changed) {
|
||||
led_update_for_count(g_life, 255);
|
||||
}
|
||||
|
||||
// ── Display dispatch ──────────────────────────────────────────────────
|
||||
if (changed) {
|
||||
switch (g_active_menu) {
|
||||
case MENU_LIFE:
|
||||
snprintf(nbuf, sizeof(nbuf), "%d", g_life);
|
||||
oled_draw_centered(nbuf);
|
||||
break;
|
||||
case MENU_CMDR:
|
||||
for (int i = 0; i < g_num_opponents; i++) {
|
||||
int found = 0;
|
||||
if (g_ble_enabled) {
|
||||
for (int j = 0; j < MAX_BLE_PEERS; j++) {
|
||||
if (g_peers[j].active && g_peers[j].player_id == (uint8_t)i) {
|
||||
snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "%.4s", g_peers[j].name);
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
snprintf(opponent_labels[i], sizeof(opponent_labels[i]), "CMD %d", i+1);
|
||||
opponent_label_ptrs[i] = opponent_labels[i];
|
||||
}
|
||||
if (!g_ble_enabled)
|
||||
oled_draw_list(opponent_label_ptrs, g_cmdr_damage, g_num_opponents, g_active_opponent);
|
||||
else
|
||||
oled_draw_players();
|
||||
break;
|
||||
case MENU_COUNTERS:
|
||||
oled_draw_list(counter_names, g_counters, NUM_COUNTERS, g_active_counter);
|
||||
break;
|
||||
case MENU_SETTINGS:
|
||||
oled_draw_settings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
if (changed || g_tick % 1000 == 0)
|
||||
serial_print_state();
|
||||
#endif
|
||||
|
||||
prev_right = right; prev_left = left; prev_mid = mid;
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "config.h"
|
||||
#include "ble.h"
|
||||
|
||||
// Game
|
||||
extern int g_life;
|
||||
extern int g_cmdr_damage[MAX_OPPONENTS];
|
||||
extern int g_counters[NUM_COUNTERS];
|
||||
extern int g_eliminated;
|
||||
|
||||
// UI
|
||||
extern int g_active_menu;
|
||||
extern int g_active_opponent;
|
||||
extern int g_active_counter;
|
||||
extern int g_active_player;
|
||||
extern int g_active_setting;
|
||||
extern int g_name_cursor;
|
||||
|
||||
// Settings
|
||||
extern int g_brightness_pct;
|
||||
extern int g_start_life_index;
|
||||
extern int g_num_opponents;
|
||||
extern char g_player_name[PLAYER_NAME_LEN + 1];
|
||||
extern int g_ble_enabled;
|
||||
extern uint8_t g_led_max;
|
||||
extern int g_game_id_cursor;
|
||||
extern uint8_t g_game_id[2];
|
||||
|
||||
// Peers
|
||||
extern ble_peer_t g_peers[MAX_BLE_PEERS];
|
||||
|
||||
// Timing
|
||||
extern uint32_t g_tick;
|
||||
@ -0,0 +1,2 @@
|
||||
(kicad_pcb (version 20260206) (generator "pcbnew") (generator_version "10.0")
|
||||
)
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -0,0 +1,14 @@
|
||||
(kicad_sch
|
||||
(version 20260306)
|
||||
(generator "eeschema")
|
||||
(generator_version "10.0")
|
||||
(uuid 4f1ddaab-5ed8-4280-ae9f-5da3efc5b3b5)
|
||||
(paper "A4")
|
||||
(lib_symbols)
|
||||
(sheet_instances
|
||||
(path "/"
|
||||
(page "1")
|
||||
)
|
||||
)
|
||||
(embedded_fonts no)
|
||||
)
|
||||
@ -0,0 +1,3 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
@ -0,0 +1,33 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(simulator C)
|
||||
|
||||
# Needed to find GLib
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GLIB2 REQUIRED glib-2.0 gio-2.0)
|
||||
|
||||
include_directories(${GLIB2_INCLUDE_DIRS})
|
||||
link_directories(${GLIB2_LIBRARY_DIRS})
|
||||
add_definitions(${GLIB2_CFLAGS_OTHER})
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
bluez_inc
|
||||
GIT_REPOSITORY https://github.com/weliem/bluez_inc.git
|
||||
GIT_TAG main
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(bluez_inc)
|
||||
|
||||
add_executable(simulator main.c)
|
||||
|
||||
target_link_libraries(simulator PRIVATE Binc ${GLIB2_LIBRARIES})
|
||||
|
||||
# Integration test binary — forks the simulator binary, needs only pthreads
|
||||
add_executable(test_ble test_ble.c)
|
||||
target_link_libraries(test_ble PRIVATE pthread)
|
||||
add_dependencies(test_ble simulator)
|
||||
target_compile_definitions(test_ble PRIVATE
|
||||
SIM_BIN="$<TARGET_FILE:simulator>"
|
||||
)
|
||||
@ -0,0 +1,283 @@
|
||||
#include <glib.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gio/gio.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <getopt.h>
|
||||
#include "adapter.h"
|
||||
#include "advertisement.h"
|
||||
#include "device.h"
|
||||
#include "logger.h"
|
||||
|
||||
#define TAG "commeownder-sim"
|
||||
|
||||
#define MANUFACTURER_ID 0xFFFF
|
||||
#define MAGIC_0 0xC0
|
||||
#define MAGIC_1 0xDE
|
||||
#define PLAYER_NAME_LEN 8
|
||||
#define MAX_OPPONENTS 4
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
uint8_t magic[2];
|
||||
uint8_t game_id[2];
|
||||
char name[PLAYER_NAME_LEN];
|
||||
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)
|
||||
|
||||
static GMainLoop *loop = NULL;
|
||||
static Adapter *default_adapter = NULL;
|
||||
static Advertisement *adv = NULL;
|
||||
|
||||
static char g_name[PLAYER_NAME_LEN + 1] = {0};
|
||||
static int16_t g_life = 40;
|
||||
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;
|
||||
#define RESET_COOLDOWN_US (10LL * 1000000LL) // 10 seconds
|
||||
|
||||
static void start_advertising(void) {
|
||||
ble_payload_t payload;
|
||||
memset(&payload, 0, sizeof(payload));
|
||||
payload.magic[0] = MAGIC_0;
|
||||
payload.magic[1] = MAGIC_1;
|
||||
payload.game_id[0] = g_game_id[0];
|
||||
payload.game_id[1] = g_game_id[1];
|
||||
strncpy(payload.name, g_name, PLAYER_NAME_LEN);
|
||||
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));
|
||||
|
||||
adv = binc_advertisement_create();
|
||||
binc_advertisement_set_manufacturer_data(adv, MANUFACTURER_ID, data);
|
||||
binc_advertisement_set_interval(adv, 500, 500);
|
||||
binc_advertisement_set_general_discoverable(adv, FALSE);
|
||||
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",
|
||||
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]);
|
||||
}
|
||||
|
||||
static void restart_advertising(void) {
|
||||
if (adv) {
|
||||
binc_adapter_stop_advertising(default_adapter, adv);
|
||||
binc_advertisement_free(adv);
|
||||
adv = NULL;
|
||||
}
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
static void on_scan_result(Adapter *adapter, Device *device) {
|
||||
(void)adapter;
|
||||
GHashTable *mfr_data = binc_device_get_manufacturer_data(device);
|
||||
if (!mfr_data) return;
|
||||
|
||||
int key = MANUFACTURER_ID;
|
||||
GByteArray *data = g_hash_table_lookup(mfr_data, &key);
|
||||
if (!data || data->len < (guint)sizeof(ble_payload_t)) return;
|
||||
|
||||
const ble_payload_t *p = (const ble_payload_t *)data->data;
|
||||
if (p->magic[0] != MAGIC_0 || p->magic[1] != MAGIC_1) return;
|
||||
if (p->game_id[0] != g_game_id[0] || p->game_id[1] != g_game_id[1]) return;
|
||||
|
||||
char name[PLAYER_NAME_LEN + 1];
|
||||
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",
|
||||
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],
|
||||
p->eliminated ? " ELIMINATED" : "",
|
||||
p->reset_cmd ? " RESET_CMD" : "");
|
||||
fflush(stdout);
|
||||
|
||||
// Handle reset_cmd with cooldown to avoid acting on repeated broadcasts
|
||||
if (p->reset_cmd) {
|
||||
gint64 now = g_get_monotonic_time();
|
||||
if (now - g_last_reset_us >= RESET_COOLDOWN_US) {
|
||||
g_last_reset_us = now;
|
||||
g_life = g_start_life;
|
||||
g_poison = 0;
|
||||
g_eliminated = 0;
|
||||
printf("RESET_ALL life=%d\n", (int)g_life);
|
||||
fflush(stdout);
|
||||
restart_advertising();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_powered_state_changed(Adapter *adapter, gboolean state) {
|
||||
if (!state) return;
|
||||
start_advertising();
|
||||
binc_adapter_set_discovery_filter(adapter, -100, NULL, NULL);
|
||||
binc_adapter_set_discovery_cb(adapter, on_scan_result);
|
||||
binc_adapter_start_discovery(adapter);
|
||||
fprintf(stderr, "scanning for peers...\n");
|
||||
}
|
||||
|
||||
static void cleanup(void) {
|
||||
if (adv) {
|
||||
binc_adapter_stop_advertising(default_adapter, adv);
|
||||
binc_advertisement_free(adv);
|
||||
adv = NULL;
|
||||
}
|
||||
if (default_adapter) {
|
||||
binc_adapter_stop_discovery(default_adapter);
|
||||
binc_adapter_free(default_adapter);
|
||||
default_adapter = NULL;
|
||||
}
|
||||
if (loop) g_main_loop_quit(loop);
|
||||
}
|
||||
|
||||
static gboolean cleanup_handler(gpointer user_data) {
|
||||
(void)user_data;
|
||||
cleanup();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
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"
|
||||
" --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",
|
||||
prog);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
log_set_level(LOG_WARN);
|
||||
|
||||
static const struct option long_opts[] = {
|
||||
{"name", required_argument, NULL, 'n'},
|
||||
{"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}
|
||||
};
|
||||
|
||||
int name_set = 0;
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "", long_opts, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
strncpy(g_name, optarg, PLAYER_NAME_LEN);
|
||||
g_name[PLAYER_NAME_LEN] = '\0';
|
||||
name_set = 1;
|
||||
break;
|
||||
case 'l':
|
||||
g_life = (int16_t)atoi(optarg);
|
||||
break;
|
||||
case 'p':
|
||||
g_poison = (uint8_t)atoi(optarg);
|
||||
break;
|
||||
case 'g': {
|
||||
unsigned int id;
|
||||
if (sscanf(optarg, "%x", &id) != 1) {
|
||||
fprintf(stderr, "invalid game-id '%s' (expected 4 hex digits)\n", optarg);
|
||||
return 1;
|
||||
}
|
||||
g_game_id[0] = (uint8_t)((id >> 8) & 0xFF);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name_set) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
g_start_life = g_life;
|
||||
|
||||
g_unix_signal_add(SIGINT, cleanup_handler, NULL);
|
||||
g_unix_signal_add(SIGTERM, cleanup_handler, NULL);
|
||||
|
||||
GDBusConnection *dbusConnection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
|
||||
if (!dbusConnection) {
|
||||
fprintf(stderr, "failed to connect to D-Bus\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
loop = g_main_loop_new(NULL, FALSE);
|
||||
|
||||
default_adapter = binc_adapter_get_default(dbusConnection);
|
||||
if (!default_adapter) {
|
||||
fprintf(stderr, "no Bluetooth adapter found\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "adapter: %s (%s)\n",
|
||||
binc_adapter_get_name(default_adapter),
|
||||
binc_adapter_get_address(default_adapter));
|
||||
|
||||
binc_adapter_set_powered_state_cb(default_adapter, on_powered_state_changed);
|
||||
|
||||
if (binc_adapter_get_powered_state(default_adapter)) {
|
||||
on_powered_state_changed(default_adapter, TRUE);
|
||||
} else {
|
||||
binc_adapter_power_on(default_adapter);
|
||||
}
|
||||
|
||||
g_main_loop_run(loop);
|
||||
|
||||
g_dbus_connection_close_sync(dbusConnection, NULL, NULL);
|
||||
g_object_unref(dbusConnection);
|
||||
g_main_loop_unref(loop);
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue