Introduce StateManager to persist state between power cycles #6

Merged
awonak merged 5 commits from refs/pull/6/head into main 2025-06-16 02:47:25 +00:00
8 changed files with 295 additions and 87 deletions
Showing only changes of commit 0cef942f2c - Show all commits

20
clock.h
View File

@ -23,22 +23,22 @@
#define MIDI_STOP 0xFC #define MIDI_STOP 0xFC
#define MIDI_CONTINUE 0xFB #define MIDI_CONTINUE 0xFB
const int DEFAULT_TEMPO = 120;
typedef void (*ExtCallback)(void); typedef void (*ExtCallback)(void);
static ExtCallback extUserCallback = nullptr; static ExtCallback extUserCallback = nullptr;
static void serialEventNoop(uint8_t msg, uint8_t status) {} static void serialEventNoop(uint8_t msg, uint8_t status) {}
enum Source {
SOURCE_INTERNAL,
SOURCE_EXTERNAL_PPQN_24,
SOURCE_EXTERNAL_PPQN_4,
SOURCE_EXTERNAL_MIDI,
SOURCE_LAST,
};
class Clock { class Clock {
public: public:
static constexpr int DEFAULT_TEMPO = 120;
enum Source {
SOURCE_INTERNAL,
SOURCE_EXTERNAL_PPQN_24,
SOURCE_EXTERNAL_PPQN_4,
SOURCE_EXTERNAL_MIDI,
SOURCE_LAST,
};
void Init() { void Init() {
NeoSerial.begin(31250); NeoSerial.begin(31250);

View File

@ -18,9 +18,10 @@
*/ */
#include <gravity.h> #include <gravity.h>
#include "save_state.h"
#include "app_state.h" #include "app_state.h"
#include "channel.h" #include "channel.h"
#include "save_state.h"
AppState app; AppState app;
@ -29,6 +30,7 @@ StateManager stateManager;
enum ParamsMainPage { enum ParamsMainPage {
PARAM_MAIN_TEMPO, PARAM_MAIN_TEMPO,
PARAM_MAIN_SOURCE, PARAM_MAIN_SOURCE,
PARAM_MAIN_RESET_STATE,
PARAM_MAIN_LAST, PARAM_MAIN_LAST,
}; };
@ -43,51 +45,51 @@ enum ParamsChannelPage {
}; };
const PROGMEM uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") = const PROGMEM uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") =
"\64\0\2\2\3\3\2\3\4\5\5\0\0\5\0\5\0\0\221\0\0\1\230 \4\200\134%\11\255tT" "\64\0\2\2\3\3\2\3\4\5\5\0\0\5\0\5\0\0\221\0\0\1\230 \4\200\134%\11\255tT"
"R\271RI(\6\252\334T\31)\7\252\134bJ\12+\7\233\345\322J\0,\5\221T\4-\5\213" "R\271RI(\6\252\334T\31)\7\252\134bJ\12+\7\233\345\322J\0,\5\221T\4-\5\213"
"f\6.\5\211T\2/\6\244\354c\33\60\10\254\354T\64\223\2\61\7\353\354\222\254\6\62\11\254l" "f\6.\5\211T\2/\6\244\354c\33\60\10\254\354T\64\223\2\61\7\353\354\222\254\6\62\11\254l"
"\66J*\217\0\63\11\254l\66J\32\215\4\64\10\254l\242\34\272\0\65\11\254l\206\336h$\0\66" "\66J*\217\0\63\11\254l\66J\32\215\4\64\10\254l\242\34\272\0\65\11\254l\206\336h$\0\66"
"\11\254\354T^\61)\0\67\10\254lF\216u\4\70\11\254\354TL*&\5\71\11\254\354TL;" "\11\254\354T^\61)\0\67\10\254lF\216u\4\70\11\254\354TL*&\5\71\11\254\354TL;"
")\0:\6\231UR\0A\10\254\354T\34S\6B\11\254lV\34)\216\4C\11\254\354T\324\61" ")\0:\6\231UR\0A\10\254\354T\34S\6B\11\254lV\34)\216\4C\11\254\354T\324\61"
")\0D\10\254lV\64G\2E\10\254l\206\36z\4F\10\254l\206^\71\3G\11\254\354TN" ")\0D\10\254lV\64G\2E\10\254l\206\36z\4F\10\254l\206^\71\3G\11\254\354TN"
"\63)\0H\10\254l\242\34S\6I\6\251T\206\0J\10\254\354k\231\24\0K\11\254l\242J\62" "\63)\0H\10\254l\242\34S\6I\6\251T\206\0J\10\254\354k\231\24\0K\11\254l\242J\62"
"\225\1L\7\254lr{\4M\11\255t\362ZI\353\0N\11\255t\362TI\356\0O\10\254\354T" "\225\1L\7\254lr{\4M\11\255t\362ZI\353\0N\11\255t\362TI\356\0O\10\254\354T"
"\64\223\2P\11\254lV\34)g\0Q\10\254\354T\264b\12R\10\254lV\34\251\31S\11\254\354" "\64\223\2P\11\254lV\34)g\0Q\10\254\354T\264b\12R\10\254lV\34\251\31S\11\254\354"
"FF\32\215\4T\7\253dVl\1U\10\254l\242\63)\0V\11\255t\262Ne\312\21W\12\255" "FF\32\215\4T\7\253dVl\1U\10\254l\242\63)\0V\11\255t\262Ne\312\21W\12\255"
"t\262J*\251.\0X\11\254l\242L*\312\0Y\12\255tr\252\63\312(\2Z\7\253df*" "t\262J*\251.\0X\11\254l\242L*\312\0Y\12\255tr\252\63\312(\2Z\7\253df*"
"\7p\10\255\364V\266\323\2q\7\255\364\216\257\5r\10\253d\242\32*\2t\6\255t\376#w\11" "\7p\10\255\364V\266\323\2q\7\255\364\216\257\5r\10\253d\242\32*\2t\6\255t\376#w\11"
"\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0"; "\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0";
const PROGMEM uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") = const PROGMEM uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") =
"#\0\4\4\4\5\2\1\6\17\30\1\0\27\0\0\0\1\77\0\0\3w%'\17\37\313\330R#&" "#\0\4\4\4\5\2\1\6\17\30\1\0\27\0\0\0\1\77\0\0\3w%'\17\37\313\330R#&"
"\32!F\14\211I\310\24!\65\204(MF\21)Cd\304\10\62b\14\215\60Vb\334\20\0/\14" "\32!F\14\211I\310\24!\65\204(MF\21)Cd\304\10\62b\14\215\60Vb\334\20\0/\14"
"\272\336\336d\244\350\263q\343\0\60\37|\377\216!%*\10\35\263\253ChD\30\21bB\14\242S" "\272\336\336d\244\350\263q\343\0\60\37|\377\216!%*\10\35\263\253ChD\30\21bB\14\242S"
"\306lv\210\204\22Ef\0\61\24z\337\322\60R\205\314\234\31\61F\310\270\371\177\224\42\3\62\33|" "\306lv\210\204\22Ef\0\61\24z\337\322\60R\205\314\234\31\61F\310\270\371\177\224\42\3\62\33|"
"\377\216)\64*\10\35\63\66r\206\304\314`c\252\34\301\221\263|\360\300\0\63\34|\377\216)\64*" "\377\216)\64*\10\35\63\66r\206\304\314`c\252\34\301\221\263|\360\300\0\63\34|\377\216)\64*"
"\10\35\63\66r \71\332YIr\226\306\16\221P\203\312\14\0\64 |\377\226\220AC\306\20\31B" "\10\35\63\66r \71\332YIr\226\306\16\221P\203\312\14\0\64 |\377\226\220AC\306\20\31B"
"f\310\240\21\204F\214\32\61j\304(cv\366\200\305\312\371\0\65\32|\377\206\212-F\316\27\204\224" "f\310\240\21\204F\214\32\61j\304(cv\366\200\305\312\371\0\65\32|\377\206\212-F\316\27\204\224"
"\254\30\65t\344,\215\35\42\241\6\225\31\0\66\33}\17\317\251\64+\206\235\63:/\314,aA\352" "\254\30\65t\344,\215\35\42\241\6\225\31\0\66\33}\17\317\251\64+\206\235\63:/\314,aA\352"
"\234\335\235\42\261&\325\31\0\67\23|\377\302\212\7)\347Crt\70\345\300\221\363\16\0\70 |\377" "\234\335\235\42\261&\325\31\0\67\23|\377\302\212\7)\347Crt\70\345\300\221\363\16\0\70 |\377"
"\216)\64*\10\35\263\354\20\11\42d\20\235BC\204\4\241cvv\210\204\32Tf\0\71\32|\377" "\216)\64*\10\35\263\354\20\11\42d\20\235BC\204\4\241cvv\210\204\32Tf\0\71\32|\377"
"\216)\64*\10\35\263\263C$\226\250I\71_\14\42\241\6\225\31\0A\26}\17S\271Si(\31" "\216)\64*\10\35\263\263C$\226\250I\71_\14\42\241\6\225\31\0A\26}\17S\271Si(\31"
"\65d\324\210q\366\356\301w\366\273\1B$}\17C\42\65KF\221\30\66b\330\210a#\206\215\30" "\65d\324\210q\366\356\301w\366\273\1B$}\17C\42\65KF\221\30\66b\330\210a#\206\215\30"
"Eb\311&\243H\14;g\317\36\204`\261\4\0C\27}\17\317\251\64K\10!\63:\377\247\304F" "Eb\311&\243H\14;g\317\36\204`\261\4\0C\27}\17\317\251\64K\10!\63:\377\247\304F"
"\20\42\261F\21\22\0D\33}\17C\42\65KF\15\31\66b\330\210q\366\77;\66b\24\211%j" "\20\42\261F\21\22\0D\33}\17C\42\65KF\15\31\66b\330\210q\366\77;\66b\24\211%j"
"\22\1E\21|\377\302\7)\347%\42\214F\316/\37<\60F\20|\377\302\7)\347\313\64\331\214\234" "\22\1E\21|\377\302\7)\347%\42\214F\316/\37<\60F\20|\377\302\7)\347\313\64\331\214\234"
"\177\11\0G\31\216\37\17*\65L\206\35\264v>\322\241\15\217\221 \65\204\215\262\63\0H\17|\377" "\177\11\0G\31\216\37\17*\65L\206\35\264v>\322\241\15\217\221 \65\204\215\262\63\0H\17|\377"
"\302\60\373g\17\36\60\263\177\66\0I\7so\302\37$J\22|\377\346\374\377\322\230\261C\210H\250" "\302\60\373g\17\36\60\263\177\66\0I\7so\302\37$J\22|\377\346\374\377\322\230\261C\210H\250"
"Ae\6\0K\42|\377\302\60S\247F\14\42\61h\310\30\42c&!\63\202\320\251\64JV\14\42" "Ae\6\0K\42|\377\302\60S\247F\14\42\61h\310\30\42c&!\63\202\320\251\64JV\14\42"
"\61\352\230\375l\0L\15{\357\302\300\371\377\37>x\60\0M$}\17\203\310r\346N\245Q\263\202" "\61\352\230\375l\0L\15{\357\302\300\371\377\37>x\60\0M$}\17\203\310r\346N\245Q\263\202"
"E\12)L\224\60Q\302\310\20#C\214\14\61\23\306L\30s\366\335\0N#}\17\203@s\346\216" "E\12)L\224\60Q\302\310\20#C\214\14\61\23\306L\30s\366\335\0N#}\17\203@s\346\216"
"\35C\205*Q\42\23cL\214\61\62\304\310\20\63#\314\214\60\224\25f\327\231\33O\26}\17\317\251" "\35C\205*Q\42\23cL\214\61\62\304\310\20\63#\314\214\60\224\25f\327\231\33O\26}\17\317\251"
"\64KF\215\30g\377\337\215\30\65dM\252\63\0P\26|\377B\32%+F\35\263W\207H\254H" "\64KF\215\30g\377\337\215\30\65dM\252\63\0P\26|\377B\32%+F\35\263W\207H\254H"
"\203h\344\374%\0Q\31}\17S\261\64KF\215\30g\377oF\230\31q\246\210\42E%F\0R" "\203h\344\374%\0Q\31}\17S\261\64KF\215\30g\377oF\230\31q\246\210\42E%F\0R"
"\61\216\37\203\242\65L\206\221\30\67b\334\210q#\306\215\30\67b\30\211QD\230(J\65d\330\230" "\61\216\37\203\242\65L\206\221\30\67b\334\210q#\306\215\30\67b\30\211QD\230(J\65d\330\230"
"Qc\10\315j\314(\42\303H\214\33\61\356\340\0S!\216\37\317\261DKH\221\30\67b\334\210\261" "Qc\10\315j\314(\42\303H\214\33\61\356\340\0S!\216\37\317\261DKH\221\30\67b\334\210\261"
"c)M\246Ji\331\331\32\64\207\212D\223Uh\0T\15}\17\303\7\251\206\316\377\377\12\0U\21" "c)M\246Ji\331\331\32\64\207\212D\223Uh\0T\15}\17\303\7\251\206\316\377\377\12\0U\21"
"|\377\302\60\373\377\317F\14\32\242\6\225\31\0X)~\37\303@\203\307H\14\33B\210\14\21RC" "|\377\302\60\373\377\317F\14\32\242\6\225\31\0X)~\37\303@\203\307H\14\33B\210\14\21RC"
"\206\241\63h\222(I\203\346\220\15\31E\204\14!\42\303F\20;h\341\0x\24\312\336\302 CG" "\206\241\63h\222(I\203\346\220\15\31E\204\14!\42\303F\20;h\341\0x\24\312\336\302 CG"
"H\240\61E\312\14\222)\6Y\64\0\0\0\0\4\377\377\0"; "H\240\61E\312\14\222)\6Y\64\0\0\0\0\4\377\377\0";
#define play_icon_width 14 #define play_icon_width 14
#define play_icon_height 14 #define play_icon_height 14
@ -110,10 +112,7 @@ void setup() {
// Initialize the state manager. This will load settings from EEPROM // Initialize the state manager. This will load settings from EEPROM
stateManager.initialize(app); stateManager.initialize(app);
InitAppState(app);
// Apply the loaded state to the hardware
gravity.clock.SetTempo(app.tempo);
gravity.clock.SetSource(app.selected_source);
// Clock handlers. // Clock handlers.
gravity.clock.AttachIntHandler(HandleIntClockTick); gravity.clock.AttachIntHandler(HandleIntClockTick);
@ -136,7 +135,7 @@ void loop() {
// Read CVs and call the update function for each channel. // Read CVs and call the update function for each channel.
int cv1 = gravity.cv1.Read(); int cv1 = gravity.cv1.Read();
int cv2 = gravity.cv2.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); app.channel[i].applyCvMod(cv1, cv2);
} }
@ -151,7 +150,7 @@ void loop() {
void HandleIntClockTick(uint32_t tick) { void HandleIntClockTick(uint32_t tick) {
bool refresh = false; 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]); app.channel[i].processClockTick(tick, gravity.outputs[i]);
if (app.channel[i].isCvModActive()) { if (app.channel[i].isCvModActive()) {
@ -192,6 +191,20 @@ void HandleShiftPressed() {
} }
void HandleEncoderPressed() { 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; app.editing_param = !app.editing_param;
stateManager.save(app); stateManager.save(app);
app.refresh_screen = true; app.refresh_screen = true;
@ -215,7 +228,7 @@ void HandleRotate(Direction dir, int val) {
} }
void HandlePressedRotate(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++; app.selected_channel++;
} else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) { } else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) {
app.selected_channel--; app.selected_channel--;
@ -237,11 +250,14 @@ void editMainParameter(int val) {
case PARAM_MAIN_SOURCE: { case PARAM_MAIN_SOURCE: {
int source = static_cast<int>(app.selected_source); int source = static_cast<int>(app.selected_source);
updateSelection(source, val, SOURCE_LAST); updateSelection(source, val, Clock::SOURCE_LAST);
app.selected_source = static_cast<Source>(source); app.selected_source = static_cast<Clock::Source>(source);
gravity.clock.SetSource(app.selected_source); gravity.clock.SetSource(app.selected_source);
break; 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. // Helper functions.
// //
void InitAppState(AppState& app) {
gravity.clock.SetTempo(app.tempo);
gravity.clock.SetSource(app.selected_source);
}
Channel& GetSelectedChannel() { Channel& GetSelectedChannel() {
return app.channel[app.selected_channel - 1]; return app.channel[app.selected_channel - 1];
} }
void ResetOutputs() { void ResetOutputs() {
for (int i = 0; i < OUTPUT_COUNT; i++) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
gravity.outputs[i].Low(); gravity.outputs[i].Low();
} }
} }
@ -336,7 +357,7 @@ void DisplayMainPage() {
switch (app.selected_param) { switch (app.selected_param) {
case PARAM_MAIN_TEMPO: case PARAM_MAIN_TEMPO:
// Serial MIDI is too unstable to display bpm in real time. // 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"); sprintf(mainText, "%s", "EXT");
} else { } else {
sprintf(mainText, "%d", gravity.clock.Tempo()); sprintf(mainText, "%d", gravity.clock.Tempo());
@ -345,31 +366,35 @@ void DisplayMainPage() {
break; break;
case PARAM_MAIN_SOURCE: case PARAM_MAIN_SOURCE:
switch (app.selected_source) { switch (app.selected_source) {
case SOURCE_INTERNAL: case Clock::SOURCE_INTERNAL:
sprintf(mainText, "%s", "INT"); sprintf(mainText, "%s", "INT");
subText = "CLOCK"; subText = "CLOCK";
break; break;
case SOURCE_EXTERNAL_PPQN_24: case Clock::SOURCE_EXTERNAL_PPQN_24:
sprintf(mainText, "%s", "EXT"); sprintf(mainText, "%s", "EXT");
subText = "24 PPQN"; subText = "24 PPQN";
break; break;
case SOURCE_EXTERNAL_PPQN_4: case Clock::SOURCE_EXTERNAL_PPQN_4:
sprintf(mainText, "%s", "EXT"); sprintf(mainText, "%s", "EXT");
subText = "4 PPQN"; subText = "4 PPQN";
break; break;
case SOURCE_EXTERNAL_MIDI: case Clock::SOURCE_EXTERNAL_MIDI:
sprintf(mainText, "%s", "EXT"); sprintf(mainText, "%s", "EXT");
subText = "MIDI"; subText = "MIDI";
break; break;
} }
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(mainText, MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT); drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
// Draw Main Page menu items // 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); drawMenuItems(menu_items, PARAM_MAIN_LAST);
} }
@ -475,7 +500,7 @@ void DisplaySelectedChannel() {
gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2); gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2);
gravity.display.drawVLine(SCREEN_WIDTH - 2, boxY, boxHeight); 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. // Draw box frame or filled selected box.
gravity.display.setDrawColor(1); gravity.display.setDrawColor(1);
(app.selected_channel == i) (app.selected_channel == i)

View File

@ -2,17 +2,19 @@
#define APP_STATE_H #define APP_STATE_H
#include <gravity.h> #include <gravity.h>
#include "channel.h" #include "channel.h"
// Global state for settings and app behavior. // Global state for settings and app behavior.
struct AppState { struct AppState {
int tempo = 120; int tempo = Clock::DEFAULT_TEMPO;
bool refresh_screen = true; bool refresh_screen = true;
bool editing_param = false; bool editing_param = false;
int selected_param = 0; int selected_param = 0;
int selected_sub_param = 0;
byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_channel = 0; // 0=tempo, 1-6=output channel
Source selected_source = SOURCE_INTERNAL; Clock::Source selected_source = Clock::SOURCE_INTERNAL;
Channel channel[OUTPUT_COUNT]; Channel channel[Gravity::OUTPUT_COUNT];
}; };
#endif // APP_STATE_H #endif // APP_STATE_H

View File

@ -30,10 +30,24 @@ static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48,
class Channel { class Channel {
public: public:
Channel() { 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_clock_mod_index = base_clock_mod_index;
cvmod_probability = base_probability; cvmod_probability = base_probability;
cvmod_duty_cycle = base_duty_cycle; cvmod_duty_cycle = base_duty_cycle;
cvmod_offset = base_offset; cvmod_offset = base_offset;
duty_cycle_pulses = 0;
offset_pulses = 0;
} }
// Setters (Set the BASE value) // Setters (Set the BASE value)
@ -121,11 +135,11 @@ class Channel {
} }
private: private:
// User-settable "base" values. // User-settable base values.
byte base_clock_mod_index = 7; byte base_clock_mod_index;
byte base_probability = 100; byte base_probability;
byte base_duty_cycle = 50; byte base_duty_cycle;
byte base_offset = 0; byte base_offset;
// Base value with cv mod applied. // Base value with cv mod applied.
byte cvmod_clock_mod_index; byte cvmod_clock_mod_index;
@ -133,8 +147,8 @@ class Channel {
byte cvmod_duty_cycle; byte cvmod_duty_cycle;
byte cvmod_offset; byte cvmod_offset;
int duty_cycle_pulses; uint32_t duty_cycle_pulses;
int offset_pulses; uint32_t offset_pulses;
// CV configuration // CV configuration
CvSource cv_source = CV_NONE; CvSource cv_source = CV_NONE;

View File

@ -1,23 +1,24 @@
// File: save_state.cpp #include "save_state.h"
#include <EEPROM.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) { bool StateManager::initialize(AppState& app) {
if (isDataValid()) { if (isDataValid()) {
EepromData loadedState; EepromData load_data;
EEPROM.get(sizeof(Metadata), loadedState); EEPROM.get(sizeof(Metadata), load_data);
// Restore main app state // Restore main app state
app.selected_param = loadedState.selected_param; app.tempo = load_data.tempo;
app.selected_channel = loadedState.selected_channel; app.selected_param = load_data.selected_param;
app.selected_source = static_cast<Source>(loadedState.selected_source); 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 --- // Loop through and restore each channel's state.
for (int i = 0; i < OUTPUT_COUNT; i++) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
auto& ch = app.channel[i]; // Get a reference to the channel object auto& ch = app.channel[i];
const auto& saved_ch_state = loadedState.channel_data[i]; // Get a const reference to the saved data const auto& saved_ch_state = load_data.channel_data[i];
ch.setClockMod(saved_ch_state.base_clock_mod_index); ch.setClockMod(saved_ch_state.base_clock_mod_index);
ch.setProbability(saved_ch_state.base_probability); ch.setProbability(saved_ch_state.base_probability);
@ -30,23 +31,24 @@ bool StateManager::initialize(AppState& app) {
return true; return true;
} else { } else {
writeMetadata(); writeMetadata();
save(app); // Save the initial default state save(app); // Save the initial default state
return false; return false;
} }
} }
void StateManager::save(const AppState& app) { void StateManager::save(const AppState& app) {
EepromData stateToSave; EepromData save_data;
// Populate main app state // Populate main app state
stateToSave.selected_param = app.selected_param; save_data.tempo = app.tempo;
stateToSave.selected_channel = app.selected_channel; save_data.selected_param = app.selected_param;
stateToSave.selected_source = static_cast<byte>(app.selected_source); 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 --- // Loop through and populate each channel's state
for (int i = 0; i < OUTPUT_COUNT; i++) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
const auto& ch = app.channel[i]; // Get a const reference to the channel object const auto& ch = app.channel[i];
auto& saved_ch_state = stateToSave.channel_data[i]; // Get a reference to the struct we're saving to auto& saved_ch_state = save_data.channel_data[i];
// Use the getters with 'withCvMod = false' to get the base values // 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_clock_mod_index = ch.getClockModIndex(false);
@ -58,28 +60,34 @@ void StateManager::save(const AppState& app) {
} }
// Write the entire state struct to EEPROM // Write the entire state struct to EEPROM
EEPROM.put(sizeof(Metadata), stateToSave); EEPROM.put(sizeof(Metadata), save_data);
} }
void StateManager::reset(AppState& app) { void StateManager::reset(AppState& app) {
AppState defaultState; app.tempo = Clock::DEFAULT_TEMPO;
app = defaultState; 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(); writeMetadata();
save(app); save(app);
} }
// isDataValid() and writeMetadata() remain unchanged
bool StateManager::isDataValid() { bool StateManager::isDataValid() {
Metadata storedMeta; Metadata load_meta;
EEPROM.get(0, storedMeta); EEPROM.get(0, load_meta);
bool nameMatch = (strcmp(storedMeta.sketchName, CURRENT_SKETCH_NAME) == 0); bool nameMatch = (strcmp(load_meta.sketchName, CURRENT_SKETCH_NAME) == 0);
bool versionMatch = (storedMeta.version == CURRENT_SKETCH_VERSION); bool versionMatch = (load_meta.version == CURRENT_SKETCH_VERSION);
return nameMatch && versionMatch; return nameMatch && versionMatch;
} }
void StateManager::writeMetadata() { void StateManager::writeMetadata() {
Metadata currentMeta; Metadata save_meta;
strcpy(currentMeta.sketchName, CURRENT_SKETCH_NAME); strcpy(save_meta.sketchName, CURRENT_SKETCH_NAME);
currentMeta.version = CURRENT_SKETCH_VERSION; save_meta.version = CURRENT_SKETCH_VERSION;
EEPROM.put(0, currentMeta); EEPROM.put(0, save_meta);
} }

View File

@ -1,31 +1,26 @@
// File: save_state.h
#ifndef SAVE_STATE_H #ifndef SAVE_STATE_H
#define SAVE_STATE_H #define SAVE_STATE_H
#include <Arduino.h> #include <Arduino.h>
#include <gravity.h> // We need this for OUTPUT_COUNT #include <gravity.h>
// Forward-declare AppState to avoid circular dependencies. // Forward-declare AppState to avoid circular dependencies.
struct AppState; struct AppState;
// Forward-declare the Source enum as well.
enum Source;
// Define the constants for the current firmware. // Define the constants for the current firmware.
const char CURRENT_SKETCH_NAME[] = "Gravity"; 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. * @brief Manages saving and loading of the application state to and from EEPROM.
*/ */
class StateManager { class StateManager {
public: public:
bool initialize(AppState& app); bool initialize(AppState& app);
void save(const AppState& app); void save(const AppState& app);
void reset(AppState& app); void reset(AppState& app);
private: private:
// This struct holds the data that identifies the firmware version. // This struct holds the data that identifies the firmware version.
struct Metadata { struct Metadata {
char sketchName[16]; char sketchName[16];
@ -36,19 +31,20 @@ private:
byte base_probability; byte base_probability;
byte base_duty_cycle; byte base_duty_cycle;
byte base_offset; byte base_offset;
byte cv_source; // We'll store the CvSource enum as a byte byte cv_source; // We'll store the CvSource enum as a byte
byte cv_destination; // We'll store the CvDestination 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. // This struct holds all the parameters we want to save.
struct EepromData { struct EepromData {
int selected_param; int tempo;
byte selected_param;
byte selected_channel; byte selected_channel;
byte selected_source; byte selected_source;
ChannelState channel_data[OUTPUT_COUNT]; ChannelState channel_data[Gravity::OUTPUT_COUNT];
}; };
bool isDataValid(); bool isDataValid();
void writeMetadata(); void writeMetadata();
}; };
#endif // SAVE_STATE_H #endif // SAVE_STATE_H

View File

@ -14,6 +14,8 @@
// Hardware abstraction wrapper for the Gravity module. // Hardware abstraction wrapper for the Gravity module.
class Gravity { class Gravity {
public: public:
static const uint8_t OUTPUT_COUNT = 6;
// Constructor // Constructor
Gravity() Gravity()
: display(U8G2_R2, SCL, SDA, U8X8_PIN_NONE) {} : display(U8G2_R2, SCL, SDA, U8X8_PIN_NONE) {}

View File

@ -39,6 +39,4 @@
#define OUT_CH5 9 #define OUT_CH5 9
#define OUT_CH6 11 #define OUT_CH6 11
const uint8_t OUTPUT_COUNT = 6;
#endif #endif