From d12764313bee2fb3fe3cb96a4066a82f6a68c71a Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 16 Jun 2025 02:47:25 +0000 Subject: [PATCH] Introduce StateManager to persist state between power cycles (#6) - add reset state menu option to return all settings back to default values. - add reverse encoder menu option and save state - make saving to EEPROM safer by wrapping put calls with noInterrupts() - improve save state behavior by using a mutex flag and update check with debounce in main loop - refactor gravity.h global const definitions to be static and more readable. - improve usage of EncoderDir in ISR with pointer to instance and static isr() method. - reduce u8g2 memory usage by using single page buffer Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/6 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- clock.h | 20 ++-- encoder_dir.h | 23 ++-- examples/Gravity/Gravity.ino | 182 ++++++++++++++++++++------------ examples/Gravity/app_state.h | 21 ++++ examples/Gravity/channel.h | 28 +++-- examples/Gravity/save_state.cpp | 119 +++++++++++++++++++++ examples/Gravity/save_state.h | 65 ++++++++++++ gravity.cpp | 19 ++-- gravity.h | 4 +- peripherials.h | 2 - 10 files changed, 378 insertions(+), 105 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/clock.h b/clock.h index 2a0e54b..19ed250 100644 --- a/clock.h +++ b/clock.h @@ -23,22 +23,22 @@ #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) {} -enum Source { - SOURCE_INTERNAL, - SOURCE_EXTERNAL_PPQN_24, - SOURCE_EXTERNAL_PPQN_4, - SOURCE_EXTERNAL_MIDI, - SOURCE_LAST, -}; - class Clock { 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() { NeoSerial.begin(31250); diff --git a/encoder_dir.h b/encoder_dir.h index dbc3c56..5375b57 100644 --- a/encoder_dir.h +++ b/encoder_dir.h @@ -34,7 +34,9 @@ class EncoderDir { public: EncoderDir() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3), - button_(ENCODER_SW_PIN) {} + button_(ENCODER_SW_PIN) { + _instance = this; + } ~EncoderDir() {} // Set to true if the encoder read direction should be reversed. @@ -81,15 +83,19 @@ class EncoderDir { } } - // Read the encoder state and update the read position. - void UpdateEncoder() { - encoder_.tick(); + static void isr() { + // If the instance has been created, call its tick() method. + if (_instance) { + _instance->encoder_.tick(); + } } private: + static EncoderDir* _instance; + int previous_pos_; bool rotated_while_held_; - bool reversed_ = true; + bool reversed_ = false; RotaryEncoder encoder_; Button button_; @@ -115,15 +121,18 @@ class EncoderDir { change *= 2; } + if (reversed_) { + change = -(change); + } return change; } inline Direction rotate_(int dir, bool reversed) { switch (dir) { case 1: - return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT; - case -1: return (reversed) ? DIRECTION_DECREMENT : DIRECTION_INCREMENT; + case -1: + return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT; default: return DIRECTION_UNCHANGED; } diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index f03adea..f3502ea 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 @@ -19,22 +19,19 @@ #include +#include "app_state.h" #include "channel.h" +#include "save_state.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, + PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_RESET_STATE, PARAM_MAIN_LAST, }; @@ -48,52 +45,52 @@ enum ParamsChannelPage { PARAM_CH_LAST, }; -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" - "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" - "\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;" - ")\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" - "\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" - "\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" - "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" - "\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0"; +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" + "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" + "\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;" + ")\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" + "\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" + "\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" + "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" + "\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") = - "#\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" - "\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|" - "\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" - "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" - "\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\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" - "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" - "\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" - "\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" - "\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" - "\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" - "\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" - "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" - "|\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" - "H\240\61E\312\14\222)\6Y\64\0\0\0\0\4\377\377\0"; +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#&" + "\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" + "\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*" + "\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" + "\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" + "\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" + "\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" + "\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" + "\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" + "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" + "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" + "\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" + "\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" + "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" + "\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"; #define play_icon_width 14 #define play_icon_height 14 @@ -114,6 +111,10 @@ void setup() { // Start Gravity. gravity.Init(); + // Initialize the state manager. This will load settings from EEPROM + stateManager.initialize(app); + InitAppState(app); + // Clock handlers. gravity.clock.AttachIntHandler(HandleIntClockTick); gravity.clock.AttachExtHandler(HandleExtClockTick); @@ -135,10 +136,13 @@ 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); } + // Check for dirty state eligible to be saved. + stateManager.update(app); + if (app.refresh_screen) { UpdateDisplay(); } @@ -150,7 +154,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()) { @@ -191,6 +195,25 @@ void HandleShiftPressed() { } void HandleEncoderPressed() { + // Check if leaving editing mode should apply a selection. + if (app.editing_param) { + if (app.selected_channel == 0) { // main page + if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { + bool reversed = app.selected_sub_param == 1; + gravity.encoder.SetReverseDirection(reversed); + } + // Reset state + if (app.selected_param == PARAM_MAIN_RESET_STATE) { + if (app.selected_sub_param == 0) { // Reset + stateManager.reset(app); + InitAppState(app); + } + } + } + // Only mark dirty when leaving editing mode. + stateManager.markDirty(); + } + app.selected_sub_param = 0; app.editing_param = !app.editing_param; app.refresh_screen = true; } @@ -212,12 +235,13 @@ 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--; } app.selected_param = 0; + stateManager.markDirty(); app.refresh_screen = true; } @@ -228,15 +252,22 @@ void editMainParameter(int val) { break; } gravity.clock.SetTempo(gravity.clock.Tempo() + val); + app.tempo = gravity.clock.Tempo(); break; case PARAM_MAIN_SOURCE: { int source = static_cast(app.selected_source); - updateSelection(source, val, SOURCE_LAST); - app.selected_source = static_cast(source); + updateSelection(source, val, Clock::SOURCE_LAST); + app.selected_source = static_cast(source); gravity.clock.SetSource(app.selected_source); break; } + case PARAM_MAIN_ENCODER_DIR: + updateSelection(app.selected_sub_param, val, 2); + break; + case PARAM_MAIN_RESET_STATE: + updateSelection(app.selected_sub_param, val, 2); + break; } } @@ -279,12 +310,18 @@ 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); + gravity.encoder.SetReverseDirection(app.encoder_reversed); +} + 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(); } } @@ -331,7 +368,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()); @@ -340,30 +377,39 @@ 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_ENCODER_DIR: + sprintf(mainText, "%s", "DIR"); + subText = app.selected_sub_param == 0 ? "DEFAULT" : "REVERSED"; + 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", "ENCODER DIR", "RESET"}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -469,7 +515,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) diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h new file mode 100644 index 0000000..a70661a --- /dev/null +++ b/examples/Gravity/app_state.h @@ -0,0 +1,21 @@ +#ifndef APP_STATE_H +#define APP_STATE_H + +#include + +#include "channel.h" + +// Global state for settings and app behavior. +struct AppState { + int tempo = Clock::DEFAULT_TEMPO; + bool encoder_reversed = false; + 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 + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Channel channel[Gravity::OUTPUT_COUNT]; +}; + +#endif // APP_STATE_H \ No newline at end of file diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index a558adf..f5999b7 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.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; diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp new file mode 100644 index 0000000..c214c63 --- /dev/null +++ b/examples/Gravity/save_state.cpp @@ -0,0 +1,119 @@ +#include "save_state.h" + +#include + +#include "app_state.h" + +StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} + +bool StateManager::initialize(AppState& app) { + if (isDataValid()) { + static EepromData load_data; + EEPROM.get(sizeof(Metadata), load_data); + + // Restore main app state + app.tempo = load_data.tempo; + app.encoder_reversed = load_data.encoder_reversed; + app.selected_param = load_data.selected_param; + app.selected_channel = load_data.selected_channel; + app.selected_source = static_cast(load_data.selected_source); + + // 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); + 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 { + reset(app); + return false; + } +} + +void StateManager::save(const AppState& app) { + // Ensure interrupts do not cause corrupt data writes. + noInterrupts(); + _save_worker(app); + interrupts(); +} + +void StateManager::reset(AppState& app) { + app.tempo = Clock::DEFAULT_TEMPO; + app.encoder_reversed = false; + 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(); + } + + noInterrupts(); + _metadata_worker(); // Write the new metadata + _save_worker(app); // Write the new (default) app state + interrupts(); + + _isDirty = false; +} + +void StateManager::update(const AppState& app) { + // Check if a save is pending and if enough time has passed. + if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { + save(app); + _isDirty = false; // Clear the flag, we are now "clean". + } +} + +void StateManager::markDirty() { + _isDirty = true; + _lastChangeTime = millis(); +} + +bool StateManager::isDataValid() { + 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::_save_worker(const AppState& app) { + static EepromData save_data; + + // Populate main app state + save_data.tempo = app.tempo; + save_data.encoder_reversed = app.encoder_reversed; + save_data.selected_param = app.selected_param; + save_data.selected_channel = app.selected_channel; + save_data.selected_source = static_cast(app.selected_source); + + // Loop through and populate each channel's state + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + const auto& ch = app.channel[i]; + auto& save_ch = save_data.channel_data[i]; + + // Use the getters with 'withCvMod = false' to get the base values + save_ch.base_clock_mod_index = ch.getClockModIndex(false); + save_ch.base_probability = ch.getProbability(false); + save_ch.base_duty_cycle = ch.getDutyCycle(false); + save_ch.base_offset = ch.getOffset(false); + save_ch.cv_source = static_cast(ch.getCvSource()); + save_ch.cv_destination = static_cast(ch.getCvDestination()); + } + EEPROM.put(sizeof(Metadata), save_data); +} + +void StateManager::_metadata_worker() { + Metadata currentMeta; + strcpy(currentMeta.sketchName, CURRENT_SKETCH_NAME); + currentMeta.version = CURRENT_SKETCH_VERSION; + EEPROM.put(0, currentMeta); +} diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h new file mode 100644 index 0000000..5ca8823 --- /dev/null +++ b/examples/Gravity/save_state.h @@ -0,0 +1,65 @@ +#ifndef SAVE_STATE_H +#define SAVE_STATE_H + +#include +#include + +// Forward-declare AppState to avoid circular dependencies. +struct AppState; + +// Define the constants for the current firmware. +const char CURRENT_SKETCH_NAME[] = "Gravity"; +const float CURRENT_SKETCH_VERSION = 0.2f; + +/** + * @brief Manages saving and loading of the application state to and from EEPROM. + */ +class StateManager { + public: + StateManager(); + + // Populate the AppState instance with values from EEPROM if they exist. + bool initialize(AppState& app); + // Reset AppState instance back to default values. + void reset(AppState& app); + // Call from main loop, check if state has changed and needs to be saved. + void update(const AppState& app); + // Indicate that state has changed and we should save. + void markDirty(); + + private: + // This struct holds the data that identifies the firmware version. + struct Metadata { + char sketchName[16]; + byte version; + }; + struct ChannelState { + byte base_clock_mod_index; + byte base_probability; + byte base_duty_cycle; + byte base_offset; + byte cv_source; // Cast the CvSource enum to a byte for storage + byte cv_destination; // Cast the CvDestination enum as a byte for storage + }; + // This struct holds all the parameters we want to save. + struct EepromData { + int tempo; + bool encoder_reversed; + byte selected_param; + byte selected_channel; + byte selected_source; + ChannelState channel_data[Gravity::OUTPUT_COUNT]; + }; + + void save(const AppState& app); + + bool isDataValid(); + void _save_worker(const AppState& app); + void _metadata_worker(); + + bool _isDirty; + unsigned long _lastChangeTime; + static const unsigned long SAVE_DELAY_MS = 2000; +}; + +#endif // SAVE_STATE_H \ No newline at end of file diff --git a/gravity.cpp b/gravity.cpp index 00efe16..3d287a9 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -11,6 +11,10 @@ #include "gravity.h" +// Initialize the static pointer for the EncoderDir class to null. We want to +// have a static pointer to decouple the ISR from the global gravity object. +EncoderDir* EncoderDir::_instance = nullptr; + void Gravity::Init() { initClock(); initInputs(); @@ -68,18 +72,13 @@ void Gravity::Process() { } } -void ReadEncoder() { - gravity.encoder.UpdateEncoder(); -} - -// Define Encoder pin ISR. -// Pin Change Interrupt on Port C (D17/A3). -ISR(PCINT2_vect) { - ReadEncoder(); -}; // Pin Change Interrupt on Port D (D4). +ISR(PCINT2_vect) { + EncoderDir::isr(); +}; +// Pin Change Interrupt on Port C (D17/A3). ISR(PCINT1_vect) { - ReadEncoder(); + EncoderDir::isr(); }; // Singleton diff --git a/gravity.h b/gravity.h index 9bba82d..2002616 100644 --- a/gravity.h +++ b/gravity.h @@ -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) {} @@ -27,7 +29,7 @@ class Gravity { // Polling check for state change of inputs and outputs. void Process(); - U8G2_SSD1306_128X64_NONAME_2_HW_I2C display; // OLED display object. + U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object. Clock clock; // Clock source wrapper. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. EncoderDir encoder; // Rotary encoder with button instance diff --git a/peripherials.h b/peripherials.h index 8f5b68d..5c4ec91 100644 --- a/peripherials.h +++ b/peripherials.h @@ -39,6 +39,4 @@ #define OUT_CH5 9 #define OUT_CH6 11 -const uint8_t OUTPUT_COUNT = 6; - #endif