Cleanup
This commit is contained in:
@@ -1 +1 @@
|
||||
7bad751cc212e33e418ee5528532e97d795b8d82
|
||||
11b43f3b2b511fe7454baf368b17d1fda87cd079
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Written by Carl Vargklint 2026-05-19
|
||||
# Install platformio CLI then run the script from this folder.
|
||||
pio run -t upload
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// ================================================================
|
||||
// badge_config.h — Secfest 2026 Badge
|
||||
// ================================================================
|
||||
// Shared badge configuration: EEPROM layout, types, and globals
|
||||
// accessed by both main.cpp and serial_games.cpp.
|
||||
//
|
||||
// EEPROM map (32 bytes):
|
||||
// Byte 0 : matrix/global brightness (1-8)
|
||||
// Byte 1 : flashlight brightness (1-8)
|
||||
// Bytes 2-25 : IR codes for 6 buttons × 4 bytes each
|
||||
// Layout per button: [protocol][addr_lo][addr_hi][command]
|
||||
// ================================================================
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// ── EEPROM ──────────────────────────────────────────────────────
|
||||
#define EEPROM_CFG_SIZE 32
|
||||
#define EEPROM_ADDR_BRIGHT 0
|
||||
#define EEPROM_ADDR_FLASH 1
|
||||
#define EEPROM_ADDR_IR_BASE 2 // 4 bytes × 6 buttons = 24 bytes
|
||||
|
||||
// ── IR protocol IDs ─────────────────────────────────────────────
|
||||
#define IR_PROTO_NEC 0
|
||||
#define IR_PROTO_SAMSUNG 1
|
||||
#define IR_PROTO_SONY 2
|
||||
#define IR_PROTO_RC5 3
|
||||
|
||||
// ── IR code record (4 bytes on EEPROM) ──────────────────────────
|
||||
struct IrCode {
|
||||
uint8_t protocol; // IR_PROTO_*
|
||||
uint8_t addr_lo; // address low byte
|
||||
uint8_t addr_hi; // address high byte
|
||||
uint8_t command; // command byte
|
||||
};
|
||||
|
||||
static inline uint16_t irCodeAddr(const IrCode& c) {
|
||||
return (uint16_t)c.addr_lo | ((uint16_t)c.addr_hi << 8);
|
||||
}
|
||||
|
||||
// ── Button indices (must match BtnIdx in main.cpp) ───────────────
|
||||
// BI_A=0, BI_B=1, BI_UP=2, BI_DOWN=3, BI_LEFT=4, BI_RIGHT=5
|
||||
#define IR_BTN_COUNT 6
|
||||
|
||||
// ── Full badge configuration ─────────────────────────────────────
|
||||
struct BadgeConfig {
|
||||
uint8_t brightness; // matrix + front LED depth, 1-8
|
||||
uint8_t flashBright; // flashlight brightness, 1-8
|
||||
IrCode ir[IR_BTN_COUNT];// per-button IR codes
|
||||
};
|
||||
|
||||
// Default IR codes — Samsung TV (NEC-compatible)
|
||||
// Power=0x02, Mute=0x0F, VolumeUp=0x07, VolDown=0x0B, ChUp=0x12, ChDn=0x10
|
||||
static const BadgeConfig BADGE_CONFIG_DEFAULT = {
|
||||
8, // brightness
|
||||
8, // flashBright
|
||||
{
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x07 }, // A → Volume Up
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x0B }, // B → Volume Down
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x12 }, // Up → Channel Up
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x10 }, // Down → Channel Down
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x0F }, // Left → Mute
|
||||
{ IR_PROTO_NEC, 0x07, 0x07, 0x02 }, // Right → Power
|
||||
}
|
||||
};
|
||||
|
||||
// ── Global config — defined in main.cpp ──────────────────────────
|
||||
extern BadgeConfig g_cfg;
|
||||
|
||||
// ── Persistence helpers — defined in main.cpp ────────────────────
|
||||
void configLoad();
|
||||
void configSave();
|
||||
void configSaveBrightness();
|
||||
void configSaveFlash();
|
||||
void configSaveIr(uint8_t btnIdx);
|
||||
+283
-35
@@ -15,27 +15,75 @@
|
||||
#include <Adafruit_IS31FL3731.h>
|
||||
#include <EEPROM.h>
|
||||
#include "font4x7.h"
|
||||
#include "badge_config.h"
|
||||
#include "serial_games.h"
|
||||
|
||||
static SerialGames serialGames;
|
||||
|
||||
// ============================================================ pin map =======
|
||||
|
||||
#define MATRIX_BRIGHTNESS 20
|
||||
|
||||
// ============================================================ brightness =====
|
||||
// Global brightness scale: 1 (dim) … 8 (full).
|
||||
// Stored in EEPROM byte 0; loaded at boot, saved whenever changed.
|
||||
#define EEPROM_SIZE 4
|
||||
#define EEPROM_ADDR_BRIGHT 0
|
||||
static uint8_t g_brightness = 8; // 1..8
|
||||
// ============================================================ config / EEPROM =
|
||||
// g_cfg is the live badge configuration. Load on boot, save whenever changed.
|
||||
// Helpers declared here, defined below (badge_config.h has extern declarations).
|
||||
BadgeConfig g_cfg = BADGE_CONFIG_DEFAULT;
|
||||
|
||||
static void loadBrightness() {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
uint8_t v = EEPROM.read(EEPROM_ADDR_BRIGHT);
|
||||
g_brightness = (v >= 1 && v <= 8) ? v : 8;
|
||||
// Convenience shorthand — used throughout the file
|
||||
#define g_brightness g_cfg.brightness
|
||||
#define g_flashBright g_cfg.flashBright
|
||||
|
||||
void configLoad() {
|
||||
EEPROM.begin(EEPROM_CFG_SIZE);
|
||||
uint8_t bright = EEPROM.read(EEPROM_ADDR_BRIGHT);
|
||||
g_cfg.brightness = (bright >= 1 && bright <= 8) ? bright : 8;
|
||||
uint8_t flash = EEPROM.read(EEPROM_ADDR_FLASH);
|
||||
g_cfg.flashBright = (flash >= 1 && flash <= 8) ? flash : 8;
|
||||
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
|
||||
uint16_t base = EEPROM_ADDR_IR_BASE + i * 4;
|
||||
uint8_t proto = EEPROM.read(base);
|
||||
if (proto > IR_PROTO_RC5) {
|
||||
// uninitialised — use default
|
||||
g_cfg.ir[i] = BADGE_CONFIG_DEFAULT.ir[i];
|
||||
} else {
|
||||
g_cfg.ir[i].protocol = proto;
|
||||
g_cfg.ir[i].addr_lo = EEPROM.read(base + 1);
|
||||
g_cfg.ir[i].addr_hi = EEPROM.read(base + 2);
|
||||
g_cfg.ir[i].command = EEPROM.read(base + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void saveBrightness(uint8_t v) {
|
||||
g_brightness = v;
|
||||
EEPROM.write(EEPROM_ADDR_BRIGHT, v);
|
||||
void configSave() {
|
||||
EEPROM.write(EEPROM_ADDR_BRIGHT, g_cfg.brightness);
|
||||
EEPROM.write(EEPROM_ADDR_FLASH, g_cfg.flashBright);
|
||||
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
|
||||
uint16_t base = EEPROM_ADDR_IR_BASE + i * 4;
|
||||
EEPROM.write(base, g_cfg.ir[i].protocol);
|
||||
EEPROM.write(base + 1, g_cfg.ir[i].addr_lo);
|
||||
EEPROM.write(base + 2, g_cfg.ir[i].addr_hi);
|
||||
EEPROM.write(base + 3, g_cfg.ir[i].command);
|
||||
}
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
void configSaveBrightness() {
|
||||
EEPROM.write(EEPROM_ADDR_BRIGHT, g_cfg.brightness);
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
void configSaveFlash() {
|
||||
EEPROM.write(EEPROM_ADDR_FLASH, g_cfg.flashBright);
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
void configSaveIr(uint8_t idx) {
|
||||
if (idx >= IR_BTN_COUNT) return;
|
||||
uint16_t base = EEPROM_ADDR_IR_BASE + idx * 4;
|
||||
EEPROM.write(base, g_cfg.ir[idx].protocol);
|
||||
EEPROM.write(base + 1, g_cfg.ir[idx].addr_lo);
|
||||
EEPROM.write(base + 2, g_cfg.ir[idx].addr_hi);
|
||||
EEPROM.write(base + 3, g_cfg.ir[idx].command);
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
@@ -49,7 +97,7 @@ static void saveBrightness(uint8_t v) {
|
||||
#define SAO_GP1 0
|
||||
#define SAO_GP2 1
|
||||
|
||||
#define LED_FLASHLIGHT 15
|
||||
#define LED_FLASHLIGHT 14
|
||||
#define LED_FRONT_1 20
|
||||
#define LED_FRONT_2 21
|
||||
#define LED_FRONT_3 22
|
||||
@@ -59,9 +107,9 @@ static void saveBrightness(uint8_t v) {
|
||||
#define LED_MATRIX_SCL 5
|
||||
#define IS31_ADDR 0x74
|
||||
|
||||
// Front LEDs are active-low (anode to 3V3, cathode to GPIO via series R).
|
||||
const int FRONT_LED_ON = LOW;
|
||||
const int FRONT_LED_OFF = HIGH;
|
||||
// Front LEDs are active-high (anode to GPIO, cathode to GND).
|
||||
const int FRONT_LED_ON = HIGH;
|
||||
const int FRONT_LED_OFF = LOW;
|
||||
const int FRONT_LEDS[] = { LED_FRONT_1, LED_FRONT_2, LED_FRONT_3, LED_FRONT_4 };
|
||||
const int NUM_FRONT_LEDS = 4;
|
||||
|
||||
@@ -665,7 +713,7 @@ static void settingsStep() {
|
||||
settingsCtx.brightness--;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) saveBrightness(settingsCtx.brightness);
|
||||
if (changed) { g_brightness = settingsCtx.brightness; configSaveBrightness(); }
|
||||
|
||||
// — Draw brightness bar ——————————————————————————————
|
||||
// Rows 3-5: filled bar, 1 column per level (1..8 → cols 0..7).
|
||||
@@ -685,9 +733,174 @@ static void settingsStep() {
|
||||
fbPush();
|
||||
}
|
||||
|
||||
// ============================================================ IR sender ======
|
||||
//
|
||||
// Software IR modulation on LED_FLASHLIGHT (GPIO15 → MOSFET → IR/white LEDs).
|
||||
// Implements NEC (32-bit) and Samsung (which is NEC-compatible but with a
|
||||
// repeated address byte). Sony SIRC-12 is also supported.
|
||||
//
|
||||
// Blocking during transmission (~67 ms for NEC), but called only on user input.
|
||||
|
||||
#define IR_PIN LED_FLASHLIGHT
|
||||
#define IR_FREQ_NEC 38000
|
||||
#define IR_FREQ_SONY 40000
|
||||
|
||||
static void irOn(uint16_t us) {
|
||||
// 33 % duty is standard for IR LEDs — avoids overheating
|
||||
analogWrite(IR_PIN, 333);
|
||||
delayMicroseconds(us);
|
||||
}
|
||||
static void irOff(uint16_t us) {
|
||||
analogWrite(IR_PIN, 0);
|
||||
delayMicroseconds(us);
|
||||
}
|
||||
|
||||
static void irSendNEC(uint16_t address, uint8_t command, bool samsung = false) {
|
||||
analogWriteFreq(IR_FREQ_NEC);
|
||||
analogWriteRange(1000);
|
||||
// Header: 9 ms burst + 4.5 ms space
|
||||
irOn(9000);
|
||||
irOff(samsung ? 4500 : 4500); // Samsung header is identical to NEC here
|
||||
// 16-bit address (8-bit addr + ~addr for NEC; 8-bit addr repeated for Samsung)
|
||||
uint8_t a1 = (uint8_t)(address & 0xFF);
|
||||
uint8_t a2 = samsung ? a1 : (uint8_t)(~a1 & 0xFF);
|
||||
uint8_t c1 = command;
|
||||
uint8_t c2 = (uint8_t)(~c1 & 0xFF);
|
||||
for (int i = 0; i < 8; i++) { irOn(562); irOff((a1 >> i) & 1 ? 1687 : 562); }
|
||||
for (int i = 0; i < 8; i++) { irOn(562); irOff((a2 >> i) & 1 ? 1687 : 562); }
|
||||
for (int i = 0; i < 8; i++) { irOn(562); irOff((c1 >> i) & 1 ? 1687 : 562); }
|
||||
for (int i = 0; i < 8; i++) { irOn(562); irOff((c2 >> i) & 1 ? 1687 : 562); }
|
||||
irOn(562);
|
||||
analogWrite(IR_PIN, 0);
|
||||
}
|
||||
|
||||
static void irSendSony(uint16_t address, uint8_t command) {
|
||||
analogWriteFreq(IR_FREQ_SONY);
|
||||
analogWriteRange(1000);
|
||||
irOn(2400); irOff(600);
|
||||
for (int i = 6; i >= 0; i--) { irOn((command >> i) & 1 ? 1200 : 600); irOff(600); }
|
||||
for (int i = 4; i >= 0; i--) { irOn((address >> i) & 1 ? 1200 : 600); irOff(600); }
|
||||
analogWrite(IR_PIN, 0);
|
||||
}
|
||||
|
||||
static void irSendCode(const IrCode& c) {
|
||||
uint16_t addr = irCodeAddr(c);
|
||||
switch (c.protocol) {
|
||||
case IR_PROTO_NEC: irSendNEC(addr, c.command, false); break;
|
||||
case IR_PROTO_SAMSUNG: irSendNEC(addr, c.command, true); break;
|
||||
case IR_PROTO_SONY: irSendSony(addr, c.command); break;
|
||||
case IR_PROTO_RC5: irSendNEC(addr, c.command, false); break; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================ flashlight =====
|
||||
//
|
||||
// All four front LEDs continue their Knight Rider animation in the background.
|
||||
// LED_FLASHLIGHT (GPIO15) is PWM'd at variable duty for the flashlight.
|
||||
// Level 8 → duty 1000 (constant on, maximum output)
|
||||
// Level 1 → duty 125 (dim — still on, no strobe at minimum)
|
||||
// Up/Down → adjust brightness level. B → exit to menu.
|
||||
|
||||
struct FlashState {
|
||||
bool exitRequested;
|
||||
};
|
||||
static FlashState flashCtx;
|
||||
|
||||
static void flashApply() {
|
||||
int duty = (int)((float)g_flashBright / 8.0f * 1000.0f);
|
||||
analogWriteFreq(1000);
|
||||
analogWriteRange(1000);
|
||||
analogWrite(IR_PIN, duty);
|
||||
}
|
||||
|
||||
static void flashReset() {
|
||||
flashCtx.exitRequested = false;
|
||||
flashApply();
|
||||
}
|
||||
|
||||
static void flashStep() {
|
||||
if (input.pressed[BI_B]) {
|
||||
flashCtx.exitRequested = true;
|
||||
analogWrite(IR_PIN, 0); // off when leaving
|
||||
return;
|
||||
}
|
||||
bool changed = false;
|
||||
if (input.pressed[BI_UP] && g_flashBright < 8) { g_flashBright++; changed = true; }
|
||||
if (input.pressed[BI_DOWN] && g_flashBright > 1) { g_flashBright--; changed = true; }
|
||||
if (changed) { configSaveFlash(); flashApply(); }
|
||||
|
||||
// Draw brightness level bar on matrix (rows 3-5, same style as Settings)
|
||||
fbClear();
|
||||
uint8_t bar = 180;
|
||||
for (int x = 0; x < g_flashBright; x++) {
|
||||
fbSet(x, 3, bar);
|
||||
fbSet(x, 4, bar);
|
||||
fbSet(x, 5, bar);
|
||||
}
|
||||
int edge = g_flashBright - 1;
|
||||
fbSet(edge, 1, 255);
|
||||
fbSet(edge, 7, 255);
|
||||
// Torch icon — two pixels above bar
|
||||
fbSet(3, 0, 200); fbSet(4, 0, 255); fbSet(5, 0, 200);
|
||||
fbPush();
|
||||
}
|
||||
|
||||
// ============================================================ IR remote ======
|
||||
//
|
||||
// Each directional/A/B button sends the configured IR code.
|
||||
// A+B held simultaneously exits. The matrix shows a remote icon.
|
||||
// The front LEDs continue Knight Rider in the background.
|
||||
|
||||
static unsigned long irLastSend = 0;
|
||||
static bool irExitRequested = false;
|
||||
|
||||
static void irRemoteReset() {
|
||||
irExitRequested = false;
|
||||
irLastSend = 0;
|
||||
}
|
||||
|
||||
static void irRemoteDrawIcon() {
|
||||
fbClear();
|
||||
// Stylised remote outline on 9×9 matrix
|
||||
for (int y = 1; y <= 7; y++) { fbSet(2, y, 80); fbSet(6, y, 80); }
|
||||
for (int x = 2; x <= 6; x++) { fbSet(x, 1, 80); fbSet(x, 7, 80); }
|
||||
fbSet(4, 2, 200); // top button
|
||||
fbSet(3, 4, 120); fbSet(5, 4, 120); // side buttons
|
||||
fbSet(4, 5, 120); // bottom button
|
||||
fbPush();
|
||||
}
|
||||
|
||||
static void irRemoteStep() {
|
||||
// Exit on A+B held together
|
||||
if (input.down[BI_A] && input.down[BI_B]) {
|
||||
irExitRequested = true;
|
||||
analogWrite(IR_PIN, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long now = millis();
|
||||
// 200 ms gap between sends to avoid flooding
|
||||
if (now - irLastSend < 200) { irRemoteDrawIcon(); return; }
|
||||
|
||||
static const int BTN_MAP[IR_BTN_COUNT] = {
|
||||
BI_A, BI_B, BI_UP, BI_DOWN, BI_LEFT, BI_RIGHT
|
||||
};
|
||||
for (int i = 0; i < IR_BTN_COUNT; i++) {
|
||||
if (input.pressed[BTN_MAP[i]]) {
|
||||
// Flash all front LEDs briefly to indicate send
|
||||
for (int p : FRONT_LEDS) analogWrite(p, 1000);
|
||||
irSendCode(g_cfg.ir[i]);
|
||||
for (int p : FRONT_LEDS) analogWrite(p, 0);
|
||||
irLastSend = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
irRemoteDrawIcon();
|
||||
}
|
||||
|
||||
// ============================================================ menu =========
|
||||
|
||||
const char* MENU_ITEMS[] = { "SNAKE", "SIMON", "WELCOME", "SETTINGS" };
|
||||
const char* MENU_ITEMS[] = { "SNAKE", "SIMON", "WELCOME", "SETTINGS", "FLASHLIGHT", "IR REMOTE" };
|
||||
const int MENU_LEN = sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]);
|
||||
static int menuIdx = 0;
|
||||
|
||||
@@ -725,6 +938,8 @@ enum AppState {
|
||||
ST_SNAKE,
|
||||
ST_SIMON,
|
||||
ST_SETTINGS,
|
||||
ST_FLASHLIGHT,
|
||||
ST_IR_REMOTE,
|
||||
};
|
||||
|
||||
static AppState appState = ST_BOOT_SWEEP;
|
||||
@@ -742,6 +957,8 @@ static void enterState(AppState s) {
|
||||
case ST_SNAKE: snakeReset(); break;
|
||||
case ST_SIMON: simonReset(); break;
|
||||
case ST_SETTINGS: settingsReset(); break;
|
||||
case ST_FLASHLIGHT: flashReset(); break;
|
||||
case ST_IR_REMOTE: irRemoteReset(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -776,29 +993,42 @@ static void saoUpdate() {
|
||||
// Passive background animation: a bright spot sweeps back and forth across
|
||||
// the four front LEDs using hardware PWM.
|
||||
//
|
||||
// Front LEDs are active-low (anode → 3V3, cathode → GPIO):
|
||||
// analogWrite duty 0 = pin always LOW = LED fully ON
|
||||
// analogWrite duty 1000 = pin always HIGH = LED fully OFF
|
||||
// Front LEDs are active-HIGH (anode → GPIO, cathode → GND via resistor):
|
||||
// analogWrite duty 0 = pin always LOW = LED fully OFF
|
||||
// analogWrite duty 1000 = pin always HIGH = LED fully ON
|
||||
//
|
||||
// g_brightness controls sweep depth:
|
||||
// 8 → all LEDs constantly fully on (no animation)
|
||||
// 1 → full Knight Rider sweep (background goes dark, peak is fully on)
|
||||
// 2-7 → intermediate — background LEDs stay at a raised floor level
|
||||
//
|
||||
// The sweep is skipped while ST_SNAKE or ST_SIMON own the front LEDs.
|
||||
|
||||
static void frontKnightRiderUpdate() {
|
||||
if (appState == ST_SNAKE || appState == ST_SIMON) return;
|
||||
if (appState == ST_SNAKE || appState == ST_SIMON || appState == ST_IR_REMOTE) return;
|
||||
|
||||
if (g_brightness >= 8) {
|
||||
// Brightness at maximum → all LEDs constantly on, no animation
|
||||
for (int i = 0; i < NUM_FRONT_LEDS; i++) analogWrite(FRONT_LEDS[i], 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Triangle wave, period 800 ms → position sweeps 0.0 → 3.0 → 0.0
|
||||
const float period = 800.0f;
|
||||
float t = fmodf((float)millis(), period) / period; // 0 .. 1
|
||||
float tri = (t < 0.5f) ? (t * 2.0f) : (2.0f - t * 2.0f); // 0 .. 1 .. 0
|
||||
float pos = tri * 3.0f; // 0.0 .. 3.0
|
||||
float t = fmodf((float)millis(), period) / period;
|
||||
float tri = (t < 0.5f) ? (t * 2.0f) : (2.0f - t * 2.0f);
|
||||
float pos = tri * 3.0f;
|
||||
|
||||
// animDepth: 1 = full sweep at level 1, 0 = no sweep at level 8
|
||||
float animDepth = (float)(8 - g_brightness) / 7.0f; // 1 at level 1, ~0 at level 7
|
||||
// floor = minimum duty at the dark end of the sweep
|
||||
float floor_val = 1.0f - animDepth;
|
||||
|
||||
for (int i = 0; i < NUM_FRONT_LEDS; i++) {
|
||||
float dist = fabsf(pos - (float)i);
|
||||
// Soft glow: full brightness within 0.5 px, fades out to 1.5 px
|
||||
float brightness = (dist < 1.5f) ? (1.0f - dist / 1.5f) : 0.0f;
|
||||
// Scale by global brightness setting
|
||||
brightness *= (float)g_brightness / 8.0f;
|
||||
// Invert for active-low: duty 0 = fully on, 1000 = fully off
|
||||
int duty = 1000 - (int)(brightness * 1000.0f);
|
||||
float dist = fabsf(pos - (float)i);
|
||||
float peak = (dist < 1.5f) ? (1.0f - dist / 1.5f) : 0.0f;
|
||||
float bright = floor_val + (1.0f - floor_val) * peak; // never below floor_val
|
||||
int duty = (int)(bright * 1000.0f);
|
||||
analogWrite(FRONT_LEDS[i], duty);
|
||||
}
|
||||
}
|
||||
@@ -834,8 +1064,11 @@ void setup() {
|
||||
// Seed RNG from a floating ADC pin if available; otherwise use micros().
|
||||
randomSeed(micros() ^ analogRead(A0));
|
||||
|
||||
// Load persisted brightness from flash-backed EEPROM
|
||||
loadBrightness();
|
||||
// Load persisted config (brightness, flashlight brightness, IR codes)
|
||||
configLoad();
|
||||
|
||||
// Serial terminal games (Mastermind, Hangman, Minesweeper)
|
||||
serialGames.begin();
|
||||
|
||||
// SAO GPIO PWM (must come after randomSeed / analogRead so ADC init is done)
|
||||
saoSetup();
|
||||
@@ -846,6 +1079,7 @@ void setup() {
|
||||
// ============================================================ loop =========
|
||||
|
||||
void loop() {
|
||||
serialGames.update(); // USB-CDC terminal games (non-blocking)
|
||||
saoUpdate(); // SAO GP1 sin-wave PWM, GP2 constant 10 %
|
||||
frontKnightRiderUpdate(); // front LED Knight Rider sweep
|
||||
inputUpdate();
|
||||
@@ -897,6 +1131,8 @@ void loop() {
|
||||
case 1: enterState(ST_SIMON); break;
|
||||
case 2: enterState(ST_WELCOME_SCROLL); break;
|
||||
case 3: enterState(ST_SETTINGS); break;
|
||||
case 4: enterState(ST_FLASHLIGHT); break;
|
||||
case 5: enterState(ST_IR_REMOTE); break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -919,5 +1155,17 @@ void loop() {
|
||||
if (settingsCtx.exitRequested) enterState(ST_MENU);
|
||||
break;
|
||||
}
|
||||
|
||||
case ST_FLASHLIGHT: {
|
||||
flashStep();
|
||||
if (flashCtx.exitRequested) enterState(ST_MENU);
|
||||
break;
|
||||
}
|
||||
|
||||
case ST_IR_REMOTE: {
|
||||
irRemoteStep();
|
||||
if (irExitRequested) enterState(ST_MENU);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,770 @@
|
||||
// ================================================================
|
||||
// serial_games.cpp — Secfest 2026 Badge
|
||||
// ================================================================
|
||||
#include "serial_games.h"
|
||||
|
||||
// ── VT100 / ANSI helpers ─────────────────────────────────────────
|
||||
#define CLS "\033[2J\033[H" // clear screen + home cursor
|
||||
#define RST "\033[0m" // reset all attributes
|
||||
#define BLD "\033[1m" // bold
|
||||
#define DIM "\033[2m" // dim
|
||||
#define RED "\033[91m" // bright red
|
||||
#define GRN "\033[92m" // bright green
|
||||
#define YLW "\033[93m" // bright yellow
|
||||
#define BLU "\033[94m" // bright blue
|
||||
#define MAG "\033[95m" // bright magenta
|
||||
#define CYN "\033[96m" // bright cyan
|
||||
#define GRY "\033[90m" // dark grey
|
||||
|
||||
// ── Hangman word list (security / hacking themed) ───────────────
|
||||
const char* const SerialGames::_hmWords[] = {
|
||||
"firewall", "exploit", "malware", "phishing", "rootkit",
|
||||
"payload", "sandbox", "fuzzing", "honeypot", "cipher",
|
||||
"botnet", "spoofing", "backdoor", "shellcode", "keylogger",
|
||||
"pivoting", "injection", "sniffing", "ransomware", "escalation",
|
||||
"obfuscation", "enumeration", "stealthy", "exfiltrate", "wireshark",
|
||||
"shodan", "zerotrust", "darknet", "privilege", "pentest",
|
||||
};
|
||||
const uint8_t SerialGames::_hmWordCount =
|
||||
sizeof(SerialGames::_hmWords) / sizeof(SerialGames::_hmWords[0]);
|
||||
|
||||
// Gallows rows [stage 0..6][row 0..6] — stage = number of wrong guesses
|
||||
static const char* const GALLOWS[7][7] = {
|
||||
{" +---+", " | |", " |", " |", " |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " |", " |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " | |", " |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " /| |", " |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " /|\\ |", " |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " /|\\ |", " / |", " |", "========="},
|
||||
{" +---+", " | |", " O |", " /|\\ |", " / \\ |", " |", "========="},
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// Public API
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::begin() {
|
||||
_st = St::IDLE;
|
||||
_prevConn = false;
|
||||
_clearLine();
|
||||
}
|
||||
|
||||
void SerialGames::update() {
|
||||
bool conn = (bool)Serial;
|
||||
|
||||
// ── detect connect / disconnect ──────────────────────────
|
||||
if (conn && !_prevConn) {
|
||||
delay(200); // let the terminal finish its handshake
|
||||
_drawMenu();
|
||||
_st = St::MENU;
|
||||
_lineMode = false;
|
||||
}
|
||||
if (!conn) {
|
||||
_prevConn = false;
|
||||
_st = St::IDLE;
|
||||
return;
|
||||
}
|
||||
_prevConn = conn;
|
||||
if (_st == St::IDLE) return;
|
||||
|
||||
// ── drain available bytes ────────────────────────────────
|
||||
while (Serial.available()) {
|
||||
char c = (char)Serial.read();
|
||||
|
||||
// Swallow VT100 escape sequences (arrow keys etc.)
|
||||
if (c == '\033') {
|
||||
unsigned long t = millis();
|
||||
while (Serial.available() && (millis() - t) < 25) Serial.read();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_lineMode) {
|
||||
_feedChar(c);
|
||||
} else {
|
||||
if (c >= 'A' && c <= 'Z') c += 32; // normalise to lower-case
|
||||
_charDispatch(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Input helpers
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::_clearLine() {
|
||||
memset(_line, 0, sizeof(_line));
|
||||
_lineLen = 0;
|
||||
}
|
||||
|
||||
// Called for every byte in line-mode. Echoes characters, handles
|
||||
// backspace, and dispatches to the relevant game on Enter.
|
||||
void SerialGames::_feedChar(char c) {
|
||||
if (c == '\r' || c == '\n') {
|
||||
// Ignore the bare \n of a \r\n pair
|
||||
if (c == '\n' && _lineLen == 0) return;
|
||||
|
||||
Serial.print("\r\n");
|
||||
_line[_lineLen] = '\0';
|
||||
|
||||
St prevSt = _st;
|
||||
bool redraw = false;
|
||||
|
||||
if (_st == St::MASTERMIND) redraw = _mmOnLine();
|
||||
else if (_st == St::MINESWEEPER) redraw = _msOnLine();
|
||||
else if (_st == St::ADVENTURE) {
|
||||
_adv.feedLine(_line);
|
||||
if (_adv.quitToMenu) {
|
||||
_adv.quitToMenu = false;
|
||||
_st = St::MENU;
|
||||
_lineMode = false;
|
||||
_drawMenu();
|
||||
}
|
||||
}
|
||||
else if (_st == St::CONFIG) {
|
||||
_cfgOnLine();
|
||||
}
|
||||
|
||||
_clearLine();
|
||||
|
||||
// Only redraw if still in the same state (handlers that exit to menu
|
||||
// have already redrawn the menu themselves)
|
||||
if (_st == prevSt && redraw) {
|
||||
if (_st == St::MASTERMIND) _mmDraw();
|
||||
if (_st == St::MINESWEEPER) _msDraw();
|
||||
}
|
||||
|
||||
} else if ((c == '\b' || c == 127) && _lineLen > 0) {
|
||||
_lineLen--;
|
||||
Serial.print("\b \b"); // erase the echoed character
|
||||
|
||||
} else if (c >= 32 && _lineLen < 31) {
|
||||
_line[_lineLen++] = c;
|
||||
Serial.print(c); // local echo
|
||||
}
|
||||
}
|
||||
|
||||
// Called for every byte in char-mode (Menu, Hangman).
|
||||
void SerialGames::_charDispatch(char c) {
|
||||
switch (_st) {
|
||||
case St::MENU:
|
||||
if (c == '1') _mmStart();
|
||||
else if (c == '2') _hmStart();
|
||||
else if (c == '3') _msStart();
|
||||
else if (c == '4') _advStart();
|
||||
else if (c == '5') _cfgStart();
|
||||
break;
|
||||
|
||||
case St::HANGMAN:
|
||||
if (c == 'q') { _drawMenu(); _st = St::MENU; }
|
||||
else if (c >= 'a' && c <= 'z') _hmOnChar(c);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Screen helpers
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::_cls() { Serial.print(CLS); }
|
||||
|
||||
void SerialGames::_drawMenu() {
|
||||
_cls();
|
||||
Serial.print(
|
||||
BLD CYN
|
||||
" ╔══════════════════════════════════════╗\r\n"
|
||||
" ║ SECFEST 2026 BADGE GAMES ║\r\n"
|
||||
" ╚══════════════════════════════════════╝\r\n"
|
||||
RST "\r\n"
|
||||
" " BLD "[1]" RST " MASTERMIND — Crack the 4-digit code\r\n"
|
||||
" " BLD "[2]" RST " HANGMAN — Security vocabulary edition\r\n"
|
||||
" " BLD "[3]" RST " MINESWEEPER — 8 × 8 grid, 10 mines\r\n"
|
||||
" " BLD "[4]" RST " ZERO DAY — Text adventure\r\n"
|
||||
" " BLD "[5]" RST " CONFIG — Badge settings & IR codes\r\n"
|
||||
"\r\n"
|
||||
GRY " Press 1–5 to start. Q inside any game returns here.\r\n" RST
|
||||
);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MASTERMIND
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::_mmStart() {
|
||||
for (uint8_t i = 0; i < 4; i++) _mm.secret[i] = (uint8_t)random(1, 7);
|
||||
_mm.nGuess = 0;
|
||||
_mm.won = false;
|
||||
_mm.over = false;
|
||||
_st = St::MASTERMIND;
|
||||
_lineMode = true;
|
||||
_clearLine();
|
||||
_mmDraw();
|
||||
}
|
||||
|
||||
void SerialGames::_mmDraw() {
|
||||
_cls();
|
||||
Serial.print(
|
||||
BLD YLW
|
||||
" ╔══════════════════════════════════════╗\r\n"
|
||||
" ║ CRACK THE CODE ║\r\n"
|
||||
" ║ Guess the 4-digit code (1 – 6) ║\r\n"
|
||||
" ╚══════════════════════════════════════╝\r\n"
|
||||
RST "\r\n"
|
||||
" # GUESS " BLD GRN "●" RST " bulls " BLD CYN "○" RST " cows\r\n"
|
||||
GRY " ──────────────────────────────────────\r\n" RST
|
||||
);
|
||||
|
||||
for (uint8_t i = 0; i < 10; i++) {
|
||||
if (i < _mm.nGuess) {
|
||||
Serial.printf(" %2u ", i + 1);
|
||||
for (uint8_t j = 0; j < 4; j++) Serial.printf("%u ", _mm.guess[i][j]);
|
||||
Serial.printf(" " BLD GRN "%u" RST " " BLD CYN "%u" RST "\r\n",
|
||||
_mm.bulls[i], _mm.cows[i]);
|
||||
} else {
|
||||
Serial.print(GRY " . _ _ _ _\r\n" RST);
|
||||
}
|
||||
}
|
||||
Serial.print("\r\n");
|
||||
|
||||
if (_mm.won) {
|
||||
Serial.printf(BLD GRN " ✓ Cracked in %u guess%s! Well played.\r\n" RST,
|
||||
_mm.nGuess, _mm.nGuess == 1 ? "" : "es");
|
||||
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
if (_mm.over) {
|
||||
Serial.printf(BLD RED " ✗ Out of guesses. Code was: %u %u %u %u\r\n" RST,
|
||||
_mm.secret[0], _mm.secret[1], _mm.secret[2], _mm.secret[3]);
|
||||
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf(" Attempts remaining: " BLD "%u" RST "\r\n\r\n", 10 - _mm.nGuess);
|
||||
Serial.print(" Enter 4 digits (1-6), e.g. 1234. Q = quit: ");
|
||||
}
|
||||
|
||||
// Returns true if the screen needs a full redraw.
|
||||
bool SerialGames::_mmOnLine() {
|
||||
// Quit
|
||||
if (_line[0] == 'q' || _line[0] == 'Q') {
|
||||
_drawMenu();
|
||||
_st = St::MENU;
|
||||
_lineMode = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow new game when already over
|
||||
if (_mm.won || _mm.over) return false;
|
||||
|
||||
// Collect exactly 4 digits 1-6, ignoring spaces and other noise
|
||||
uint8_t digits[4], n = 0;
|
||||
for (uint8_t i = 0; _line[i] && n < 4; i++) {
|
||||
if (_line[i] >= '1' && _line[i] <= '6') digits[n++] = (uint8_t)(_line[i] - '0');
|
||||
}
|
||||
if (n < 4) {
|
||||
Serial.print(RED " ✗ Need exactly 4 digits in range 1-6 (e.g. 1234).\r\n" RST);
|
||||
Serial.print(" Enter 4 digits (1-6), e.g. 1234. Q = quit: ");
|
||||
return false; // bad input — don't redraw; we already printed a message
|
||||
}
|
||||
|
||||
uint8_t r = _mm.nGuess;
|
||||
for (uint8_t i = 0; i < 4; i++) _mm.guess[r][i] = digits[i];
|
||||
|
||||
// Score: bulls (right digit + right position), cows (right digit, wrong position)
|
||||
uint8_t bulls = 0, cows = 0;
|
||||
uint8_t sc[7] = {}, gc[7] = {};
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (_mm.secret[i] == digits[i]) {
|
||||
bulls++;
|
||||
} else {
|
||||
sc[_mm.secret[i]]++;
|
||||
gc[digits[i]]++;
|
||||
}
|
||||
}
|
||||
for (uint8_t d = 1; d <= 6; d++) cows += min(sc[d], gc[d]);
|
||||
|
||||
_mm.bulls[r] = bulls;
|
||||
_mm.cows[r] = cows;
|
||||
_mm.nGuess++;
|
||||
|
||||
if (bulls == 4) _mm.won = true;
|
||||
else if (_mm.nGuess >= 10) _mm.over = true;
|
||||
|
||||
return true; // valid input processed — caller will redraw
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// HANGMAN
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::_hmStart() {
|
||||
_hm.word = _hmWords[random(_hmWordCount)];
|
||||
_hm.nWrong = 0;
|
||||
_hm.won = false;
|
||||
_hm.over = false;
|
||||
memset(_hm.guessed, 0, sizeof(_hm.guessed));
|
||||
_st = St::HANGMAN;
|
||||
_lineMode = false;
|
||||
_hmDraw();
|
||||
}
|
||||
|
||||
void SerialGames::_hmDraw() {
|
||||
_cls();
|
||||
Serial.print(
|
||||
BLD MAG
|
||||
" ╔══════════════════════════════════════╗\r\n"
|
||||
" ║ HANGMAN — Security Edition ║\r\n"
|
||||
" ╚══════════════════════════════════════╝\r\n"
|
||||
RST "\r\n"
|
||||
);
|
||||
|
||||
// Gallows — figure turns red at maximum wrong guesses
|
||||
uint8_t stage = (_hm.nWrong > 6) ? 6 : _hm.nWrong;
|
||||
for (uint8_t row = 0; row < 7; row++) {
|
||||
bool isBody = (row >= 2 && row <= 5);
|
||||
const char* col = (isBody && _hm.nWrong >= 6) ? RED : GRY;
|
||||
Serial.printf(" %s%s" RST "\r\n", col, GALLOWS[stage][row]);
|
||||
}
|
||||
Serial.print("\r\n");
|
||||
|
||||
// Word — revealed letters in green, blanks in white
|
||||
Serial.print(" ");
|
||||
uint8_t missing = 0;
|
||||
for (uint8_t i = 0; _hm.word[i]; i++) {
|
||||
uint8_t idx = (uint8_t)(_hm.word[i] - 'a');
|
||||
if (_hm.guessed[idx]) {
|
||||
Serial.printf(BLD GRN "%c " RST, _hm.word[i]);
|
||||
} else {
|
||||
Serial.print(BLD "_ " RST);
|
||||
missing++;
|
||||
}
|
||||
}
|
||||
Serial.print("\r\n\r\n");
|
||||
|
||||
// Wrong-guess list
|
||||
Serial.print(" Wrong: ");
|
||||
bool any = false;
|
||||
for (uint8_t i = 0; i < 26; i++) {
|
||||
if (!_hm.guessed[i]) continue;
|
||||
bool inWord = false;
|
||||
for (uint8_t j = 0; _hm.word[j]; j++)
|
||||
if (_hm.word[j] == (char)('a' + i)) { inWord = true; break; }
|
||||
if (!inWord) { Serial.printf(RED "%c " RST, 'a' + i); any = true; }
|
||||
}
|
||||
if (!any) Serial.print(GRY "none" RST);
|
||||
Serial.printf(" " GRY "(%u / 6 wrong)" RST "\r\n\r\n", _hm.nWrong);
|
||||
|
||||
// Win / lose check
|
||||
if (missing == 0 && !_hm.over) {
|
||||
_hm.won = true;
|
||||
_hm.over = true;
|
||||
}
|
||||
if (_hm.nWrong >= 6 && !_hm.won) {
|
||||
_hm.over = true;
|
||||
}
|
||||
|
||||
if (_hm.won) {
|
||||
Serial.print(BLD GRN " ✓ You survived! Well played, hacker.\r\n" RST);
|
||||
Serial.print(GRY " Press any key to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
if (_hm.over) {
|
||||
Serial.printf(BLD RED " ✗ Caught! The word was: " YLW "%s\r\n" RST, _hm.word);
|
||||
Serial.print(GRY " Press any key to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.print(" Guess a letter (Q = quit): ");
|
||||
}
|
||||
|
||||
void SerialGames::_hmOnChar(char c) {
|
||||
// After game over, any key returns to menu
|
||||
if (_hm.over) { _drawMenu(); _st = St::MENU; return; }
|
||||
|
||||
uint8_t idx = (uint8_t)(c - 'a');
|
||||
if (_hm.guessed[idx]) return; // already guessed — ignore silently
|
||||
_hm.guessed[idx] = true;
|
||||
|
||||
// Count miss
|
||||
bool hit = false;
|
||||
for (uint8_t i = 0; _hm.word[i]; i++)
|
||||
if (_hm.word[i] == c) { hit = true; break; }
|
||||
if (!hit) _hm.nWrong++;
|
||||
|
||||
_hmDraw();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MINESWEEPER
|
||||
// ================================================================
|
||||
|
||||
void SerialGames::_msStart() {
|
||||
memset(&_ms, 0, sizeof(_ms));
|
||||
_st = St::MINESWEEPER;
|
||||
_lineMode = true;
|
||||
_clearLine();
|
||||
_msDraw();
|
||||
}
|
||||
|
||||
void SerialGames::_msSeed(uint8_t safeR, uint8_t safeC) {
|
||||
_ms.seeded = true;
|
||||
uint8_t placed = 0;
|
||||
while (placed < MSN) {
|
||||
uint8_t r = (uint8_t)random(MSR);
|
||||
uint8_t c = (uint8_t)random(MSC);
|
||||
if (_ms.mine[r][c]) continue;
|
||||
// Keep a 1-cell safety radius around the first click
|
||||
if (abs((int)r - safeR) <= 1 && abs((int)c - safeC) <= 1) continue;
|
||||
_ms.mine[r][c] = true;
|
||||
placed++;
|
||||
}
|
||||
// Pre-compute neighbour mine counts
|
||||
for (uint8_t r = 0; r < MSR; r++)
|
||||
for (uint8_t c = 0; c < MSC; c++) {
|
||||
uint8_t n = 0;
|
||||
for (int8_t dr = -1; dr <= 1; dr++)
|
||||
for (int8_t dc = -1; dc <= 1; dc++) {
|
||||
int nr = r + dr, nc = c + dc;
|
||||
if (nr >= 0 && nr < MSR && nc >= 0 && nc < MSC && _ms.mine[nr][nc]) n++;
|
||||
}
|
||||
_ms.count[r][c] = n;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive flood-fill reveal — safe on an 8×8 grid
|
||||
void SerialGames::_msReveal(int r, int c) {
|
||||
if (r < 0 || r >= MSR || c < 0 || c >= MSC) return;
|
||||
if (_ms.revealed[r][c] || _ms.flagged[r][c]) return;
|
||||
if (_ms.mine[r][c]) { _ms.over = true; return; }
|
||||
_ms.revealed[r][c] = true;
|
||||
_ms.nRevealed++;
|
||||
if (_ms.count[r][c] == 0) {
|
||||
for (int8_t dr = -1; dr <= 1; dr++)
|
||||
for (int8_t dc = -1; dc <= 1; dc++)
|
||||
if (dr || dc) _msReveal(r + dr, c + dc);
|
||||
}
|
||||
}
|
||||
|
||||
void SerialGames::_msDraw() {
|
||||
_cls();
|
||||
|
||||
uint8_t flags = 0;
|
||||
for (uint8_t r = 0; r < MSR; r++)
|
||||
for (uint8_t c = 0; c < MSC; c++)
|
||||
if (_ms.flagged[r][c]) flags++;
|
||||
|
||||
Serial.print(
|
||||
BLD GRN
|
||||
" ╔══════════════════════════════════════╗\r\n"
|
||||
" ║ MINESWEEPER — 8×8, 10 mines ║\r\n"
|
||||
" ╚══════════════════════════════════════╝\r\n"
|
||||
RST "\r\n"
|
||||
);
|
||||
|
||||
// Column header + top border
|
||||
Serial.print(" 1 2 3 4 5 6 7 8\r\n");
|
||||
Serial.print(" +-----------------+\r\n");
|
||||
|
||||
for (uint8_t r = 0; r < MSR; r++) {
|
||||
Serial.printf(" %u | ", r + 1);
|
||||
for (uint8_t c = 0; c < MSC; c++) {
|
||||
if (_ms.flagged[r][c]) {
|
||||
Serial.print(YLW "F" RST " ");
|
||||
} else if (!_ms.revealed[r][c]) {
|
||||
// On game over, show all mine locations
|
||||
if (_ms.over && _ms.mine[r][c])
|
||||
Serial.print(RED "*" RST " ");
|
||||
else
|
||||
Serial.print(GRY "?" RST " ");
|
||||
} else if (_ms.count[r][c] == 0) {
|
||||
Serial.print(" "); // clear / empty cell
|
||||
} else {
|
||||
// Colour-code by danger: 1-2 cyan, 3-4 yellow, 5+ red
|
||||
const char* nc =
|
||||
(_ms.count[r][c] <= 2) ? CYN :
|
||||
(_ms.count[r][c] <= 4) ? YLW : RED;
|
||||
Serial.printf("%s%u" RST " ", nc, _ms.count[r][c]);
|
||||
}
|
||||
}
|
||||
Serial.print("|\r\n");
|
||||
}
|
||||
|
||||
Serial.print(" +-----------------+\r\n\r\n");
|
||||
|
||||
Serial.printf(" " BLD "Mines left: %d" RST
|
||||
GRY " (F=flag ?=hidden blank=safe)" RST "\r\n\r\n",
|
||||
(int)MSN - (int)flags);
|
||||
|
||||
if (_ms.won) {
|
||||
Serial.print(BLD GRN " ✓ Cleared! Every mine avoided.\r\n" RST);
|
||||
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
if (_ms.over) {
|
||||
Serial.print(BLD RED " ✗ BOOM! You hit a mine.\r\n" RST);
|
||||
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.print(
|
||||
" " GRY "r ROW COL" RST " = reveal "
|
||||
GRY "f ROW COL" RST " = flag/unflag "
|
||||
GRY "Q" RST " = quit\r\n"
|
||||
" > "
|
||||
);
|
||||
}
|
||||
|
||||
// Returns true if the screen needs a full redraw.
|
||||
bool SerialGames::_msOnLine() {
|
||||
// Quit (works mid-game and on win/lose screen)
|
||||
if (_line[0] == 'q' || _line[0] == 'Q') {
|
||||
_drawMenu();
|
||||
_st = St::MENU;
|
||||
_lineMode = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ms.won || _ms.over) return false;
|
||||
|
||||
char cmd = _line[0];
|
||||
if (cmd != 'r' && cmd != 'R' && cmd != 'f' && cmd != 'F') {
|
||||
Serial.print(RED " ✗ Unknown command. Use: r ROW COL or f ROW COL\r\n" RST " > ");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse two integers following the command letter
|
||||
const char* p = _line + 1;
|
||||
while (*p == ' ') p++;
|
||||
int row = 0;
|
||||
while (*p >= '0' && *p <= '9') row = row * 10 + (*p++ - '0');
|
||||
while (*p == ' ') p++;
|
||||
int col = 0;
|
||||
while (*p >= '0' && *p <= '9') col = col * 10 + (*p++ - '0');
|
||||
row--; col--; // convert 1-indexed user input to 0-indexed
|
||||
|
||||
if (row < 0 || row >= MSR || col < 0 || col >= MSC) {
|
||||
Serial.print(RED " ✗ Row and column must each be 1–8.\r\n" RST " > ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cmd == 'f' || cmd == 'F') {
|
||||
if (!_ms.revealed[row][col])
|
||||
_ms.flagged[row][col] = !_ms.flagged[row][col];
|
||||
} else {
|
||||
if (!_ms.seeded) _msSeed((uint8_t)row, (uint8_t)col);
|
||||
_msReveal(row, col);
|
||||
}
|
||||
|
||||
if (_ms.nRevealed == MSR * MSC - MSN) _ms.won = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// TEXT ADVENTURE
|
||||
// ================================================================
|
||||
void SerialGames::_advStart() {
|
||||
_st = St::ADVENTURE;
|
||||
_lineMode = true;
|
||||
_clearLine();
|
||||
_adv.begin();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// CONFIG — badge settings over serial CLI
|
||||
// ================================================================
|
||||
|
||||
static const char* _protoName(uint8_t p) {
|
||||
switch (p) {
|
||||
case IR_PROTO_NEC: return "NEC";
|
||||
case IR_PROTO_SAMSUNG: return "SAMSUNG";
|
||||
case IR_PROTO_SONY: return "SONY";
|
||||
case IR_PROTO_RC5: return "RC5";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static const char* _btnName(uint8_t i) {
|
||||
switch (i) {
|
||||
case 0: return "A";
|
||||
case 1: return "B";
|
||||
case 2: return "UP";
|
||||
case 3: return "DOWN";
|
||||
case 4: return "LEFT";
|
||||
case 5: return "RIGHT";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
uint8_t SerialGames::_cfgParseBtn(const char* s) {
|
||||
if (strcmp(s,"a")==0) return 0;
|
||||
if (strcmp(s,"b")==0) return 1;
|
||||
if (strcmp(s,"up")==0) return 2;
|
||||
if (strcmp(s,"down")==0) return 3;
|
||||
if (strcmp(s,"left")==0) return 4;
|
||||
if (strcmp(s,"right")==0) return 5;
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
uint8_t SerialGames::_cfgParseProto(const char* s) {
|
||||
if (strcmp(s,"nec")==0) return IR_PROTO_NEC;
|
||||
if (strcmp(s,"samsung")==0) return IR_PROTO_SAMSUNG;
|
||||
if (strcmp(s,"sony")==0) return IR_PROTO_SONY;
|
||||
if (strcmp(s,"rc5")==0) return IR_PROTO_RC5;
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
void SerialGames::_cfgStart() {
|
||||
_st = St::CONFIG;
|
||||
_lineMode = true;
|
||||
_clearLine();
|
||||
_cfgDraw();
|
||||
}
|
||||
|
||||
void SerialGames::_cfgDraw() {
|
||||
_cls();
|
||||
Serial.print(
|
||||
BLD CYN
|
||||
" ╔══════════════════════════════════════════════╗\r\n"
|
||||
" ║ BADGE CONFIG ║\r\n"
|
||||
" ╚══════════════════════════════════════════════╝\r\n"
|
||||
RST "\r\n"
|
||||
GRY
|
||||
" Commands (case-insensitive):\r\n"
|
||||
" " RST BLD "show" RST GRY " — print current settings\r\n"
|
||||
" " RST BLD "bright matrix <1-8>" RST GRY " — set matrix / LED animation depth\r\n"
|
||||
" " RST BLD "bright flash <1-8>" RST GRY " — set flashlight brightness\r\n"
|
||||
" " RST BLD "ir <btn> <proto> <addr> <cmd>" RST GRY "\r\n"
|
||||
" btn = A B UP DOWN LEFT RIGHT\r\n"
|
||||
" proto = NEC SAMSUNG SONY RC5\r\n"
|
||||
" addr = 0x… (16-bit hex)\r\n"
|
||||
" cmd = 0x… (8-bit hex)\r\n"
|
||||
" " RST BLD "default" RST GRY " — restore factory defaults\r\n"
|
||||
" " RST BLD "q / back" RST GRY " — return to main menu\r\n"
|
||||
RST "\r\n"
|
||||
" > "
|
||||
);
|
||||
}
|
||||
|
||||
void SerialGames::_cfgShowConfig() {
|
||||
Serial.print("\r\n");
|
||||
Serial.printf(" Matrix brightness : " BLD "%d" RST "/8\r\n", g_cfg.brightness);
|
||||
Serial.printf(" Flash brightness : " BLD "%d" RST "/8\r\n", g_cfg.flashBright);
|
||||
Serial.print("\r\n");
|
||||
Serial.print(" " BLD "Button Protocol Address Command\r\n" RST);
|
||||
Serial.print(GRY " ────────────────────────────────────\r\n" RST);
|
||||
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
|
||||
const IrCode& c = g_cfg.ir[i];
|
||||
Serial.printf(" %-6s %-8s 0x%04X 0x%02X\r\n",
|
||||
_btnName(i), _protoName(c.protocol),
|
||||
(unsigned)(irCodeAddr(c)), (unsigned)c.command);
|
||||
}
|
||||
Serial.print("\r\n > ");
|
||||
}
|
||||
|
||||
void SerialGames::_cfgOnLine() {
|
||||
// Lower-case
|
||||
char buf[64];
|
||||
uint8_t len = 0;
|
||||
while (_line[len] && len < 63) { buf[len] = _line[len]; len++; }
|
||||
buf[len] = '\0';
|
||||
for (uint8_t i = 0; i < len; i++)
|
||||
if (buf[i] >= 'A' && buf[i] <= 'Z') buf[i] += 32;
|
||||
|
||||
// Tokenise (up to 5 tokens)
|
||||
char* toks[5] = {nullptr,nullptr,nullptr,nullptr,nullptr};
|
||||
uint8_t tc = 0;
|
||||
char* p = buf;
|
||||
while (*p && tc < 5) {
|
||||
while (*p == ' ') p++;
|
||||
if (!*p) break;
|
||||
toks[tc++] = p;
|
||||
while (*p && *p != ' ') p++;
|
||||
if (*p) { *p = '\0'; p++; }
|
||||
}
|
||||
|
||||
if (tc == 0) { Serial.print(" > "); return; }
|
||||
|
||||
// q / back
|
||||
if (strcmp(toks[0],"q")==0 || strcmp(toks[0],"back")==0) {
|
||||
_st = St::MENU;
|
||||
_lineMode = false;
|
||||
_drawMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// help
|
||||
if (strcmp(toks[0],"help")==0) {
|
||||
_cfgDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// show
|
||||
if (strcmp(toks[0],"show")==0) {
|
||||
_cfgShowConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// default
|
||||
if (strcmp(toks[0],"default")==0) {
|
||||
g_cfg = BADGE_CONFIG_DEFAULT;
|
||||
configSave();
|
||||
Serial.print(" " GRN "Factory defaults restored and saved.\r\n" RST "\r\n > ");
|
||||
return;
|
||||
}
|
||||
|
||||
// bright matrix|flash <1-8>
|
||||
if (strcmp(toks[0],"bright")==0 && tc >= 3) {
|
||||
uint8_t val = (uint8_t)atoi(toks[2]);
|
||||
if (val < 1 || val > 8) {
|
||||
Serial.print(" " RED "Value must be 1-8.\r\n" RST " > ");
|
||||
return;
|
||||
}
|
||||
if (strcmp(toks[1],"matrix")==0) {
|
||||
g_cfg.brightness = val;
|
||||
configSaveBrightness();
|
||||
Serial.printf(" " GRN "Matrix brightness set to %d.\r\n" RST " > ", val);
|
||||
} else if (strcmp(toks[1],"flash")==0) {
|
||||
g_cfg.flashBright = val;
|
||||
configSaveFlash();
|
||||
Serial.printf(" " GRN "Flashlight brightness set to %d.\r\n" RST " > ", val);
|
||||
} else {
|
||||
Serial.print(" " RED "Unknown target. Use 'matrix' or 'flash'.\r\n" RST " > ");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ir <btn> <proto> <addr> <cmd>
|
||||
if (strcmp(toks[0],"ir")==0) {
|
||||
if (tc < 5) {
|
||||
Serial.print(" " RED "Usage: ir <btn> <proto> <addr> <cmd>\r\n" RST " > ");
|
||||
return;
|
||||
}
|
||||
uint8_t btn = _cfgParseBtn(toks[1]);
|
||||
uint8_t proto = _cfgParseProto(toks[2]);
|
||||
if (btn == 0xFF) {
|
||||
Serial.print(" " RED "Unknown button. Use A B UP DOWN LEFT RIGHT.\r\n" RST " > ");
|
||||
return;
|
||||
}
|
||||
if (proto == 0xFF) {
|
||||
Serial.print(" " RED "Unknown protocol. Use NEC SAMSUNG SONY RC5.\r\n" RST " > ");
|
||||
return;
|
||||
}
|
||||
uint32_t addr = (uint32_t)strtoul(toks[3], nullptr, 16);
|
||||
uint32_t cmd = (uint32_t)strtoul(toks[4], nullptr, 16);
|
||||
if (addr > 0xFFFF || cmd > 0xFF) {
|
||||
Serial.print(" " RED "Address must be ≤ 0xFFFF, command ≤ 0xFF.\r\n" RST " > ");
|
||||
return;
|
||||
}
|
||||
g_cfg.ir[btn].protocol = proto;
|
||||
g_cfg.ir[btn].addr_lo = (uint8_t)(addr & 0xFF);
|
||||
g_cfg.ir[btn].addr_hi = (uint8_t)(addr >> 8);
|
||||
g_cfg.ir[btn].command = (uint8_t)cmd;
|
||||
configSaveIr(btn);
|
||||
Serial.printf(" " GRN "%s → %s addr=0x%04lX cmd=0x%02lX — saved.\r\n" RST " > ",
|
||||
_btnName(btn), _protoName(proto), addr, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.print(" " DIM "Unrecognised command. Type 'help'.\r\n" RST " > ");
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// ================================================================
|
||||
// serial_games.h — Secfest 2026 Badge
|
||||
// ================================================================
|
||||
// Text-based games over USB-CDC serial.
|
||||
// Connect with PuTTY or Minicom; the terminal must support
|
||||
// VT100 / ANSI escape codes (colour + clear-screen).
|
||||
//
|
||||
// Usage in main.cpp:
|
||||
// #include "serial_games.h"
|
||||
// static SerialGames serialGames;
|
||||
// // in setup(): serialGames.begin();
|
||||
// // in loop(): serialGames.update();
|
||||
// ================================================================
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include "badge_config.h"
|
||||
#include "text_adventure.h"
|
||||
|
||||
class SerialGames {
|
||||
public:
|
||||
void begin();
|
||||
void update(); // call every loop() iteration — fully non-blocking
|
||||
|
||||
private:
|
||||
// ── top-level state ─────────────────────────────────────
|
||||
enum class St : uint8_t { IDLE, MENU, MASTERMIND, HANGMAN, MINESWEEPER, ADVENTURE, CONFIG };
|
||||
St _st = St::IDLE;
|
||||
bool _prevConn = false;
|
||||
|
||||
// ── shared line-input buffer ────────────────────────────
|
||||
// lineMode=true → buffer chars until Enter, then dispatch
|
||||
// lineMode=false → dispatch each printable char immediately
|
||||
bool _lineMode = false;
|
||||
char _line[32] = {};
|
||||
uint8_t _lineLen = 0;
|
||||
|
||||
void _clearLine();
|
||||
void _feedChar(char c); // line-mode byte handler
|
||||
void _charDispatch(char c); // char-mode byte handler
|
||||
|
||||
// ── screen helpers ──────────────────────────────────────
|
||||
static void _cls();
|
||||
void _drawMenu();
|
||||
|
||||
// ── MASTERMIND ──────────────────────────────────────────
|
||||
// Guess a secret 4-digit code (each digit 1-6) in ≤10 tries.
|
||||
// Feedback: ● (bull) = right digit, right place;
|
||||
// ○ (cow) = right digit, wrong place.
|
||||
struct MMState {
|
||||
uint8_t secret[4];
|
||||
uint8_t guess[10][4];
|
||||
uint8_t bulls[10];
|
||||
uint8_t cows[10];
|
||||
uint8_t nGuess;
|
||||
bool won, over;
|
||||
} _mm;
|
||||
|
||||
void _mmStart();
|
||||
bool _mmOnLine(); // returns true if a full redraw is needed
|
||||
void _mmDraw();
|
||||
|
||||
// ── HANGMAN ─────────────────────────────────────────────
|
||||
// Guess a security-themed word before the figure is complete
|
||||
// (≤6 wrong guesses). Single-keystroke input — no Enter needed.
|
||||
struct HMState {
|
||||
const char* word;
|
||||
bool guessed[26];
|
||||
uint8_t nWrong;
|
||||
bool won, over;
|
||||
} _hm;
|
||||
|
||||
static const char* const _hmWords[];
|
||||
static const uint8_t _hmWordCount;
|
||||
|
||||
void _hmStart();
|
||||
void _hmOnChar(char c);
|
||||
void _hmDraw();
|
||||
|
||||
// ── MINESWEEPER ─────────────────────────────────────────
|
||||
// 8 × 8 grid, 10 mines. First reveal is always safe.
|
||||
// Commands: r ROW COL (reveal)
|
||||
// f ROW COL (flag / unflag)
|
||||
// q (quit to menu)
|
||||
static constexpr uint8_t MSR = 8;
|
||||
static constexpr uint8_t MSC = 8;
|
||||
static constexpr uint8_t MSN = 10;
|
||||
|
||||
struct MSState {
|
||||
bool mine [MSR][MSC];
|
||||
bool revealed[MSR][MSC];
|
||||
bool flagged [MSR][MSC];
|
||||
uint8_t count [MSR][MSC];
|
||||
bool seeded, won, over;
|
||||
uint8_t nRevealed;
|
||||
} _ms;
|
||||
|
||||
void _msStart();
|
||||
void _msSeed(uint8_t safeR, uint8_t safeC);
|
||||
void _msReveal(int r, int c);
|
||||
bool _msOnLine(); // returns true if a full redraw is needed
|
||||
void _msDraw();
|
||||
|
||||
// ── TEXT ADVENTURE ──────────────────────────────────────
|
||||
// ZERO DAY — hacker infiltration story.
|
||||
// Full line-based input; adventure manages its own prompts.
|
||||
TextAdventure _adv;
|
||||
|
||||
void _advStart();
|
||||
|
||||
// ── CONFIG ──────────────────────────────────────────────
|
||||
// Line-mode CLI for editing badge_config (brightness, IR codes).
|
||||
// Commands (not case-sensitive):
|
||||
// show — print current config
|
||||
// bright matrix <1-8> — set matrix brightness
|
||||
// bright flash <1-8> — set flashlight brightness
|
||||
// ir <btn> <proto> <addr> <cmd>
|
||||
// btn = a b up down left right
|
||||
// proto = nec samsung sony rc5
|
||||
// addr = 0x… (16-bit hex)
|
||||
// cmd = 0x… (8-bit hex)
|
||||
// help — show commands
|
||||
// q / back — return to main menu
|
||||
void _cfgStart();
|
||||
void _cfgOnLine(); // called from _feedChar on Enter
|
||||
void _cfgDraw();
|
||||
void _cfgShowConfig();
|
||||
static uint8_t _cfgParseBtn(const char* s); // BI_* or 0xFF
|
||||
static uint8_t _cfgParseProto(const char* s);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
// ================================================================
|
||||
// text_adventure.h — Secfest 2026 Badge
|
||||
// ================================================================
|
||||
// ZERO DAY — A hacker text adventure.
|
||||
//
|
||||
// You are a security operative sent to extract stolen research data
|
||||
// from NovaCorp HQ before the building goes into full lockdown.
|
||||
// Uncover the truth, free a trapped contact, and vanish without
|
||||
// a trace — or blow your cover trying.
|
||||
//
|
||||
// Play over USB-CDC serial (PuTTY / Minicom).
|
||||
// All interaction is line-based: type a command and press Enter.
|
||||
//
|
||||
// Commands: go/move DIRECTION, look, examine ITEM,
|
||||
// take/get ITEM, drop ITEM, use ITEM,
|
||||
// use ITEM on TARGET, talk NPC,
|
||||
// inventory/inv, score, help, quit
|
||||
// ================================================================
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
class TextAdventure {
|
||||
public:
|
||||
void begin(); // reset to start state
|
||||
void update(); // call from SerialGames::update() — non-blocking
|
||||
void feedLine(const char* line); // dispatch a completed input line
|
||||
|
||||
bool isActive() const { return _active; }
|
||||
// Called when 'q'/'quit' should return to the SerialGames menu
|
||||
// — caller checks _quitToMenu and resets it after reading
|
||||
bool quitToMenu = false;
|
||||
|
||||
private:
|
||||
bool _active = false;
|
||||
|
||||
// ── Rooms ────────────────────────────────────────────────────────
|
||||
enum Room : uint8_t {
|
||||
PARKING=0, LOBBY, STAIRWELL, BREAKROOM, OFFICE,
|
||||
SERVERCLOSET, HALL3, MEETINGROOM, MANAGERSOFFICE, SECURITYOFFICE,
|
||||
DATACENTER, ROOF, BASEMENT, POWERROOM, TUNNEL,
|
||||
STREET, // win-condition room — not truly reachable as normal room
|
||||
ROOM_COUNT
|
||||
};
|
||||
|
||||
// ── Items ────────────────────────────────────────────────────────
|
||||
enum Item : uint8_t {
|
||||
BADGE=0, LOCKPICK, MULTITOOL, STICKYNOTE, USB_DRIVE,
|
||||
KEYCARD, MASTERKEY, EXTINGUISHER, THERMOS, DATA_CHIP,
|
||||
ROPE, PHOTO,
|
||||
ITEM_COUNT,
|
||||
ITEM_NONE = 0xFF
|
||||
};
|
||||
|
||||
// ── NPCs ─────────────────────────────────────────────────────────
|
||||
enum NPC : uint8_t { BOB=0, JANET, GHOST, NPC_COUNT };
|
||||
|
||||
// ── Flags ────────────────────────────────────────────────────────
|
||||
enum Flag : uint8_t {
|
||||
FL_BADGE_SHOWN=0, FL_GUARD_DISTRACT, FL_CAMERAS_OFF, FL_ALARM,
|
||||
FL_MANAGER_OPEN, FL_TERMINAL_USED, FL_GUARD_AWAKE, FL_DATA_COPIED,
|
||||
FL_GHOST_FOUND, FL_GHOST_FREED, FL_ROPE_USED, FL_POWER_CUT,
|
||||
FL_JANET_TALKED, FL_LAPTOP_OPEN,
|
||||
FLAG_COUNT
|
||||
};
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────
|
||||
Room _room = PARKING;
|
||||
uint8_t _inv[ITEM_COUNT]; // 1 = in inventory
|
||||
uint8_t _roomItem[ROOM_COUNT][ITEM_COUNT]; // items on floor per room
|
||||
bool _flags[FLAG_COUNT];
|
||||
uint8_t _bobTalks = 0; // how many times player talked to Glenn
|
||||
uint8_t _janetTalks = 0;
|
||||
uint8_t _ghostTalks = 0;
|
||||
int16_t _score = 0;
|
||||
bool _ended = false;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
void _printBanner();
|
||||
void _printHelp();
|
||||
void _printScore();
|
||||
void _printInventory();
|
||||
void _look();
|
||||
void _describeRoom();
|
||||
const char* _roomName(Room r);
|
||||
const char* _itemName(Item it);
|
||||
const char* _itemShort(Item it);
|
||||
const char* _npcName(NPC n);
|
||||
|
||||
bool _hasItem(Item it) const { return _inv[it]; }
|
||||
bool _roomHasItem(Item it) const { return _roomItem[_room][it]; }
|
||||
bool _flag(Flag f) const { return _flags[f]; }
|
||||
void _setFlag(Flag f) { if (!_flags[f]) { _flags[f] = true; } }
|
||||
void _addScore(int16_t pts) { _score += pts; }
|
||||
|
||||
Item _parseItem(const char* word) const;
|
||||
NPC _parseNPC(const char* word) const;
|
||||
Room _parseDir(const char* word) const; // returns ROOM_COUNT on fail
|
||||
|
||||
void _cmdGo(const char* dir);
|
||||
void _cmdLook();
|
||||
void _cmdExamine(const char* what);
|
||||
void _cmdTake(const char* what);
|
||||
void _cmdDrop(const char* what);
|
||||
void _cmdUse(const char* what, const char* on);
|
||||
void _cmdTalk(const char* who);
|
||||
|
||||
void _triggerEnding();
|
||||
void _endingGhostMode();
|
||||
void _endingOperative();
|
||||
void _endingBurned();
|
||||
|
||||
static void _p(const char* s); // Serial.print wrapper
|
||||
static void _pl(const char* s); // print + \r\n
|
||||
static void _pr(); // print \r\n
|
||||
};
|
||||
Reference in New Issue
Block a user