diff --git a/Firmware/components/backend-esp/include/cardboy/backend/esp/buzzer.hpp b/Firmware/components/backend-esp/include/cardboy/backend/esp/buzzer.hpp index 941688c..c3d73e9 100644 --- a/Firmware/components/backend-esp/include/cardboy/backend/esp/buzzer.hpp +++ b/Firmware/components/backend-esp/include/cardboy/backend/esp/buzzer.hpp @@ -6,7 +6,7 @@ class Buzzer { public: - static Buzzer &get(); + static Buzzer& get(); void init(); // call once from app_main @@ -17,8 +17,8 @@ public: void beepRotate(); void beepMove(); void beepLock(); - void beepLines(int lines); // 1..4 lines - void beepLevelUp(int level); // after increment + void beepLines(int lines); // 1..4 lines + void beepLevelUp(int level); // after increment void beepGameOver(); // Mute controls @@ -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) - bool _muted = false; + 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) + bool _muted = false; Buzzer() = default; - void enqueue(const Step &s); - bool empty() const { return _q_head == _q_tail; } - 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); - void clearQueue() { _q_head = _q_tail = 0; } + void enqueue(const Step& s); + bool empty() const { return _q_head == _q_tail; } + 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); + void clearQueue() { _q_head = _q_tail = 0; } }; diff --git a/Firmware/components/backend-esp/src/buzzer.cpp b/Firmware/components/backend-esp/src/buzzer.cpp index 05015c1..9e995da 100644 --- a/Firmware/components/backend-esp/src/buzzer.cpp +++ b/Firmware/components/backend-esp/src/buzzer.cpp @@ -5,30 +5,18 @@ #include #include #include -#include -#include -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; +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(&_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(_timer)); - esp_timer_start_once(reinterpret_cast(_timer), (uint64_t)ms * 1000ULL); + esp_timer_start_once(reinterpret_cast(_timer), (uint64_t) ms * 1000ULL); } -void Buzzer::timerCb(void *arg) { - auto *self = static_cast(arg); - if (!self) return; +void Buzzer::timerCb(void* arg) { + auto* self = static_cast(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); - } -} diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index e4a7bcf..2de476d 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -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(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()); diff --git a/Firmware/sdk/apps/settings/src/settings_app.cpp b/Firmware/sdk/apps/settings/src/settings_app.cpp index 205cef1..16d50ca 100644 --- a/Firmware/sdk/apps/settings/src/settings_app.cpp +++ b/Firmware/sdk/apps/settings/src/settings_app.cpp @@ -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 +#include #include #include #include @@ -15,12 +18,22 @@ namespace { using cardboy::sdk::AppContext; using Framebuffer = typename AppContext::Framebuffer; +enum class SettingOption { + Sound, + AutoLightSleep, +}; + +constexpr std::array 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(); } @@ -55,29 +79,63 @@ private: AppContext& context; Framebuffer& framebuffer; - bool buzzerAvailable = false; - bool muted = false; - bool dirty = false; + bool buzzerAvailable = 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(kOptions.size()); + if (count == 0) + return; + const int current = static_cast(selectedIndex); + int next = (current + delta) % count; + if (next < 0) + next += count; + selectedIndex = static_cast(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) - buzzer->beepMove(); + 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(); } diff --git a/Firmware/sdk/core/CMakeLists.txt b/Firmware/sdk/core/CMakeLists.txt index 4b30d08..bffdd2c 100644 --- a/Firmware/sdk/core/CMakeLists.txt +++ b/Firmware/sdk/core/CMakeLists.txt @@ -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 diff --git a/Firmware/sdk/core/include/cardboy/sdk/persistent_settings.hpp b/Firmware/sdk/core/include/cardboy/sdk/persistent_settings.hpp new file mode 100644 index 0000000..a8a9763 --- /dev/null +++ b/Firmware/sdk/core/include/cardboy/sdk/persistent_settings.hpp @@ -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 diff --git a/Firmware/sdk/core/src/persistent_settings.cpp b/Firmware/sdk/core/src/persistent_settings.cpp new file mode 100644 index 0000000..f0fe61e --- /dev/null +++ b/Firmware/sdk/core/src/persistent_settings.cpp @@ -0,0 +1,40 @@ +#include "cardboy/sdk/persistent_settings.hpp" + +#include +#include + +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 diff --git a/Firmware/sdk/launchers/desktop/src/main.cpp b/Firmware/sdk/launchers/desktop/src/main.cpp index 82d3832..564ac35 100644 --- a/Firmware/sdk/launchers/desktop/src/main.cpp +++ b/Firmware/sdk/launchers/desktop/src/main.cpp @@ -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 #include @@ -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());