Introduce StateManager to persist state between power cycles #6
8
clock.h
8
clock.h
@ -23,12 +23,14 @@
|
||||
#define MIDI_STOP 0xFC
|
||||
#define MIDI_CONTINUE 0xFB
|
||||
|
||||
const int DEFAULT_TEMPO = 120;
|
||||
|
||||
typedef void (*ExtCallback)(void);
|
||||
static ExtCallback extUserCallback = nullptr;
|
||||
static void serialEventNoop(uint8_t msg, uint8_t status) {}
|
||||
|
||||
class Clock {
|
||||
public:
|
||||
static constexpr int DEFAULT_TEMPO = 120;
|
||||
|
||||
enum Source {
|
||||
SOURCE_INTERNAL,
|
||||
SOURCE_EXTERNAL_PPQN_24,
|
||||
@ -37,8 +39,6 @@ enum Source {
|
||||
SOURCE_LAST,
|
||||
};
|
||||
|
||||
class Clock {
|
||||
public:
|
||||
void Init() {
|
||||
NeoSerial.begin(31250);
|
||||
|
||||
|
||||
@ -18,9 +18,10 @@
|
||||
*/
|
||||
|
||||
#include <gravity.h>
|
||||
#include "save_state.h"
|
||||
|
||||
#include "app_state.h"
|
||||
#include "channel.h"
|
||||
#include "save_state.h"
|
||||
|
||||
AppState app;
|
||||
|
||||
@ -29,6 +30,7 @@ StateManager stateManager;
|
||||
enum ParamsMainPage {
|
||||
PARAM_MAIN_TEMPO,
|
||||
PARAM_MAIN_SOURCE,
|
||||
PARAM_MAIN_RESET_STATE,
|
||||
PARAM_MAIN_LAST,
|
||||
};
|
||||
|
||||
@ -110,10 +112,7 @@ void setup() {
|
||||
|
||||
// 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);
|
||||
InitAppState(app);
|
||||
|
||||
// Clock handlers.
|
||||
gravity.clock.AttachIntHandler(HandleIntClockTick);
|
||||
@ -136,7 +135,7 @@ void loop() {
|
||||
// Read CVs and call the update function for each channel.
|
||||
int cv1 = gravity.cv1.Read();
|
||||
int cv2 = gravity.cv2.Read();
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
app.channel[i].applyCvMod(cv1, cv2);
|
||||
}
|
||||
|
||||
@ -151,7 +150,7 @@ void loop() {
|
||||
|
||||
void HandleIntClockTick(uint32_t tick) {
|
||||
bool refresh = false;
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
app.channel[i].processClockTick(tick, gravity.outputs[i]);
|
||||
|
||||
if (app.channel[i].isCvModActive()) {
|
||||
@ -192,6 +191,20 @@ void HandleShiftPressed() {
|
||||
}
|
||||
|
||||
void HandleEncoderPressed() {
|
||||
// Check if leaving editing mode should apply a selection.
|
||||
if (app.editing_param) {
|
||||
if (app.selected_channel == 0) { // main page
|
||||
// Reset state
|
||||
if (app.selected_param == PARAM_MAIN_RESET_STATE) {
|
||||
if (app.selected_sub_param == 0) { // Reset
|
||||
stateManager.reset(app);
|
||||
// stateManager.initialize(app);
|
||||
InitAppState(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app.selected_sub_param = 0;
|
||||
app.editing_param = !app.editing_param;
|
||||
stateManager.save(app);
|
||||
app.refresh_screen = true;
|
||||
@ -215,7 +228,7 @@ void HandleRotate(Direction dir, int val) {
|
||||
}
|
||||
|
||||
void HandlePressedRotate(Direction dir, int val) {
|
||||
if (dir == DIRECTION_INCREMENT && app.selected_channel < OUTPUT_COUNT) {
|
||||
if (dir == DIRECTION_INCREMENT && app.selected_channel < Gravity::OUTPUT_COUNT) {
|
||||
app.selected_channel++;
|
||||
} else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) {
|
||||
app.selected_channel--;
|
||||
@ -237,11 +250,14 @@ void editMainParameter(int val) {
|
||||
|
||||
case PARAM_MAIN_SOURCE: {
|
||||
int source = static_cast<int>(app.selected_source);
|
||||
updateSelection(source, val, SOURCE_LAST);
|
||||
app.selected_source = static_cast<Source>(source);
|
||||
updateSelection(source, val, Clock::SOURCE_LAST);
|
||||
app.selected_source = static_cast<Clock::Source>(source);
|
||||
gravity.clock.SetSource(app.selected_source);
|
||||
break;
|
||||
}
|
||||
case PARAM_MAIN_RESET_STATE:
|
||||
updateSelection(app.selected_sub_param, val, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,12 +300,17 @@ void updateSelection(int& param, int change, int maxValue) {
|
||||
// Helper functions.
|
||||
//
|
||||
|
||||
void InitAppState(AppState& app) {
|
||||
gravity.clock.SetTempo(app.tempo);
|
||||
gravity.clock.SetSource(app.selected_source);
|
||||
}
|
||||
|
||||
Channel& GetSelectedChannel() {
|
||||
return app.channel[app.selected_channel - 1];
|
||||
}
|
||||
|
||||
void ResetOutputs() {
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
gravity.outputs[i].Low();
|
||||
}
|
||||
}
|
||||
@ -336,7 +357,7 @@ void DisplayMainPage() {
|
||||
switch (app.selected_param) {
|
||||
case PARAM_MAIN_TEMPO:
|
||||
// Serial MIDI is too unstable to display bpm in real time.
|
||||
if (app.selected_source == SOURCE_EXTERNAL_MIDI) {
|
||||
if (app.selected_source == Clock::SOURCE_EXTERNAL_MIDI) {
|
||||
sprintf(mainText, "%s", "EXT");
|
||||
} else {
|
||||
sprintf(mainText, "%d", gravity.clock.Tempo());
|
||||
@ -345,31 +366,35 @@ void DisplayMainPage() {
|
||||
break;
|
||||
case PARAM_MAIN_SOURCE:
|
||||
switch (app.selected_source) {
|
||||
case SOURCE_INTERNAL:
|
||||
case Clock::SOURCE_INTERNAL:
|
||||
sprintf(mainText, "%s", "INT");
|
||||
subText = "CLOCK";
|
||||
break;
|
||||
case SOURCE_EXTERNAL_PPQN_24:
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_24:
|
||||
sprintf(mainText, "%s", "EXT");
|
||||
subText = "24 PPQN";
|
||||
break;
|
||||
case SOURCE_EXTERNAL_PPQN_4:
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_4:
|
||||
sprintf(mainText, "%s", "EXT");
|
||||
subText = "4 PPQN";
|
||||
break;
|
||||
case SOURCE_EXTERNAL_MIDI:
|
||||
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||
sprintf(mainText, "%s", "EXT");
|
||||
subText = "MIDI";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case PARAM_MAIN_RESET_STATE:
|
||||
sprintf(mainText, "%s", "RST");
|
||||
subText = app.selected_sub_param == 0 ? "RESET ALL" : "BACK";
|
||||
break;
|
||||
}
|
||||
|
||||
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
|
||||
|
||||
// Draw Main Page menu items
|
||||
const char* menu_items[PARAM_MAIN_LAST] = {"TEMPO", "SOURCE"};
|
||||
const char* menu_items[PARAM_MAIN_LAST] = {"TEMPO", "SOURCE", "RESET"};
|
||||
drawMenuItems(menu_items, PARAM_MAIN_LAST);
|
||||
}
|
||||
|
||||
@ -475,7 +500,7 @@ void DisplaySelectedChannel() {
|
||||
gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2);
|
||||
gravity.display.drawVLine(SCREEN_WIDTH - 2, boxY, boxHeight);
|
||||
|
||||
for (int i = 0; i < OUTPUT_COUNT + 1; i++) {
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT + 1; i++) {
|
||||
// Draw box frame or filled selected box.
|
||||
gravity.display.setDrawColor(1);
|
||||
(app.selected_channel == i)
|
||||
|
||||
@ -2,17 +2,19 @@
|
||||
#define APP_STATE_H
|
||||
|
||||
#include <gravity.h>
|
||||
|
||||
#include "channel.h"
|
||||
|
||||
// Global state for settings and app behavior.
|
||||
struct AppState {
|
||||
int tempo = 120;
|
||||
int tempo = Clock::DEFAULT_TEMPO;
|
||||
bool refresh_screen = true;
|
||||
bool editing_param = false;
|
||||
int selected_param = 0;
|
||||
int selected_sub_param = 0;
|
||||
byte selected_channel = 0; // 0=tempo, 1-6=output channel
|
||||
Source selected_source = SOURCE_INTERNAL;
|
||||
Channel channel[OUTPUT_COUNT];
|
||||
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
|
||||
Channel channel[Gravity::OUTPUT_COUNT];
|
||||
};
|
||||
|
||||
#endif // APP_STATE_H
|
||||
@ -30,10 +30,24 @@ static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48,
|
||||
class Channel {
|
||||
public:
|
||||
Channel() {
|
||||
Init();
|
||||
}
|
||||
|
||||
void Init() {
|
||||
// Reset base values to their defaults
|
||||
base_clock_mod_index = 7;
|
||||
base_probability = 100;
|
||||
base_duty_cycle = 50;
|
||||
base_offset = 0;
|
||||
cv_source = CV_NONE;
|
||||
cv_destination = CV_DEST_NONE;
|
||||
|
||||
cvmod_clock_mod_index = base_clock_mod_index;
|
||||
cvmod_probability = base_probability;
|
||||
cvmod_duty_cycle = base_duty_cycle;
|
||||
cvmod_offset = base_offset;
|
||||
duty_cycle_pulses = 0;
|
||||
offset_pulses = 0;
|
||||
}
|
||||
|
||||
// Setters (Set the BASE value)
|
||||
@ -121,11 +135,11 @@ class Channel {
|
||||
}
|
||||
|
||||
private:
|
||||
// User-settable "base" values.
|
||||
byte base_clock_mod_index = 7;
|
||||
byte base_probability = 100;
|
||||
byte base_duty_cycle = 50;
|
||||
byte base_offset = 0;
|
||||
// User-settable base values.
|
||||
byte base_clock_mod_index;
|
||||
byte base_probability;
|
||||
byte base_duty_cycle;
|
||||
byte base_offset;
|
||||
|
||||
// Base value with cv mod applied.
|
||||
byte cvmod_clock_mod_index;
|
||||
@ -133,8 +147,8 @@ class Channel {
|
||||
byte cvmod_duty_cycle;
|
||||
byte cvmod_offset;
|
||||
|
||||
int duty_cycle_pulses;
|
||||
int offset_pulses;
|
||||
uint32_t duty_cycle_pulses;
|
||||
uint32_t offset_pulses;
|
||||
|
||||
// CV configuration
|
||||
CvSource cv_source = CV_NONE;
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
// File: save_state.cpp
|
||||
#include "save_state.h"
|
||||
|
||||
#include <EEPROM.h>
|
||||
#include "save_state.h"
|
||||
#include "app_state.h" // Includes AppState and Channel definitions
|
||||
|
||||
#include "app_state.h"
|
||||
|
||||
bool StateManager::initialize(AppState& app) {
|
||||
if (isDataValid()) {
|
||||
EepromData loadedState;
|
||||
EEPROM.get(sizeof(Metadata), loadedState);
|
||||
EepromData load_data;
|
||||
EEPROM.get(sizeof(Metadata), load_data);
|
||||
|
||||
// Restore main app state
|
||||
app.selected_param = loadedState.selected_param;
|
||||
app.selected_channel = loadedState.selected_channel;
|
||||
app.selected_source = static_cast<Source>(loadedState.selected_source);
|
||||
app.tempo = load_data.tempo;
|
||||
app.selected_param = load_data.selected_param;
|
||||
app.selected_channel = load_data.selected_channel;
|
||||
app.selected_source = static_cast<Clock::Source>(load_data.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
|
||||
// Loop through and restore each channel's state.
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
auto& ch = app.channel[i];
|
||||
const auto& saved_ch_state = load_data.channel_data[i];
|
||||
|
||||
ch.setClockMod(saved_ch_state.base_clock_mod_index);
|
||||
ch.setProbability(saved_ch_state.base_probability);
|
||||
@ -36,17 +37,18 @@ bool StateManager::initialize(AppState& app) {
|
||||
}
|
||||
|
||||
void StateManager::save(const AppState& app) {
|
||||
EepromData stateToSave;
|
||||
EepromData save_data;
|
||||
|
||||
// Populate main app state
|
||||
stateToSave.selected_param = app.selected_param;
|
||||
stateToSave.selected_channel = app.selected_channel;
|
||||
stateToSave.selected_source = static_cast<byte>(app.selected_source);
|
||||
save_data.tempo = app.tempo;
|
||||
save_data.selected_param = app.selected_param;
|
||||
save_data.selected_channel = app.selected_channel;
|
||||
save_data.selected_source = static_cast<byte>(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
|
||||
// Loop through and populate each channel's state
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
const auto& ch = app.channel[i];
|
||||
auto& saved_ch_state = save_data.channel_data[i];
|
||||
|
||||
// Use the getters with 'withCvMod = false' to get the base values
|
||||
saved_ch_state.base_clock_mod_index = ch.getClockModIndex(false);
|
||||
@ -58,28 +60,34 @@ void StateManager::save(const AppState& app) {
|
||||
}
|
||||
|
||||
// Write the entire state struct to EEPROM
|
||||
EEPROM.put(sizeof(Metadata), stateToSave);
|
||||
EEPROM.put(sizeof(Metadata), save_data);
|
||||
}
|
||||
|
||||
void StateManager::reset(AppState& app) {
|
||||
AppState defaultState;
|
||||
app = defaultState;
|
||||
app.tempo = Clock::DEFAULT_TEMPO;
|
||||
app.selected_param = 0;
|
||||
app.selected_channel = 0;
|
||||
app.selected_source = Clock::SOURCE_INTERNAL;
|
||||
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
app.channel[i].Init();
|
||||
}
|
||||
|
||||
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);
|
||||
Metadata load_meta;
|
||||
EEPROM.get(0, load_meta);
|
||||
bool nameMatch = (strcmp(load_meta.sketchName, CURRENT_SKETCH_NAME) == 0);
|
||||
bool versionMatch = (load_meta.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);
|
||||
Metadata save_meta;
|
||||
strcpy(save_meta.sketchName, CURRENT_SKETCH_NAME);
|
||||
save_meta.version = CURRENT_SKETCH_VERSION;
|
||||
EEPROM.put(0, save_meta);
|
||||
}
|
||||
@ -1,20 +1,15 @@
|
||||
// File: save_state.h
|
||||
|
||||
#ifndef SAVE_STATE_H
|
||||
#define SAVE_STATE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <gravity.h> // We need this for OUTPUT_COUNT
|
||||
#include <gravity.h>
|
||||
|
||||
// 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
|
||||
const float CURRENT_SKETCH_VERSION = 0.2f;
|
||||
|
||||
/**
|
||||
* @brief Manages saving and loading of the application state to and from EEPROM.
|
||||
@ -41,10 +36,11 @@ private:
|
||||
};
|
||||
// This struct holds all the parameters we want to save.
|
||||
struct EepromData {
|
||||
int selected_param;
|
||||
int tempo;
|
||||
byte selected_param;
|
||||
byte selected_channel;
|
||||
byte selected_source;
|
||||
ChannelState channel_data[OUTPUT_COUNT];
|
||||
ChannelState channel_data[Gravity::OUTPUT_COUNT];
|
||||
};
|
||||
|
||||
bool isDataValid();
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
// Hardware abstraction wrapper for the Gravity module.
|
||||
class Gravity {
|
||||
public:
|
||||
static const uint8_t OUTPUT_COUNT = 6;
|
||||
|
||||
// Constructor
|
||||
Gravity()
|
||||
: display(U8G2_R2, SCL, SDA, U8X8_PIN_NONE) {}
|
||||
|
||||
@ -39,6 +39,4 @@
|
||||
#define OUT_CH5 9
|
||||
#define OUT_CH6 11
|
||||
|
||||
const uint8_t OUTPUT_COUNT = 6;
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user