more settings

This commit is contained in:
2025-10-12 17:11:22 +02:00
parent 1b6e9a0f78
commit aaac0514c0
8 changed files with 227 additions and 119 deletions

View File

@@ -6,7 +6,7 @@
class Buzzer {
public:
static Buzzer &get();
static Buzzer& get();
void init(); // call once from app_main
@@ -26,29 +26,29 @@ public:
void toggleMuted();
bool isMuted() const { return _muted; }
// Persistence
void loadState();
void saveState();
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;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = 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;
Buzzer() = default;
void enqueue(const Step &s);
void enqueue(const Step& s);
bool empty() const { return _q_head == _q_tail; }
Step &front() { return _queue[_q_head]; }
Step& front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void *arg);
static void timerCb(void* arg);
void clearQueue() { _q_head = _q_tail = 0; }
};

View File

@@ -5,30 +5,18 @@
#include <driver/ledc.h>
#include <esp_err.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_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
Buzzer &Buzzer::get() {
Buzzer& Buzzer::get() {
static Buzzer b;
return b;
}
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{};
tcfg.speed_mode = LEDC_MODE;
tcfg.timer_num = LEDC_TIMER;
@@ -52,7 +40,6 @@ void Buzzer::init() {
args.arg = this;
args.name = "buzz";
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
loadState();
}
void Buzzer::applyFreq(uint32_t freq) {
@@ -65,7 +52,7 @@ void Buzzer::applyFreq(uint32_t freq) {
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;
if (nextTail == _q_head) { // full, drop oldest
_q_head = (_q_head + 1) % MAX_QUEUE;
@@ -87,21 +74,23 @@ void Buzzer::startNext() {
}
_running = true;
_in_gap = false;
Step &s = front();
Step& s = front();
applyFreq(s.freq);
schedule(s.dur_ms, false);
}
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
if (!_timer) return;
if (!_timer)
return;
_in_gap = gapPhase;
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) {
auto *self = static_cast<Buzzer*>(arg);
if (!self) return;
void Buzzer::timerCb(void* arg) {
auto* self = static_cast<Buzzer*>(arg);
if (!self)
return;
if (self->_in_gap) {
self->popFront();
self->startNext();
@@ -109,7 +98,7 @@ void Buzzer::timerCb(void *arg) {
}
// Tone finished
if (!self->empty()) {
auto &s = self->front();
auto& s = self->front();
if (s.gap_ms) {
self->applyFreq(0);
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) {
if (_muted) return; // ignore while muted
if (_muted)
return; // ignore while muted
Step s{freq, duration_ms, gap_ms};
enqueue(s);
if (!_running)
@@ -149,7 +139,8 @@ void Buzzer::beepGameOver() {
}
void Buzzer::setMuted(bool m) {
if (m == _muted) return;
if (m == _muted)
return;
_muted = m;
if (_muted) {
clearQueue();
@@ -164,28 +155,6 @@ void Buzzer::setMuted(bool m) {
tone(1500, 40, 10);
tone(1900, 60, 0);
}
saveState();
}
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);
}
}

View File

@@ -7,6 +7,7 @@
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include "esp_err.h"
#include "esp_pm.h"
@@ -204,15 +205,6 @@ inline void start_task_usage_monitor() {}
#endif
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));
static cardboy::backend::esp::EspRuntime runtime;
@@ -222,6 +214,23 @@ extern "C" void app_main() {
cardboy::sdk::AppSystem system(context);
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::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());

View File

@@ -3,7 +3,10 @@
#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>
@@ -15,12 +18,22 @@ 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 {
refreshState();
loadSettings();
dirty = true;
renderIfNeeded();
}
@@ -32,10 +45,9 @@ public:
const auto& current = event.button.current;
const auto& previous = event.button.previous;
const bool prevAvailable = buzzerAvailable;
const bool prevMuted = muted;
refreshState();
if (prevAvailable != buzzerAvailable || prevMuted != muted)
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
if (current.b && !previous.b) {
@@ -43,10 +55,22 @@ public:
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)
toggleMute();
handleToggle();
if (moved)
dirty = true;
renderIfNeeded();
}
@@ -56,28 +80,62 @@ private:
Framebuffer& framebuffer;
bool buzzerAvailable = false;
bool muted = false;
cardboy::sdk::PersistentSettings settings{};
std::size_t selectedIndex = 0;
bool dirty = false;
void refreshState() {
if (auto* buzzer = context.buzzer()) {
buzzerAvailable = true;
muted = buzzer->isMuted();
} else {
buzzerAvailable = false;
muted = 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 toggleMute() {
auto* buzzer = context.buzzer();
if (!buzzer)
void toggleSound() {
if (!buzzerAvailable)
return;
const bool targetMuted = !muted;
buzzer->setMuted(targetMuted);
muted = buzzer->isMuted();
if (!muted)
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;
}
@@ -87,6 +145,17 @@ private:
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;
@@ -97,19 +166,18 @@ private:
drawCenteredText(framebuffer, 24, "SETTINGS", 1, 1);
const int centerY = framebuffer.height() / 2;
if (!buzzerAvailable) {
drawCenteredText(framebuffer, centerY - 12, "BUZZER SERVICE", 1, 1);
drawCenteredText(framebuffer, centerY + 8, "UNAVAILABLE", 1, 1);
} else {
const char* stateText = muted ? "ON" : "OFF";
std::string line = std::string("MUTE: ") + stateText;
drawCenteredText(framebuffer, centerY - 20, line, 2, 0);
drawCenteredText(framebuffer, centerY + 26, "A TOGGLE", 1, 1);
drawCenteredText(framebuffer, centerY + 44, "START/SELECT ALSO", 1, 1);
}
const std::string soundValue = buzzerAvailable ? (settings.mute ? "OFF" : "ON") : "N/A";
drawOptionRow(0, "SOUND", soundValue, selectedIndex == 0);
drawCenteredText(framebuffer, framebuffer.height() - 28, "B BACK", 1, 1);
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();
}

View File

@@ -4,6 +4,7 @@ add_library(cardboy_sdk STATIC
${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

View File

@@ -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

View 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 = "auto_light_sleep";
[[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

View File

@@ -5,6 +5,7 @@
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/desktop_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include <cstdio>
#include <exception>
@@ -19,6 +20,11 @@ int main() {
context.services = &runtime.serviceRegistry();
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::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());