mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
Compare commits
7 Commits
df57e55171
...
a6713859b2
| Author | SHA1 | Date | |
|---|---|---|---|
| a6713859b2 | |||
| aaac0514c0 | |||
| 1b6e9a0f78 | |||
| c64f03a09f | |||
| 5ab8662332 | |||
| 6d8834d9b2 | |||
| 83ba775971 |
4
Firmware/AGENTS.md
Normal file
4
Firmware/AGENTS.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
To build:
|
||||||
|
(in zsh)
|
||||||
|
. "$HOME/esp/esp-idf/export.sh"
|
||||||
|
idf.py build
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
class Buzzer {
|
class Buzzer {
|
||||||
public:
|
public:
|
||||||
static Buzzer &get();
|
static Buzzer& get();
|
||||||
|
|
||||||
void init(); // call once from app_main
|
void init(); // call once from app_main
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ public:
|
|||||||
void beepRotate();
|
void beepRotate();
|
||||||
void beepMove();
|
void beepMove();
|
||||||
void beepLock();
|
void beepLock();
|
||||||
void beepLines(int lines); // 1..4 lines
|
void beepLines(int lines); // 1..4 lines
|
||||||
void beepLevelUp(int level); // after increment
|
void beepLevelUp(int level); // after increment
|
||||||
void beepGameOver();
|
void beepGameOver();
|
||||||
|
|
||||||
// Mute controls
|
// Mute controls
|
||||||
@@ -26,29 +26,29 @@ public:
|
|||||||
void toggleMuted();
|
void toggleMuted();
|
||||||
bool isMuted() const { return _muted; }
|
bool isMuted() const { return _muted; }
|
||||||
|
|
||||||
// Persistence
|
|
||||||
void loadState();
|
|
||||||
void saveState();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
|
struct Step {
|
||||||
|
uint32_t freq;
|
||||||
|
uint32_t dur_ms;
|
||||||
|
uint32_t gap_ms;
|
||||||
|
};
|
||||||
static constexpr int MAX_QUEUE = 16;
|
static constexpr int MAX_QUEUE = 16;
|
||||||
Step _queue[MAX_QUEUE]{};
|
Step _queue[MAX_QUEUE]{};
|
||||||
int _q_head = 0; // inclusive
|
int _q_head = 0; // inclusive
|
||||||
int _q_tail = 0; // exclusive
|
int _q_tail = 0; // exclusive
|
||||||
bool _running = false;
|
bool _running = false;
|
||||||
bool _in_gap = false;
|
bool _in_gap = false;
|
||||||
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
|
void* _timer = nullptr; // esp_timer_handle_t (opaque here)
|
||||||
bool _muted = false;
|
bool _muted = false;
|
||||||
|
|
||||||
Buzzer() = default;
|
Buzzer() = default;
|
||||||
void enqueue(const Step &s);
|
void enqueue(const Step& s);
|
||||||
bool empty() const { return _q_head == _q_tail; }
|
bool empty() const { return _q_head == _q_tail; }
|
||||||
Step &front() { return _queue[_q_head]; }
|
Step& front() { return _queue[_q_head]; }
|
||||||
void popFront();
|
void popFront();
|
||||||
void startNext();
|
void startNext();
|
||||||
void schedule(uint32_t ms, bool gapPhase);
|
void schedule(uint32_t ms, bool gapPhase);
|
||||||
void applyFreq(uint32_t freq);
|
void applyFreq(uint32_t freq);
|
||||||
static void timerCb(void *arg);
|
static void timerCb(void* arg);
|
||||||
void clearQueue() { _q_head = _q_tail = 0; }
|
void clearQueue() { _q_head = _q_tail = 0; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,30 +5,18 @@
|
|||||||
#include <driver/ledc.h>
|
#include <driver/ledc.h>
|
||||||
#include <esp_err.h>
|
#include <esp_err.h>
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
#include <nvs_flash.h>
|
|
||||||
#include <nvs.h>
|
|
||||||
|
|
||||||
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
|
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
|
||||||
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
|
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
|
||||||
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
|
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
|
||||||
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
|
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
|
||||||
|
|
||||||
Buzzer &Buzzer::get() {
|
Buzzer& Buzzer::get() {
|
||||||
static Buzzer b;
|
static Buzzer b;
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::init() {
|
void Buzzer::init() {
|
||||||
// Initialize NVS once (safe if already done)
|
|
||||||
static bool nvsInited = false;
|
|
||||||
if (!nvsInited) {
|
|
||||||
esp_err_t err = nvs_flash_init();
|
|
||||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
||||||
nvs_flash_erase();
|
|
||||||
nvs_flash_init();
|
|
||||||
}
|
|
||||||
nvsInited = true;
|
|
||||||
}
|
|
||||||
ledc_timer_config_t tcfg{};
|
ledc_timer_config_t tcfg{};
|
||||||
tcfg.speed_mode = LEDC_MODE;
|
tcfg.speed_mode = LEDC_MODE;
|
||||||
tcfg.timer_num = LEDC_TIMER;
|
tcfg.timer_num = LEDC_TIMER;
|
||||||
@@ -52,7 +40,6 @@ void Buzzer::init() {
|
|||||||
args.arg = this;
|
args.arg = this;
|
||||||
args.name = "buzz";
|
args.name = "buzz";
|
||||||
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
|
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
|
||||||
loadState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::applyFreq(uint32_t freq) {
|
void Buzzer::applyFreq(uint32_t freq) {
|
||||||
@@ -65,7 +52,7 @@ void Buzzer::applyFreq(uint32_t freq) {
|
|||||||
ledc_update_duty(LEDC_MODE, LEDC_CH);
|
ledc_update_duty(LEDC_MODE, LEDC_CH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::enqueue(const Step &s) {
|
void Buzzer::enqueue(const Step& s) {
|
||||||
int nextTail = (_q_tail + 1) % MAX_QUEUE;
|
int nextTail = (_q_tail + 1) % MAX_QUEUE;
|
||||||
if (nextTail == _q_head) { // full, drop oldest
|
if (nextTail == _q_head) { // full, drop oldest
|
||||||
_q_head = (_q_head + 1) % MAX_QUEUE;
|
_q_head = (_q_head + 1) % MAX_QUEUE;
|
||||||
@@ -87,21 +74,23 @@ void Buzzer::startNext() {
|
|||||||
}
|
}
|
||||||
_running = true;
|
_running = true;
|
||||||
_in_gap = false;
|
_in_gap = false;
|
||||||
Step &s = front();
|
Step& s = front();
|
||||||
applyFreq(s.freq);
|
applyFreq(s.freq);
|
||||||
schedule(s.dur_ms, false);
|
schedule(s.dur_ms, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
|
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
|
||||||
if (!_timer) return;
|
if (!_timer)
|
||||||
|
return;
|
||||||
_in_gap = gapPhase;
|
_in_gap = gapPhase;
|
||||||
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
||||||
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
|
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t) ms * 1000ULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::timerCb(void *arg) {
|
void Buzzer::timerCb(void* arg) {
|
||||||
auto *self = static_cast<Buzzer*>(arg);
|
auto* self = static_cast<Buzzer*>(arg);
|
||||||
if (!self) return;
|
if (!self)
|
||||||
|
return;
|
||||||
if (self->_in_gap) {
|
if (self->_in_gap) {
|
||||||
self->popFront();
|
self->popFront();
|
||||||
self->startNext();
|
self->startNext();
|
||||||
@@ -109,7 +98,7 @@ void Buzzer::timerCb(void *arg) {
|
|||||||
}
|
}
|
||||||
// Tone finished
|
// Tone finished
|
||||||
if (!self->empty()) {
|
if (!self->empty()) {
|
||||||
auto &s = self->front();
|
auto& s = self->front();
|
||||||
if (s.gap_ms) {
|
if (s.gap_ms) {
|
||||||
self->applyFreq(0);
|
self->applyFreq(0);
|
||||||
self->schedule(s.gap_ms, true);
|
self->schedule(s.gap_ms, true);
|
||||||
@@ -121,7 +110,8 @@ void Buzzer::timerCb(void *arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
|
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
|
||||||
if (_muted) return; // ignore while muted
|
if (_muted)
|
||||||
|
return; // ignore while muted
|
||||||
Step s{freq, duration_ms, gap_ms};
|
Step s{freq, duration_ms, gap_ms};
|
||||||
enqueue(s);
|
enqueue(s);
|
||||||
if (!_running)
|
if (!_running)
|
||||||
@@ -149,7 +139,8 @@ void Buzzer::beepGameOver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::setMuted(bool m) {
|
void Buzzer::setMuted(bool m) {
|
||||||
if (m == _muted) return;
|
if (m == _muted)
|
||||||
|
return;
|
||||||
_muted = m;
|
_muted = m;
|
||||||
if (_muted) {
|
if (_muted) {
|
||||||
clearQueue();
|
clearQueue();
|
||||||
@@ -164,28 +155,6 @@ void Buzzer::setMuted(bool m) {
|
|||||||
tone(1500, 40, 10);
|
tone(1500, 40, 10);
|
||||||
tone(1900, 60, 0);
|
tone(1900, 60, 0);
|
||||||
}
|
}
|
||||||
saveState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buzzer::toggleMuted() { setMuted(!_muted); }
|
void Buzzer::toggleMuted() { setMuted(!_muted); }
|
||||||
|
|
||||||
void Buzzer::loadState() {
|
|
||||||
nvs_handle_t h;
|
|
||||||
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
|
|
||||||
uint8_t v = 0;
|
|
||||||
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
|
|
||||||
_muted = (v != 0);
|
|
||||||
}
|
|
||||||
nvs_close(h);
|
|
||||||
}
|
|
||||||
if (_muted) applyFreq(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Buzzer::saveState() {
|
|
||||||
nvs_handle_t h;
|
|
||||||
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
|
|
||||||
nvs_set_u8(h, "mute", _muted ? 1 : 0);
|
|
||||||
nvs_commit(h);
|
|
||||||
nvs_close(h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
248
Firmware/ghettoprof.sh
Executable file
248
Firmware/ghettoprof.sh
Executable file
@@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# parallel-pc-profile.sh — parallel symbol resolver + optional annotated disassembly
|
||||||
|
# Supports C++ demangling, LLVM disassembler, and optional no-inlines aggregation (symbol-table based).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./parallel-pc-profile.sh [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ANNOTATE=0
|
||||||
|
JOBS=""
|
||||||
|
NO_INLINES=0
|
||||||
|
|
||||||
|
# ---- args ----
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-j) JOBS="$2"; shift 2 ;;
|
||||||
|
--annotate) ANNOTATE=1; shift ;;
|
||||||
|
--no-inlines) NO_INLINES=1; shift ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ $# -lt 2 ]] && usage
|
||||||
|
ELF="$1"
|
||||||
|
PCS_IN="$2"
|
||||||
|
|
||||||
|
[[ ! -f "$ELF" ]] && { echo "ELF not found: $ELF" >&2; exit 2; }
|
||||||
|
[[ ! -f "$PCS_IN" ]] && { echo "PC log not found: $PCS_IN" >&2; exit 3; }
|
||||||
|
|
||||||
|
# ---- tools ----
|
||||||
|
ADDR2LINE=""
|
||||||
|
for t in llvm-addr2line eu-addr2line riscv32-esp-elf-addr2line xtensa-esp32-elf-addr2line addr2line; do
|
||||||
|
if command -v "$t" >/dev/null 2>&1; then ADDR2LINE="$t"; break; fi
|
||||||
|
done
|
||||||
|
[[ -z "$ADDR2LINE" ]] && { echo "No addr2line found"; exit 4; }
|
||||||
|
|
||||||
|
if command -v llvm-objdump >/dev/null 2>&1; then
|
||||||
|
OBJDUMP="llvm-objdump"
|
||||||
|
else
|
||||||
|
for t in riscv32-esp-elf-objdump xtensa-esp32-elf-objdump objdump; do
|
||||||
|
if command -v "$t" >/dev/null 2>&1; then OBJDUMP="$t"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[[ -z "${OBJDUMP:-}" ]] && { echo "No objdump found"; exit 5; }
|
||||||
|
|
||||||
|
if command -v llvm-nm >/dev/null 2>&1; then
|
||||||
|
NM="llvm-nm"
|
||||||
|
elif command -v nm >/dev/null 2>&1; then
|
||||||
|
NM="nm"
|
||||||
|
else
|
||||||
|
NM=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v c++filt >/dev/null 2>&1; then
|
||||||
|
CPPFILT="c++filt"
|
||||||
|
elif command -v llvm-cxxfilt >/dev/null 2>&1; then
|
||||||
|
CPPFILT="llvm-cxxfilt"
|
||||||
|
else
|
||||||
|
CPPFILT=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- cores ----
|
||||||
|
if [[ -z "$JOBS" ]]; then
|
||||||
|
if command -v nproc >/dev/null 2>&1; then JOBS=$(nproc)
|
||||||
|
elif [[ "$OSTYPE" == "darwin"* ]]; then JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||||
|
else JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
(( JOBS = JOBS > 1 ? JOBS - 1 : 1 ))
|
||||||
|
echo ">> Using $JOBS parallel jobs"
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
# ---- extract PCs ----
|
||||||
|
grep -aoE '0x[0-9a-fA-F]+' "$PCS_IN" | tr 'A-F' 'a-f' | sort | uniq -c >"$TMP/pc_counts.txt" || true
|
||||||
|
awk '{print $2}' "$TMP/pc_counts.txt" >"$TMP/addrs.txt"
|
||||||
|
[[ ! -s "$TMP/addrs.txt" ]] && { echo "No addresses found"; exit 5; }
|
||||||
|
|
||||||
|
# ---- parallel addr2line (live PC -> function to stderr) ----
|
||||||
|
CHUNK=400
|
||||||
|
split -l "$CHUNK" "$TMP/addrs.txt" "$TMP/chunk."
|
||||||
|
|
||||||
|
find "$TMP" -name 'chunk.*' -type f -print0 \
|
||||||
|
| xargs -0 -I{} -n1 -P "$JOBS" bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
ADDR2LINE="$1"; ELF="$2"; CHUNK="$3"; CPP="$4"
|
||||||
|
OUT="${CHUNK}.sym"
|
||||||
|
"$ADDR2LINE" -a -f -e "$ELF" $(cat "$CHUNK") \
|
||||||
|
| tee "$OUT" \
|
||||||
|
| awk '"'"'NR%3==1{a=$0;next} NR%3==2{f=$0; printf "%s\t%s\n",a,f; next} NR%3==0{next}'"'"' \
|
||||||
|
| { if [[ -n "$CPP" ]]; then "$CPP"; else cat; fi; } 1>&2
|
||||||
|
' _ "$ADDR2LINE" "$ELF" {} "$CPPFILT"
|
||||||
|
|
||||||
|
# Collate triplets
|
||||||
|
cat "$TMP"/chunk.*.sym > "$TMP/symbols.raw"
|
||||||
|
|
||||||
|
# ---- parse 3-line addr/func/file:line ----
|
||||||
|
# Normalize leading zeros in addresses so joins match grep-extracted PCs
|
||||||
|
awk 'NR%3==1{a=$0; sub(/^0x0+/, "0x", a); next} NR%3==2{f=$0; next} NR%3==0{print a "\t" f "\t" $0}' \
|
||||||
|
"$TMP/symbols.raw" >"$TMP/map.tsv"
|
||||||
|
|
||||||
|
# ---- counts: addr -> samplecount ----
|
||||||
|
awk '{printf "%s\t%s\n",$2,$1}' "$TMP/pc_counts.txt" | sort -k1,1 >"$TMP/counts.tsv"
|
||||||
|
|
||||||
|
# ---- choose mapping: default (addr2line; may show inlined names) vs --no-inlines (symbol-table) ----
|
||||||
|
DEFAULT_ADDR_FUNC="$TMP/addr_func.tsv"
|
||||||
|
cut -f1,2 "$TMP/map.tsv" | sort -k1,1 >"$DEFAULT_ADDR_FUNC"
|
||||||
|
|
||||||
|
if [[ "$NO_INLINES" == "1" ]]; then
|
||||||
|
if [[ -z "$NM" ]]; then
|
||||||
|
echo "WARNING: nm/llvm-nm not found; falling back to inline-aware mapping." >&2
|
||||||
|
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||||
|
else
|
||||||
|
echo ">> Building symbol table for no-inlines mapping..."
|
||||||
|
# Create sorted function symbols: hexaddr\tname (demangled if possible afterwards)
|
||||||
|
# Try llvm-nm format first; fall back to generic nm.
|
||||||
|
if [[ "$NM" == "llvm-nm" ]]; then
|
||||||
|
# llvm-nm -n --defined-only emits: ADDRESS TYPE NAME
|
||||||
|
"$NM" -n --defined-only "$ELF" \
|
||||||
|
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw"
|
||||||
|
else
|
||||||
|
# generic nm -n emits: ADDRESS TYPE NAME (varies a bit across platforms)
|
||||||
|
"$NM" -n --defined-only "$ELF" 2>/dev/null \
|
||||||
|
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
|
||||||
|
# macOS nm might output different columns; handle common alt layout:
|
||||||
|
if [[ ! -s "$TMP/syms.raw" ]]; then
|
||||||
|
"$NM" -n "$ELF" 2>/dev/null | awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CPPFILT" && -s "$TMP/syms.raw" ]]; then
|
||||||
|
"$CPPFILT" < "$TMP/syms.raw" > "$TMP/syms.dem.raw" || cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
|
||||||
|
else
|
||||||
|
cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Normalize addresses and sort ascending
|
||||||
|
awk '{addr=$1; sub(/^0x0+/, "0x", addr); print addr "\t" $2}' "$TMP/syms.dem.raw" \
|
||||||
|
| awk 'NF' \
|
||||||
|
| sort -k1,1 > "$TMP/syms.tsv"
|
||||||
|
|
||||||
|
if [[ ! -s "$TMP/syms.tsv" ]]; then
|
||||||
|
echo "WARNING: no text symbols found; falling back to inline-aware mapping." >&2
|
||||||
|
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||||
|
else
|
||||||
|
# Map each PC to the *containing* function: last symbol with addr <= PC.
|
||||||
|
# Both syms.tsv and addrs.txt are sorted asc → single pass merge.
|
||||||
|
awk '
|
||||||
|
function hex2num(h, x, n,i,c) {
|
||||||
|
gsub(/^0x/,"",h); n=0
|
||||||
|
for(i=1;i<=length(h);i++){ c=substr(h,i,1)
|
||||||
|
x = index("0123456789abcdef", tolower(c)) - 1
|
||||||
|
if (x<0) x = index("0123456789ABCDEF", c) - 1
|
||||||
|
n = n*16 + x
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
BEGIN {
|
||||||
|
# preload symbols
|
||||||
|
while ((getline < ARGV[1]) > 0) {
|
||||||
|
saddr[NSYM]=$1; sname[NSYM]=$2; NSYM++
|
||||||
|
}
|
||||||
|
# load PCs
|
||||||
|
while ((getline < ARGV[2]) > 0) {
|
||||||
|
pc[NPC]=$0; NPC++
|
||||||
|
}
|
||||||
|
# pointers
|
||||||
|
si=0
|
||||||
|
for (i=0; i<NPC; i++) {
|
||||||
|
p=pc[i]; pn=hex2num(p)
|
||||||
|
# advance symbol index while next symbol start <= pc
|
||||||
|
while (si+1<NSYM && hex2num(saddr[si+1]) <= pn) si++
|
||||||
|
# output mapping: p -> sname[si] (if any)
|
||||||
|
if (si<NSYM && hex2num(saddr[si]) <= pn)
|
||||||
|
printf "%s\t%s\n", p, sname[si]
|
||||||
|
else
|
||||||
|
printf "%s\t<unknown>\n", p
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
' "$TMP/syms.tsv" "$TMP/addrs.txt" \
|
||||||
|
| sort -k1,1 > "$TMP/addr_func.noinline.tsv"
|
||||||
|
|
||||||
|
ADDR_FUNC_FILE="$TMP/addr_func.noinline.tsv"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- aggregate to hot functions ----
|
||||||
|
join -t $'\t' -a1 -e "<unknown>" -o 1.2,2.2 "$TMP/counts.tsv" "$ADDR_FUNC_FILE" \
|
||||||
|
| awk -F'\t' '{s[$2]+=$1} END{for(k in s) printf "%8d %s\n",s[k],k}' \
|
||||||
|
| sort -nr > "$TMP/hot.txt"
|
||||||
|
|
||||||
|
# ---- demangle final hot list (if available) ----
|
||||||
|
if [[ -n "$CPPFILT" ]]; then
|
||||||
|
"$CPPFILT" < "$TMP/hot.txt" > hot_functions.txt
|
||||||
|
else
|
||||||
|
cp "$TMP/hot.txt" hot_functions.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Top 50 hot functions ==="
|
||||||
|
head -50 hot_functions.txt
|
||||||
|
echo "Full list in: hot_functions.txt"
|
||||||
|
|
||||||
|
# ---- annotated source+assembly (optional) ----
|
||||||
|
if (( ANNOTATE )); then
|
||||||
|
echo ">> Generating annotated source+assembly..."
|
||||||
|
awk '{printf "%s %s\n",$2,$1}' "$TMP/pc_counts.txt" >"$TMP/count.map"
|
||||||
|
|
||||||
|
if [[ "$OBJDUMP" == "llvm-objdump" ]]; then
|
||||||
|
# Portable across llvm-objdump versions
|
||||||
|
"$OBJDUMP" --source -l --demangle -d "$ELF" >"$TMP/disasm.txt"
|
||||||
|
else
|
||||||
|
"$OBJDUMP" -S -C -l -d "$ELF" >"$TMP/disasm.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Overlay hit counts onto the disassembly
|
||||||
|
awk -v counts="$TMP/count.map" '
|
||||||
|
BEGIN {
|
||||||
|
while ((getline < counts) > 0) {
|
||||||
|
addr=$1; cnt=$2
|
||||||
|
gsub(/^0x/,"",addr)
|
||||||
|
map[addr]=cnt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^[[:space:]]*[0-9a-f]+:/ {
|
||||||
|
split($1,a,":"); key=a[1]
|
||||||
|
if (key in map)
|
||||||
|
printf("%-12s %6d | %s\n", $1, map[key], substr($0, index($0,$2)))
|
||||||
|
else
|
||||||
|
print $0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' "$TMP/disasm.txt" > annotated.S
|
||||||
|
|
||||||
|
echo "Annotated source + assembly written to: annotated.S"
|
||||||
|
echo "Tip: less -R annotated.S"
|
||||||
|
fi
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
||||||
|
|
||||||
#include "cardboy/backend/esp_backend.hpp"
|
|
||||||
#include "cardboy/apps/clock_app.hpp"
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
#include "cardboy/apps/gameboy_app.hpp"
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
#include "cardboy/apps/menu_app.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
|
#include "cardboy/apps/settings_app.hpp"
|
||||||
#include "cardboy/apps/tetris_app.hpp"
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
|
#include "cardboy/backend/esp_backend.hpp"
|
||||||
#include "cardboy/sdk/app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
#include "cardboy/sdk/persistent_settings.hpp"
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/task.h"
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_pm.h"
|
#include "esp_pm.h"
|
||||||
#include "esp_sleep.h"
|
#include "esp_sleep.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -19,9 +22,9 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <span>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -53,11 +56,11 @@ constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
|
|||||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
|
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
|
||||||
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
|
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
|
||||||
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
|
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
|
||||||
constexpr uint32_t kStatsTaskStack = 4096;
|
constexpr uint32_t kStatsTaskStack = 4096;
|
||||||
constexpr char kStatsTaskName[] = "TaskStats";
|
constexpr char kStatsTaskName[] = "TaskStats";
|
||||||
|
|
||||||
struct TaskRuntimeSample {
|
struct TaskRuntimeSample {
|
||||||
TaskHandle_t handle;
|
TaskHandle_t handle;
|
||||||
@@ -65,11 +68,11 @@ struct TaskRuntimeSample {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct TaskUsageRow {
|
struct TaskUsageRow {
|
||||||
std::string name;
|
std::string name;
|
||||||
uint64_t delta;
|
uint64_t delta;
|
||||||
UBaseType_t priority;
|
UBaseType_t priority;
|
||||||
uint32_t stackHighWaterBytes;
|
uint32_t stackHighWaterBytes;
|
||||||
bool isIdle;
|
bool isIdle;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
|
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
|
||||||
@@ -79,7 +82,7 @@ struct TaskUsageRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void task_usage_monitor(void*) {
|
void task_usage_monitor(void*) {
|
||||||
static constexpr char tag[] = "TaskUsage";
|
static constexpr char tag[] = "TaskUsage";
|
||||||
std::vector<TaskRuntimeSample> lastSamples;
|
std::vector<TaskRuntimeSample> lastSamples;
|
||||||
uint32_t lastTotal = 0;
|
uint32_t lastTotal = 0;
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ void task_usage_monitor(void*) {
|
|||||||
|
|
||||||
std::vector<TaskStatus_t> statusBuffer(taskCount);
|
std::vector<TaskStatus_t> statusBuffer(taskCount);
|
||||||
uint32_t totalRuntime = 0;
|
uint32_t totalRuntime = 0;
|
||||||
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
|
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
|
||||||
if (captured == 0)
|
if (captured == 0)
|
||||||
continue;
|
continue;
|
||||||
statusBuffer.resize(captured);
|
statusBuffer.resize(captured);
|
||||||
@@ -118,8 +121,8 @@ void task_usage_monitor(void*) {
|
|||||||
std::vector<TaskUsageRow> rows;
|
std::vector<TaskUsageRow> rows;
|
||||||
rows.reserve(statusBuffer.size());
|
rows.reserve(statusBuffer.size());
|
||||||
|
|
||||||
uint64_t idleDelta = 0;
|
uint64_t idleDelta = 0;
|
||||||
uint64_t activeDelta = 0;
|
uint64_t activeDelta = 0;
|
||||||
uint64_t accountedDelta = 0;
|
uint64_t accountedDelta = 0;
|
||||||
|
|
||||||
for (const auto& status: statusBuffer) {
|
for (const auto& status: statusBuffer) {
|
||||||
@@ -128,18 +131,18 @@ void task_usage_monitor(void*) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
|
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
|
||||||
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
|
const uint64_t taskDelta =
|
||||||
|
(it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
|
||||||
|
|
||||||
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
||||||
|
|
||||||
TaskUsageRow row{
|
TaskUsageRow row{.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
|
||||||
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
|
.delta = taskDelta,
|
||||||
.delta = taskDelta,
|
.priority = status.uxCurrentPriority,
|
||||||
.priority = status.uxCurrentPriority,
|
.stackHighWaterBytes =
|
||||||
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
|
static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
|
||||||
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
|
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
|
||||||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
|
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)};
|
||||||
};
|
|
||||||
|
|
||||||
rows.push_back(std::move(row));
|
rows.push_back(std::move(row));
|
||||||
|
|
||||||
@@ -156,9 +159,8 @@ void task_usage_monitor(void*) {
|
|||||||
if (rows.empty())
|
if (rows.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
|
std::sort(rows.begin(), rows.end(),
|
||||||
return a.delta > b.delta;
|
[](const TaskUsageRow& a, const TaskUsageRow& b) { return a.delta > b.delta; });
|
||||||
});
|
|
||||||
|
|
||||||
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
|
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
|
||||||
|
|
||||||
@@ -181,14 +183,20 @@ void task_usage_monitor(void*) {
|
|||||||
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
|
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
|
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag, (activeDelta * 100.0) / static_cast<double>(totalDelta),
|
||||||
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
|
idlePct);
|
||||||
|
|
||||||
|
const uint32_t heapFree = esp_get_free_heap_size();
|
||||||
|
const uint32_t heapMinimum = esp_get_minimum_free_heap_size();
|
||||||
|
std::printf("[%s] Heap free %lu B | Min free %lu B\n", tag, static_cast<unsigned long>(heapFree),
|
||||||
|
static_cast<unsigned long>(heapMinimum));
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void start_task_usage_monitor() {
|
void start_task_usage_monitor() {
|
||||||
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
|
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr,
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -197,15 +205,6 @@ inline void start_task_usage_monitor() {}
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern "C" void app_main() {
|
extern "C" void app_main() {
|
||||||
#ifdef CONFIG_PM_ENABLE
|
|
||||||
// const esp_pm_config_t pm_config = {
|
|
||||||
// .max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
|
|
||||||
// .min_freq_mhz = 16,
|
|
||||||
// .light_sleep_enable = true};
|
|
||||||
// ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
|
|
||||||
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
|
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
|
||||||
|
|
||||||
static cardboy::backend::esp::EspRuntime runtime;
|
static cardboy::backend::esp::EspRuntime runtime;
|
||||||
@@ -215,12 +214,30 @@ extern "C" void app_main() {
|
|||||||
cardboy::sdk::AppSystem system(context);
|
cardboy::sdk::AppSystem system(context);
|
||||||
context.system = &system;
|
context.system = &system;
|
||||||
|
|
||||||
|
const cardboy::sdk::PersistentSettings persistentSettings =
|
||||||
|
cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||||
|
if (auto* buzzer = context.buzzer())
|
||||||
|
buzzer->setMuted(persistentSettings.mute);
|
||||||
|
|
||||||
|
#ifdef CONFIG_PM_ENABLE
|
||||||
|
if (persistentSettings.autoLightSleep) {
|
||||||
|
const esp_pm_config_t pm_config = {
|
||||||
|
.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
|
||||||
|
.min_freq_mhz = 16,
|
||||||
|
.light_sleep_enable = true,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||||
|
#endif
|
||||||
|
|
||||||
system.registerApp(apps::createMenuAppFactory());
|
system.registerApp(apps::createMenuAppFactory());
|
||||||
|
system.registerApp(apps::createSettingsAppFactory());
|
||||||
system.registerApp(apps::createClockAppFactory());
|
system.registerApp(apps::createClockAppFactory());
|
||||||
system.registerApp(apps::createTetrisAppFactory());
|
system.registerApp(apps::createTetrisAppFactory());
|
||||||
system.registerApp(apps::createGameboyAppFactory());
|
system.registerApp(apps::createGameboyAppFactory());
|
||||||
|
|
||||||
start_task_usage_monitor();
|
// start_task_usage_monitor();
|
||||||
|
|
||||||
system.run();
|
system.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
|||||||
|
|
||||||
add_subdirectory(menu)
|
add_subdirectory(menu)
|
||||||
add_subdirectory(clock)
|
add_subdirectory(clock)
|
||||||
|
add_subdirectory(settings)
|
||||||
add_subdirectory(gameboy)
|
add_subdirectory(gameboy)
|
||||||
add_subdirectory(tetris)
|
add_subdirectory(tetris)
|
||||||
|
|||||||
9
Firmware/sdk/apps/settings/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/settings/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
target_sources(cardboy_apps
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/settings_app.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace apps {
|
||||||
|
|
||||||
|
inline constexpr char kSettingsAppName[] = "Settings";
|
||||||
|
|
||||||
|
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory();
|
||||||
|
|
||||||
|
} // namespace apps
|
||||||
198
Firmware/sdk/apps/settings/src/settings_app.cpp
Normal file
198
Firmware/sdk/apps/settings/src/settings_app.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#include "cardboy/apps/settings_app.hpp"
|
||||||
|
|
||||||
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
|
#include "cardboy/sdk/persistent_settings.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace apps {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using cardboy::sdk::AppContext;
|
||||||
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
|
|
||||||
|
enum class SettingOption {
|
||||||
|
Sound,
|
||||||
|
AutoLightSleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array<SettingOption, 2> kOptions = {
|
||||||
|
SettingOption::Sound,
|
||||||
|
SettingOption::AutoLightSleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettingsApp final : public cardboy::sdk::IApp {
|
||||||
|
public:
|
||||||
|
explicit SettingsApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
||||||
|
|
||||||
|
void onStart() override {
|
||||||
|
loadSettings();
|
||||||
|
dirty = true;
|
||||||
|
renderIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||||
|
if (event.type != cardboy::sdk::AppEventType::Button)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& current = event.button.current;
|
||||||
|
const auto& previous = event.button.previous;
|
||||||
|
|
||||||
|
const bool previousAvailable = buzzerAvailable;
|
||||||
|
syncBuzzerState();
|
||||||
|
if (previousAvailable != buzzerAvailable)
|
||||||
|
dirty = true;
|
||||||
|
|
||||||
|
if (current.b && !previous.b) {
|
||||||
|
context.requestAppSwitchByName(kMenuAppName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool moved = false;
|
||||||
|
if (current.down && !previous.down) {
|
||||||
|
moveSelection(+1);
|
||||||
|
moved = true;
|
||||||
|
} else if (current.up && !previous.up) {
|
||||||
|
moveSelection(-1);
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool togglePressed = (current.a && !previous.a) || (current.select && !previous.select) ||
|
||||||
|
(current.start && !previous.start);
|
||||||
|
if (togglePressed)
|
||||||
|
handleToggle();
|
||||||
|
|
||||||
|
if (moved)
|
||||||
|
dirty = true;
|
||||||
|
|
||||||
|
renderIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
AppContext& context;
|
||||||
|
Framebuffer& framebuffer;
|
||||||
|
|
||||||
|
bool buzzerAvailable = false;
|
||||||
|
cardboy::sdk::PersistentSettings settings{};
|
||||||
|
std::size_t selectedIndex = 0;
|
||||||
|
bool dirty = false;
|
||||||
|
|
||||||
|
void loadSettings() {
|
||||||
|
settings = cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||||
|
syncBuzzerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void syncBuzzerState() {
|
||||||
|
auto* buzzer = context.buzzer();
|
||||||
|
buzzerAvailable = (buzzer != nullptr);
|
||||||
|
if (!buzzer)
|
||||||
|
return;
|
||||||
|
if (buzzer->isMuted() != settings.mute)
|
||||||
|
buzzer->setMuted(settings.mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveSelection(int delta) {
|
||||||
|
const int count = static_cast<int>(kOptions.size());
|
||||||
|
if (count == 0)
|
||||||
|
return;
|
||||||
|
const int current = static_cast<int>(selectedIndex);
|
||||||
|
int next = (current + delta) % count;
|
||||||
|
if (next < 0)
|
||||||
|
next += count;
|
||||||
|
selectedIndex = static_cast<std::size_t>(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleToggle() {
|
||||||
|
switch (kOptions[selectedIndex]) {
|
||||||
|
case SettingOption::Sound:
|
||||||
|
toggleSound();
|
||||||
|
break;
|
||||||
|
case SettingOption::AutoLightSleep:
|
||||||
|
toggleAutoLightSleep();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleSound() {
|
||||||
|
if (!buzzerAvailable)
|
||||||
|
return;
|
||||||
|
settings.mute = !settings.mute;
|
||||||
|
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
|
||||||
|
syncBuzzerState();
|
||||||
|
if (!settings.mute) {
|
||||||
|
if (auto* buzzer = context.buzzer())
|
||||||
|
buzzer->beepMove();
|
||||||
|
}
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleAutoLightSleep() {
|
||||||
|
settings.autoLightSleep = !settings.autoLightSleep;
|
||||||
|
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 1) {
|
||||||
|
const int width = font16x8::measureText(text, scale, letterSpacing);
|
||||||
|
const int x = (fb.width() - width) / 2;
|
||||||
|
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawOptionRow(int row, std::string_view label, std::string_view value, bool selected) {
|
||||||
|
std::string prefix = selected ? "> " : " ";
|
||||||
|
std::string line = prefix;
|
||||||
|
line.append(label);
|
||||||
|
line.append(": ");
|
||||||
|
line.append(value);
|
||||||
|
const int x = 24;
|
||||||
|
const int y = 56 + row * 24;
|
||||||
|
font16x8::drawText(framebuffer, x, y, line, 1, true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderIfNeeded() {
|
||||||
|
if (!dirty)
|
||||||
|
return;
|
||||||
|
dirty = false;
|
||||||
|
|
||||||
|
framebuffer.frameReady();
|
||||||
|
framebuffer.clear(false);
|
||||||
|
|
||||||
|
drawCenteredText(framebuffer, 24, "SETTINGS", 1, 1);
|
||||||
|
|
||||||
|
const std::string soundValue = buzzerAvailable ? (settings.mute ? "OFF" : "ON") : "N/A";
|
||||||
|
drawOptionRow(0, "SOUND", soundValue, selectedIndex == 0);
|
||||||
|
|
||||||
|
const std::string lightSleepValue = settings.autoLightSleep ? "ON" : "OFF";
|
||||||
|
drawOptionRow(1, "AUTO LIGHT SLEEP", lightSleepValue, selectedIndex == 1);
|
||||||
|
|
||||||
|
if (!buzzerAvailable)
|
||||||
|
drawCenteredText(framebuffer, 120, "SOUND CONTROL UNAVAILABLE", 1, 1);
|
||||||
|
|
||||||
|
drawCenteredText(framebuffer, framebuffer.height() - 54, "UP/DOWN MOVE", 1, 1);
|
||||||
|
drawCenteredText(framebuffer, framebuffer.height() - 36, "A/START/SELECT TOGGLE", 1, 1);
|
||||||
|
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK | LIGHT SLEEP AFTER RESET", 1, 1);
|
||||||
|
|
||||||
|
framebuffer.sendFrame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettingsAppFactory final : public cardboy::sdk::IAppFactory {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return kSettingsAppName; }
|
||||||
|
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||||
|
return std::make_unique<SettingsApp>(context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory() { return std::make_unique<SettingsAppFactory>(); }
|
||||||
|
|
||||||
|
} // namespace apps
|
||||||
@@ -70,13 +70,13 @@ constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks)
|
|||||||
}
|
}
|
||||||
|
|
||||||
constexpr std::array<Tetromino, 7> kPieces = {{
|
constexpr std::array<Tetromino, 7> kPieces = {{
|
||||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
|
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
|
||||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
|
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
|
||||||
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
|
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
|
||||||
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
|
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
|
||||||
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
|
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
|
||||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
|
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
|
||||||
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
|
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
|
||||||
}};
|
}};
|
||||||
|
|
||||||
class RandomBag {
|
class RandomBag {
|
||||||
@@ -107,22 +107,22 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ActivePiece {
|
struct ActivePiece {
|
||||||
int type = 0;
|
int type = 0;
|
||||||
int rotation = 0;
|
int rotation = 0;
|
||||||
int x = 0;
|
int x = 0;
|
||||||
int y = 0;
|
int y = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GameState {
|
struct GameState {
|
||||||
std::array<int, kBoardWidth * kBoardHeight> board{};
|
std::array<int, kBoardWidth * kBoardHeight> board{};
|
||||||
ActivePiece current{};
|
ActivePiece current{};
|
||||||
int nextPiece = 0;
|
int nextPiece = 0;
|
||||||
int level = 1;
|
int level = 1;
|
||||||
int linesCleared = 0;
|
int linesCleared = 0;
|
||||||
int score = 0;
|
int score = 0;
|
||||||
int highScore = 0;
|
int highScore = 0;
|
||||||
bool paused = false;
|
bool paused = false;
|
||||||
bool gameOver = false;
|
bool gameOver = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
|
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
|
||||||
@@ -161,21 +161,21 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AppContext& context;
|
AppContext& context;
|
||||||
typename AppContext::Framebuffer& framebuffer;
|
typename AppContext::Framebuffer& framebuffer;
|
||||||
|
|
||||||
GameState state;
|
GameState state;
|
||||||
RandomBag bag;
|
RandomBag bag;
|
||||||
InputState lastInput{};
|
InputState lastInput{};
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
|
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||||
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
|
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
cancelTimers();
|
cancelTimers();
|
||||||
int oldHigh = state.highScore;
|
int oldHigh = state.highScore;
|
||||||
state = {};
|
state = {};
|
||||||
state.highScore = oldHigh;
|
state.highScore = oldHigh;
|
||||||
state.current.type = bag.next();
|
state.current.type = bag.next();
|
||||||
state.nextPiece = bag.next();
|
state.nextPiece = bag.next();
|
||||||
state.current.x = kBoardWidth / 2;
|
state.current.x = kBoardWidth / 2;
|
||||||
@@ -190,7 +190,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleButtons(const AppButtonEvent& evt) {
|
void handleButtons(const AppButtonEvent& evt) {
|
||||||
const auto& cur = evt.current;
|
const auto& cur = evt.current;
|
||||||
const auto& prev = evt.previous;
|
const auto& prev = evt.previous;
|
||||||
lastInput = cur;
|
lastInput = cur;
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ private:
|
|||||||
void scheduleDropTimer() {
|
void scheduleDropTimer() {
|
||||||
cancelDropTimer();
|
cancelDropTimer();
|
||||||
const std::uint32_t interval = dropIntervalMs();
|
const std::uint32_t interval = dropIntervalMs();
|
||||||
dropTimer = context.scheduleRepeatingTimer(interval);
|
dropTimer = context.scheduleRepeatingTimer(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelDropTimer() {
|
void cancelDropTimer() {
|
||||||
@@ -276,8 +276,8 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::uint32_t dropIntervalMs() const {
|
[[nodiscard]] std::uint32_t dropIntervalMs() const {
|
||||||
const int base = 700;
|
const int base = 700;
|
||||||
const int step = 50;
|
const int step = 50;
|
||||||
int interval = base - (state.level - 1) * step;
|
int interval = base - (state.level - 1) * step;
|
||||||
if (interval < 120)
|
if (interval < 120)
|
||||||
interval = 120;
|
interval = 120;
|
||||||
@@ -322,7 +322,7 @@ private:
|
|||||||
|
|
||||||
void rotate(int direction) {
|
void rotate(int direction) {
|
||||||
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
|
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
|
||||||
nextRot = ((nextRot % 4) + 4) % 4;
|
nextRot = ((nextRot % 4) + 4) % 4;
|
||||||
if (canPlace(state.current.x, state.current.y, nextRot)) {
|
if (canPlace(state.current.x, state.current.y, nextRot)) {
|
||||||
state.current.rotation = nextRot;
|
state.current.rotation = nextRot;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
@@ -484,11 +484,10 @@ private:
|
|||||||
for (int y = 0; y < kBoardHeight; ++y) {
|
for (int y = 0; y < kBoardHeight; ++y) {
|
||||||
for (int x = 0; x < kBoardWidth; ++x) {
|
for (int x = 0; x < kBoardWidth; ++x) {
|
||||||
if (int value = cellAt(x, y); value != 0)
|
if (int value = cellAt(x, y); value != 0)
|
||||||
drawCell(originX, originY, x, y, value, true);
|
drawCell(originX, originY, x, y, value - 1, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drawBoardFrame(originX, originY);
|
||||||
drawGuides(originX, originY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawActivePiece() {
|
void drawActivePiece() {
|
||||||
@@ -502,32 +501,76 @@ private:
|
|||||||
int gy = state.current.y + block.y;
|
int gy = state.current.y + block.y;
|
||||||
if (gy < 0)
|
if (gy < 0)
|
||||||
continue;
|
continue;
|
||||||
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
|
drawCell(originX, originY, gx, gy, state.current.type, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
|
static bool patternPixel(int pieceIndex, int dx, int dy) {
|
||||||
const int x0 = originX + cx * kCellSize;
|
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
|
||||||
const int y0 = originY + cy * kCellSize;
|
const int mx = dx & 0x3;
|
||||||
for (int dy = 0; dy < kCellSize; ++dy) {
|
const int my = dy & 0x3;
|
||||||
for (int dx = 0; dx < kCellSize; ++dx) {
|
switch (idx) {
|
||||||
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
|
case 0: // I - vertical stripes
|
||||||
|
return mx < 2;
|
||||||
|
case 1: // J - horizontal stripes
|
||||||
|
return my < 2;
|
||||||
|
case 2: { // L - forward diagonal
|
||||||
|
const int sum = (mx + my) & 0x3;
|
||||||
|
return sum < 2;
|
||||||
|
}
|
||||||
|
case 3: // O - diamond centerpiece
|
||||||
|
return (mx == 1 && my == 1) || (mx == 2 && my == 1) || (mx == 1 && my == 2) || (mx == 2 && my == 2);
|
||||||
|
case 4: // S - checkerboard
|
||||||
|
return ((mx ^ my) & 0x1) == 0;
|
||||||
|
case 5: { // T - cross
|
||||||
|
return (mx == 0) || (my == 0);
|
||||||
|
}
|
||||||
|
case 6: { // Z - backward diagonal
|
||||||
|
int diff = mx - my;
|
||||||
|
if (diff < 0)
|
||||||
|
diff += 4;
|
||||||
|
diff &= 0x3;
|
||||||
|
return diff < 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawPatternBlock(int x0, int y0, int size, int pieceIndex, bool locked) {
|
||||||
|
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
|
||||||
|
for (int dy = 0; dy < size; ++dy) {
|
||||||
|
for (int dx = 0; dx < size; ++dx) {
|
||||||
|
const bool border = dx == 0 || dx == size - 1 || dy == 0 || dy == size - 1;
|
||||||
|
bool fill = patternPixel(idx, dx, dy);
|
||||||
|
if (!locked && !border)
|
||||||
|
fill = fill && (((dx + dy) & 0x1) == 0);
|
||||||
|
const bool on = border || fill;
|
||||||
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
|
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(void) value; // value currently unused (monochrome display)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawGuides(int originX, int originY) {
|
void drawCell(int originX, int originY, int cx, int cy, int pieceIndex, bool locked) {
|
||||||
for (int y = 0; y <= kBoardHeight; ++y) {
|
const int x0 = originX + cx * kCellSize;
|
||||||
const int py = originY + y * kCellSize;
|
const int y0 = originY + cy * kCellSize;
|
||||||
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
|
drawPatternBlock(x0, y0, kCellSize, pieceIndex, locked);
|
||||||
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
|
}
|
||||||
|
|
||||||
|
void drawBoardFrame(int originX, int originY) {
|
||||||
|
const int widthPixels = kBoardWidth * kCellSize;
|
||||||
|
const int heightPixels = kBoardHeight * kCellSize;
|
||||||
|
const int x0 = originX;
|
||||||
|
const int y0 = originY;
|
||||||
|
const int x1 = originX + widthPixels - 1;
|
||||||
|
const int y1 = originY + heightPixels - 1;
|
||||||
|
|
||||||
|
for (int x = x0; x <= x1; ++x) {
|
||||||
|
framebuffer.drawPixel(x, y0, true);
|
||||||
|
framebuffer.drawPixel(x, y1, true);
|
||||||
}
|
}
|
||||||
for (int x = 0; x <= kBoardWidth; ++x) {
|
for (int y = y0; y <= y1; ++y) {
|
||||||
const int px = originX + x * kCellSize;
|
framebuffer.drawPixel(x0, y, true);
|
||||||
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
|
framebuffer.drawPixel(x1, y, true);
|
||||||
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,15 +582,14 @@ private:
|
|||||||
|
|
||||||
for (int dy = 0; dy < boxSize; ++dy)
|
for (int dy = 0; dy < boxSize; ++dy)
|
||||||
for (int dx = 0; dx < boxSize; ++dx)
|
for (int dx = 0; dx < boxSize; ++dx)
|
||||||
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
|
framebuffer.drawPixel(originX + dx, originY + dy,
|
||||||
|
(dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
|
||||||
|
|
||||||
const auto& piece = kPieces[state.nextPiece];
|
const auto& piece = kPieces[state.nextPiece];
|
||||||
for (const auto& block: piece.rotations[0]) {
|
for (const auto& block: piece.rotations[0]) {
|
||||||
const int px = originX + (block.x + 1) * blockSize;
|
const int px = originX + (block.x + 1) * blockSize + 1;
|
||||||
const int py = originY + (block.y + 1) * blockSize;
|
const int py = originY + (block.y + 1) * blockSize + 1;
|
||||||
for (int dy = 1; dy < blockSize - 1; ++dy)
|
drawPatternBlock(px, py, blockSize - 2, state.nextPiece, true);
|
||||||
for (int dx = 1; dx < blockSize - 1; ++dx)
|
|
||||||
framebuffer.drawPixel(px + dx, py + dy, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +650,7 @@ private:
|
|||||||
|
|
||||||
class TetrisFactory final : public cardboy::sdk::IAppFactory {
|
class TetrisFactory final : public cardboy::sdk::IAppFactory {
|
||||||
public:
|
public:
|
||||||
const char* name() const override { return kTetrisAppName; }
|
const char* name() const override { return kTetrisAppName; }
|
||||||
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
|
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
|
||||||
return std::make_unique<TetrisApp>(context);
|
return std::make_unique<TetrisApp>(context);
|
||||||
}
|
}
|
||||||
@@ -616,8 +658,6 @@ public:
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
|
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() { return std::make_unique<TetrisFactory>(); }
|
||||||
return std::make_unique<TetrisFactory>();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace apps
|
} // namespace apps
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
class FramebufferHooks {
|
||||||
|
public:
|
||||||
|
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||||
|
|
||||||
|
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||||
|
static void clearPreSendHook();
|
||||||
|
static void invokePreSend(void* framebuffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static PreSendHook hook_;
|
||||||
|
static void* userData_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||||
#include "input_state.hpp"
|
#include "input_state.hpp"
|
||||||
|
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
@@ -67,8 +68,10 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
|
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
|
||||||
if constexpr (detail::HasSendFrameImpl<Impl>)
|
if constexpr (detail::HasSendFrameImpl<Impl>) {
|
||||||
|
FramebufferHooks::invokePreSend(&impl());
|
||||||
impl().sendFrame_impl(clearDrawBuffer);
|
impl().sendFrame_impl(clearDrawBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
|
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
|
|
||||||
add_library(cardboy_sdk STATIC
|
add_library(cardboy_sdk STATIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(cardboy_sdk PROPERTIES
|
set_target_properties(cardboy_sdk PROPERTIES
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace cardboy::sdk {
|
|||||||
class AppSystem {
|
class AppSystem {
|
||||||
public:
|
public:
|
||||||
explicit AppSystem(AppContext context);
|
explicit AppSystem(AppContext context);
|
||||||
|
~AppSystem();
|
||||||
|
|
||||||
void registerApp(std::unique_ptr<IAppFactory> factory);
|
void registerApp(std::unique_ptr<IAppFactory> factory);
|
||||||
bool startApp(const std::string& name);
|
bool startApp(const std::string& name);
|
||||||
@@ -19,12 +20,12 @@ public:
|
|||||||
|
|
||||||
void run();
|
void run();
|
||||||
|
|
||||||
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
|
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
|
||||||
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
|
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
|
||||||
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
|
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
|
||||||
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
|
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
|
||||||
|
|
||||||
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
|
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
|
||||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -51,15 +52,15 @@ private:
|
|||||||
TimerRecord* findTimer(AppTimerHandle handle);
|
TimerRecord* findTimer(AppTimerHandle handle);
|
||||||
bool handlePendingSwitchRequest();
|
bool handlePendingSwitchRequest();
|
||||||
|
|
||||||
AppContext context;
|
AppContext context;
|
||||||
std::vector<std::unique_ptr<IAppFactory>> factories;
|
std::vector<std::unique_ptr<IAppFactory>> factories;
|
||||||
std::unique_ptr<IApp> current;
|
std::unique_ptr<IApp> current;
|
||||||
IAppFactory* activeFactory = nullptr;
|
IAppFactory* activeFactory = nullptr;
|
||||||
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
||||||
std::vector<TimerRecord> timers;
|
std::vector<TimerRecord> timers;
|
||||||
AppTimerHandle nextTimerId = 1;
|
AppTimerHandle nextTimerId = 1;
|
||||||
std::uint32_t currentGeneration = 0;
|
std::uint32_t currentGeneration = 0;
|
||||||
InputState lastInputState{};
|
InputState lastInputState{};
|
||||||
};
|
};
|
||||||
|
|
||||||
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
|
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
|
||||||
|
|||||||
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
class FramebufferHooks {
|
||||||
|
public:
|
||||||
|
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||||
|
|
||||||
|
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||||
|
static void clearPreSendHook();
|
||||||
|
static void invokePreSend(void* framebuffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static PreSendHook hook_;
|
||||||
|
static void* userData_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
struct PersistentSettings {
|
||||||
|
bool mute = false;
|
||||||
|
bool autoLightSleep = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
PersistentSettings loadPersistentSettings(Services* services);
|
||||||
|
void savePersistentSettings(Services* services, const PersistentSettings& settings);
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
92
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
92
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/input_state.hpp"
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
class StatusBar {
|
||||||
|
public:
|
||||||
|
static StatusBar& instance();
|
||||||
|
|
||||||
|
void setServices(Services* services) { services_ = services; }
|
||||||
|
|
||||||
|
void setEnabled(bool value);
|
||||||
|
void toggle();
|
||||||
|
[[nodiscard]] bool isEnabled() const { return enabled_; }
|
||||||
|
|
||||||
|
void setCurrentAppName(std::string_view name);
|
||||||
|
|
||||||
|
[[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous);
|
||||||
|
|
||||||
|
template<typename Framebuffer>
|
||||||
|
void renderIfEnabled(Framebuffer& fb) {
|
||||||
|
if (!enabled_)
|
||||||
|
return;
|
||||||
|
renderBar(fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
StatusBar() = default;
|
||||||
|
|
||||||
|
template<typename Framebuffer>
|
||||||
|
void renderBar(Framebuffer& fb) {
|
||||||
|
const int width = fb.width();
|
||||||
|
if (width <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int barHeight = font16x8::kGlyphHeight + 2;
|
||||||
|
const int fillHeight = std::min(barHeight, fb.height());
|
||||||
|
if (fillHeight <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string leftText = prepareLeftText(width);
|
||||||
|
const std::string rightText = prepareRightText();
|
||||||
|
|
||||||
|
for (int y = 0; y < fillHeight; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x)
|
||||||
|
fb.drawPixel(x, y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int x = 0; x < width; ++x)
|
||||||
|
fb.drawPixel(x, 0, false);
|
||||||
|
|
||||||
|
const int textY = 1;
|
||||||
|
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
|
||||||
|
if (bottomSeparatorY < fillHeight) {
|
||||||
|
for (int x = 0; x < width; ++x)
|
||||||
|
fb.drawPixel(x, bottomSeparatorY, (x % 2) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int leftX = 2;
|
||||||
|
if (!leftText.empty())
|
||||||
|
font16x8::drawText(fb, leftX, textY, leftText, 1, false, 1);
|
||||||
|
|
||||||
|
if (!rightText.empty()) {
|
||||||
|
int rightWidth = font16x8::measureText(rightText, 1, 1);
|
||||||
|
int rightX = width - rightWidth - 2;
|
||||||
|
const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6;
|
||||||
|
if (rightX < minRightX)
|
||||||
|
rightX = std::max(minRightX, width / 2);
|
||||||
|
if (rightX < width)
|
||||||
|
font16x8::drawText(fb, rightX, textY, rightText, 1, false, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
|
||||||
|
[[nodiscard]] std::string prepareRightText() const;
|
||||||
|
|
||||||
|
bool enabled_ = false;
|
||||||
|
Services* services_ = nullptr;
|
||||||
|
std::string appName_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "cardboy/sdk/app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||||
|
#include "cardboy/sdk/status_bar.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -12,9 +14,25 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constexpr std::uint32_t kIdlePollMs = 16;
|
constexpr std::uint32_t kIdlePollMs = 16;
|
||||||
|
|
||||||
|
template<typename Framebuffer>
|
||||||
|
void statusBarPreSendHook(void* framebuffer, void* userData) {
|
||||||
|
auto* fb = static_cast<Framebuffer*>(framebuffer);
|
||||||
|
auto* status = static_cast<StatusBar*>(userData);
|
||||||
|
if (fb && status)
|
||||||
|
status->renderIfEnabled(*fb);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
|
||||||
|
context.system = this;
|
||||||
|
auto& statusBar = StatusBar::instance();
|
||||||
|
statusBar.setServices(context.services);
|
||||||
|
using FBType = typename AppContext::Framebuffer;
|
||||||
|
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
|
||||||
|
|
||||||
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||||
if (!factory)
|
if (!factory)
|
||||||
@@ -53,6 +71,8 @@ bool AppSystem::startAppByIndex(std::size_t index) {
|
|||||||
clearTimersForCurrentApp();
|
clearTimersForCurrentApp();
|
||||||
current = std::move(app);
|
current = std::move(app);
|
||||||
lastInputState = context.input.readState();
|
lastInputState = context.input.readState();
|
||||||
|
StatusBar::instance().setServices(context.services);
|
||||||
|
StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : "");
|
||||||
current->onStart();
|
current->onStart();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -71,8 +91,10 @@ void AppSystem::run() {
|
|||||||
const std::uint32_t now = context.clock.millis();
|
const std::uint32_t now = context.clock.millis();
|
||||||
processDueTimers(now, events);
|
processDueTimers(now, events);
|
||||||
|
|
||||||
const InputState inputNow = context.input.readState();
|
const InputState inputNow = context.input.readState();
|
||||||
if (inputsDiffer(inputNow, lastInputState)) {
|
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
|
||||||
|
|
||||||
|
if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) {
|
||||||
AppEvent evt{};
|
AppEvent evt{};
|
||||||
evt.type = AppEventType::Button;
|
evt.type = AppEventType::Button;
|
||||||
evt.timestamp_ms = now;
|
evt.timestamp_ms = now;
|
||||||
@@ -80,6 +102,8 @@ void AppSystem::run() {
|
|||||||
evt.button.previous = lastInputState;
|
evt.button.previous = lastInputState;
|
||||||
events.push_back(evt);
|
events.push_back(evt);
|
||||||
lastInputState = inputNow;
|
lastInputState = inputNow;
|
||||||
|
} else if (consumedByStatusToggle) {
|
||||||
|
lastInputState = inputNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& evt: events) {
|
for (const auto& evt: events) {
|
||||||
@@ -228,4 +252,3 @@ bool AppSystem::handlePendingSwitchRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace cardboy::sdk
|
} // namespace cardboy::sdk
|
||||||
|
|
||||||
|
|||||||
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr;
|
||||||
|
void* FramebufferHooks::userData_ = nullptr;
|
||||||
|
|
||||||
|
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
|
||||||
|
hook_ = hook;
|
||||||
|
userData_ = userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FramebufferHooks::clearPreSendHook() {
|
||||||
|
hook_ = nullptr;
|
||||||
|
userData_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FramebufferHooks::invokePreSend(void* framebuffer) {
|
||||||
|
if (hook_)
|
||||||
|
hook_(framebuffer, userData_);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
40
Firmware/sdk/core/src/persistent_settings.cpp
Normal file
40
Firmware/sdk/core/src/persistent_settings.cpp
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#include "cardboy/sdk/persistent_settings.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::string_view kNamespace = "settings";
|
||||||
|
constexpr std::string_view kMuteKey = "mute";
|
||||||
|
constexpr std::string_view kAutoLightSleepKey = "autosleep";
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t boolToStorage(bool value) { return value ? 1U : 0U; }
|
||||||
|
[[nodiscard]] bool storageToBool(std::uint32_t value) { return value != 0U; }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PersistentSettings loadPersistentSettings(Services* services) {
|
||||||
|
PersistentSettings settings{};
|
||||||
|
if (!services || !services->storage)
|
||||||
|
return settings;
|
||||||
|
|
||||||
|
std::uint32_t raw = 0;
|
||||||
|
if (services->storage->readUint32(kNamespace, kMuteKey, raw))
|
||||||
|
settings.mute = storageToBool(raw);
|
||||||
|
|
||||||
|
if (services->storage->readUint32(kNamespace, kAutoLightSleepKey, raw))
|
||||||
|
settings.autoLightSleep = storageToBool(raw);
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
void savePersistentSettings(Services* services, const PersistentSettings& settings) {
|
||||||
|
if (!services || !services->storage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
services->storage->writeUint32(kNamespace, kMuteKey, boolToStorage(settings.mute));
|
||||||
|
services->storage->writeUint32(kNamespace, kAutoLightSleepKey, boolToStorage(settings.autoLightSleep));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
81
Firmware/sdk/core/src/status_bar.cpp
Normal file
81
Firmware/sdk/core/src/status_bar.cpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#include "cardboy/sdk/status_bar.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
StatusBar& StatusBar::instance() {
|
||||||
|
static StatusBar bar;
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusBar::setEnabled(bool value) { enabled_ = value; }
|
||||||
|
|
||||||
|
void StatusBar::toggle() {
|
||||||
|
enabled_ = !enabled_;
|
||||||
|
if (services_ && services_->buzzer)
|
||||||
|
services_->buzzer->beepMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusBar::setCurrentAppName(std::string_view name) {
|
||||||
|
appName_.assign(name.begin(), name.end());
|
||||||
|
std::transform(appName_.begin(), appName_.end(), appName_.begin(),
|
||||||
|
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) {
|
||||||
|
const bool comboNow = current.start && current.select && current.up;
|
||||||
|
const bool comboPrev = previous.start && previous.select && previous.up;
|
||||||
|
if (comboNow && !comboPrev) {
|
||||||
|
toggle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StatusBar::prepareLeftText(int displayWidth) const {
|
||||||
|
std::string text = appName_.empty() ? std::string("CARDBOY") : appName_;
|
||||||
|
int maxWidth = std::max(0, displayWidth - 32);
|
||||||
|
while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth)
|
||||||
|
text.pop_back();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StatusBar::prepareRightText() const {
|
||||||
|
if (!services_)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::string right;
|
||||||
|
if (services_->battery && services_->battery->hasData()) {
|
||||||
|
const float current = services_->battery->current();
|
||||||
|
const float chargeMah = services_->battery->charge();
|
||||||
|
const float fallbackV = services_->battery->voltage();
|
||||||
|
char buf[64];
|
||||||
|
if (std::isfinite(current) && std::isfinite(chargeMah)) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current),
|
||||||
|
static_cast<double>(chargeMah));
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
|
||||||
|
}
|
||||||
|
right.assign(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (services_->powerManager && services_->powerManager->isSlowMode()) {
|
||||||
|
if (!right.empty())
|
||||||
|
right.append(" ");
|
||||||
|
right.append("SLOW");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (services_->buzzer && services_->buzzer->isMuted()) {
|
||||||
|
if (!right.empty())
|
||||||
|
right.append(" ");
|
||||||
|
right.append("MUTE");
|
||||||
|
}
|
||||||
|
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
#include "cardboy/apps/clock_app.hpp"
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
#include "cardboy/apps/gameboy_app.hpp"
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
#include "cardboy/apps/menu_app.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
|
#include "cardboy/apps/settings_app.hpp"
|
||||||
#include "cardboy/apps/tetris_app.hpp"
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
#include "cardboy/backend/desktop_backend.hpp"
|
#include "cardboy/backend/desktop_backend.hpp"
|
||||||
#include "cardboy/sdk/app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
#include "cardboy/sdk/persistent_settings.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
@@ -18,7 +20,13 @@ int main() {
|
|||||||
context.services = &runtime.serviceRegistry();
|
context.services = &runtime.serviceRegistry();
|
||||||
cardboy::sdk::AppSystem system(context);
|
cardboy::sdk::AppSystem system(context);
|
||||||
|
|
||||||
|
const cardboy::sdk::PersistentSettings persistentSettings =
|
||||||
|
cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||||
|
if (auto* buzzer = context.buzzer())
|
||||||
|
buzzer->setMuted(persistentSettings.mute);
|
||||||
|
|
||||||
system.registerApp(apps::createMenuAppFactory());
|
system.registerApp(apps::createMenuAppFactory());
|
||||||
|
system.registerApp(apps::createSettingsAppFactory());
|
||||||
system.registerApp(apps::createClockAppFactory());
|
system.registerApp(apps::createClockAppFactory());
|
||||||
system.registerApp(apps::createGameboyAppFactory());
|
system.registerApp(apps::createGameboyAppFactory());
|
||||||
system.registerApp(apps::createTetrisAppFactory());
|
system.registerApp(apps::createTetrisAppFactory());
|
||||||
|
|||||||
Reference in New Issue
Block a user