mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
more settings
This commit is contained in:
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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 = "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
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user