From 696229cfe1f466373bdbc0a974cc68132f118c85 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 14 Jun 2025 21:08:04 -0700 Subject: [PATCH] Introduce StateManager class for persisting state to EEPROM. --- examples/Gravity/Gravity.ino | 28 ++++++----- examples/Gravity/app_state.h | 18 +++++++ examples/Gravity/save_state.cpp | 85 +++++++++++++++++++++++++++++++++ examples/Gravity/save_state.h | 54 +++++++++++++++++++++ 4 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 examples/Gravity/app_state.h create mode 100644 examples/Gravity/save_state.cpp create mode 100644 examples/Gravity/save_state.h diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index f03adea..417d39d 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -1,5 +1,5 @@ /** - * @file clock_mod.ino + * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Demo firmware for Sitka Instruments Gravity. * @version 0.1 @@ -18,20 +18,14 @@ */ #include - +#include "save_state.h" +#include "app_state.h" #include "channel.h" -// Firmware state variables. -struct AppState { - bool refresh_screen = true; - bool editing_param = false; - int selected_param = 0; - byte selected_channel = 0; // 0=tempo, 1-6=output channel - Source selected_source = SOURCE_INTERNAL; - Channel channel[OUTPUT_COUNT]; -}; AppState app; +StateManager stateManager; + enum ParamsMainPage { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, @@ -114,6 +108,13 @@ void setup() { // Start Gravity. gravity.Init(); + // Initialize the state manager. This will load settings from EEPROM + stateManager.initialize(app); + + // Apply the loaded state to the hardware + gravity.clock.SetTempo(app.tempo); + gravity.clock.SetSource(app.selected_source); + // Clock handlers. gravity.clock.AttachIntHandler(HandleIntClockTick); gravity.clock.AttachExtHandler(HandleExtClockTick); @@ -192,6 +193,7 @@ void HandleShiftPressed() { void HandleEncoderPressed() { app.editing_param = !app.editing_param; + stateManager.save(app); app.refresh_screen = true; } @@ -208,6 +210,7 @@ void HandleRotate(Direction dir, int val) { editChannelParameter(val); } } + stateManager.save(app); app.refresh_screen = true; } @@ -218,6 +221,7 @@ void HandlePressedRotate(Direction dir, int val) { app.selected_channel--; } app.selected_param = 0; + stateManager.save(app); app.refresh_screen = true; } @@ -228,6 +232,7 @@ void editMainParameter(int val) { break; } gravity.clock.SetTempo(gravity.clock.Tempo() + val); + app.tempo = gravity.clock.Tempo(); break; case PARAM_MAIN_SOURCE: { @@ -357,6 +362,7 @@ void DisplayMainPage() { subText = "MIDI"; break; } + break; } drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT); diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h new file mode 100644 index 0000000..35b03e3 --- /dev/null +++ b/examples/Gravity/app_state.h @@ -0,0 +1,18 @@ +#ifndef APP_STATE_H +#define APP_STATE_H + +#include +#include "channel.h" + +// Global state for settings and app behavior. +struct AppState { + int tempo = 120; + bool refresh_screen = true; + bool editing_param = false; + int selected_param = 0; + byte selected_channel = 0; // 0=tempo, 1-6=output channel + Source selected_source = SOURCE_INTERNAL; + Channel channel[OUTPUT_COUNT]; +}; + +#endif // APP_STATE_H \ No newline at end of file diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp new file mode 100644 index 0000000..1233440 --- /dev/null +++ b/examples/Gravity/save_state.cpp @@ -0,0 +1,85 @@ +// File: save_state.cpp + +#include +#include "save_state.h" +#include "app_state.h" // Includes AppState and Channel definitions + +bool StateManager::initialize(AppState& app) { + if (isDataValid()) { + EepromData loadedState; + EEPROM.get(sizeof(Metadata), loadedState); + + // Restore main app state + app.selected_param = loadedState.selected_param; + app.selected_channel = loadedState.selected_channel; + app.selected_source = static_cast(loadedState.selected_source); + + // --- NEW: Loop through and restore each channel's state --- + for (int i = 0; i < OUTPUT_COUNT; i++) { + auto& ch = app.channel[i]; // Get a reference to the channel object + const auto& saved_ch_state = loadedState.channel_data[i]; // Get a const reference to the saved data + + ch.setClockMod(saved_ch_state.base_clock_mod_index); + ch.setProbability(saved_ch_state.base_probability); + ch.setDutyCycle(saved_ch_state.base_duty_cycle); + ch.setOffset(saved_ch_state.base_offset); + ch.setCvSource(static_cast(saved_ch_state.cv_source)); + ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); + } + + return true; + } else { + writeMetadata(); + save(app); // Save the initial default state + return false; + } +} + +void StateManager::save(const AppState& app) { + EepromData stateToSave; + + // Populate main app state + stateToSave.selected_param = app.selected_param; + stateToSave.selected_channel = app.selected_channel; + stateToSave.selected_source = static_cast(app.selected_source); + + // --- NEW: Loop through and populate each channel's state --- + for (int i = 0; i < OUTPUT_COUNT; i++) { + const auto& ch = app.channel[i]; // Get a const reference to the channel object + auto& saved_ch_state = stateToSave.channel_data[i]; // Get a reference to the struct we're saving to + + // Use the getters with 'withCvMod = false' to get the base values + saved_ch_state.base_clock_mod_index = ch.getClockModIndex(false); + saved_ch_state.base_probability = ch.getProbability(false); + saved_ch_state.base_duty_cycle = ch.getDutyCycle(false); + saved_ch_state.base_offset = ch.getOffset(false); + saved_ch_state.cv_source = static_cast(ch.getCvSource()); + saved_ch_state.cv_destination = static_cast(ch.getCvDestination()); + } + + // Write the entire state struct to EEPROM + EEPROM.put(sizeof(Metadata), stateToSave); +} + +void StateManager::reset(AppState& app) { + AppState defaultState; + app = defaultState; + writeMetadata(); + save(app); +} + +// isDataValid() and writeMetadata() remain unchanged +bool StateManager::isDataValid() { + Metadata storedMeta; + EEPROM.get(0, storedMeta); + bool nameMatch = (strcmp(storedMeta.sketchName, CURRENT_SKETCH_NAME) == 0); + bool versionMatch = (storedMeta.version == CURRENT_SKETCH_VERSION); + return nameMatch && versionMatch; +} + +void StateManager::writeMetadata() { + Metadata currentMeta; + strcpy(currentMeta.sketchName, CURRENT_SKETCH_NAME); + currentMeta.version = CURRENT_SKETCH_VERSION; + EEPROM.put(0, currentMeta); +} \ No newline at end of file diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h new file mode 100644 index 0000000..3842a8f --- /dev/null +++ b/examples/Gravity/save_state.h @@ -0,0 +1,54 @@ +// File: save_state.h + +#ifndef SAVE_STATE_H +#define SAVE_STATE_H + +#include +#include // We need this for OUTPUT_COUNT + +// Forward-declare AppState to avoid circular dependencies. +struct AppState; + +// Forward-declare the Source enum as well. +enum Source; + +// Define the constants for the current firmware. +const char CURRENT_SKETCH_NAME[] = "Gravity"; +const float CURRENT_SKETCH_VERSION = 0.2f; // You could increment this to 0.2 if you want to force a reset + +/** + * @brief Manages saving and loading of the application state to and from EEPROM. + */ +class StateManager { +public: + bool initialize(AppState& app); + void save(const AppState& app); + void reset(AppState& app); + +private: + // This struct holds the data that identifies the firmware version. + struct Metadata { + char sketchName[16]; + float version; + }; + struct ChannelState { + byte base_clock_mod_index; + byte base_probability; + byte base_duty_cycle; + byte base_offset; + byte cv_source; // We'll store the CvSource enum as a byte + byte cv_destination; // We'll store the CvDestination enum as a byte + }; + // This struct holds all the parameters we want to save. + struct EepromData { + int selected_param; + byte selected_channel; + byte selected_source; + ChannelState channel_data[OUTPUT_COUNT]; + }; + + bool isDataValid(); + void writeMetadata(); +}; + +#endif // SAVE_STATE_H \ No newline at end of file