From 6fb61a85bb9504a5f2fed774884f9bea7aceb123 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 20 Jul 2025 10:39:44 -0700 Subject: [PATCH 01/25] When loading EEPROM, if the metadata is not valid, erase all EEPROM. --- firmware/Gravity/save_state.cpp | 12 +++++++++++- firmware/Gravity/save_state.h | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 052779b..16ef342 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -26,7 +26,8 @@ bool StateManager::initialize(AppState& app) { return loadData(app, MAX_SAVE_SLOTS); } else { // EEPROM does not contain save data for this firmware & version. - // Initialize eeprom and save default patter to all save slots. + // Erase EEPROM and initialize state. Save default pattern to all save slots. + factoryReset(); reset(app); _saveMetadata(); // MAX_SAVE_SLOTS slot is reserved for transient state. @@ -82,6 +83,15 @@ void StateManager::markDirty() { _lastChangeTime = millis(); } +// Erases all data in the EEPROM by writing 0 to every address. +void StateManager::factoryReset() { + noInterrupts(); + for (unsigned int i = 0 ; i < EEPROM.length() ; i++) { + EEPROM.write(i, 0); + } + interrupts(); +} + bool StateManager::_isDataValid() { Metadata load_meta; EEPROM.get(0, load_meta); diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 354253a..29ae845 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -19,8 +19,8 @@ struct AppState; // Define the constants for the current firmware. -const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 7; +const char SKETCH_NAME[] = "AltGravity"; +const byte SKETCH_VERSION = 1; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; @@ -52,6 +52,8 @@ class StateManager { void update(const AppState& app); // Indicate that state has changed and we should save. void markDirty(); + // Erase all data stored in the EEPROM. + void factoryReset(); // This struct holds the data that identifies the firmware version. struct Metadata { -- 2.39.5 From 78cf5e0f3325c201270e574481d4e9b8349d1984 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 20 Jul 2025 10:56:56 -0700 Subject: [PATCH 02/25] Add Factory Reset to the global menu --- firmware/Gravity/Gravity.ino | 9 +++++++++ firmware/Gravity/app_state.h | 24 ---------------------- firmware/Gravity/display.h | 39 +++++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 374ee03..06934f9 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -203,6 +203,12 @@ void HandleEncoderPressed() { InitGravity(app); } } + if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { + if (app.selected_sub_param == 0) { // Reset + stateManager.factoryReset(); + InitGravity(app); + } + } } // Only mark dirty and reset selected_sub_param when leaving editing mode. stateManager.markDirty(); @@ -277,6 +283,9 @@ void editMainParameter(int val) { case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); break; + case PARAM_MAIN_FACTORY_RESET: + updateSelection(app.selected_sub_param, val, 2); + break; } } diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index e7d9ab5..0ddd014 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -38,28 +38,4 @@ static Channel& GetSelectedChannel() { return app.channel[app.selected_channel - 1]; } -enum ParamsMainPage : uint8_t { - PARAM_MAIN_TEMPO, - PARAM_MAIN_SOURCE, - PARAM_MAIN_PULSE, - PARAM_MAIN_ENCODER_DIR, - PARAM_MAIN_SAVE_DATA, - PARAM_MAIN_LOAD_DATA, - PARAM_MAIN_RESET_STATE, - PARAM_MAIN_LAST, -}; - -enum ParamsChannelPage : uint8_t { - PARAM_CH_MOD, - PARAM_CH_PROB, - PARAM_CH_DUTY, - PARAM_CH_OFFSET, - PARAM_CH_SWING, - PARAM_CH_EUC_STEPS, - PARAM_CH_EUC_HITS, - PARAM_CH_CV1_DEST, - PARAM_CH_CV2_DEST, - PARAM_CH_LAST, -}; - #endif // APP_STATE_H \ No newline at end of file diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index cf37631..1823b75 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -96,6 +96,33 @@ constexpr uint8_t CHANNEL_BOXES_Y = 50; constexpr uint8_t CHANNEL_BOX_WIDTH = 18; constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; +// Menu items for editing global parameters. +enum ParamsMainPage : uint8_t { + PARAM_MAIN_TEMPO, + PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, + PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_SAVE_DATA, + PARAM_MAIN_LOAD_DATA, + PARAM_MAIN_RESET_STATE, + PARAM_MAIN_FACTORY_RESET, + PARAM_MAIN_LAST, +}; + +// Menu items for editing channel parameters. +enum ParamsChannelPage : uint8_t { + PARAM_CH_MOD, + PARAM_CH_PROB, + PARAM_CH_DUTY, + PARAM_CH_OFFSET, + PARAM_CH_SWING, + PARAM_CH_EUC_STEPS, + PARAM_CH_EUC_HITS, + PARAM_CH_CV1_DEST, + PARAM_CH_CV2_DEST, + PARAM_CH_LAST, +}; + // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { gravity.display.setFont(font); @@ -278,13 +305,23 @@ void DisplayMainPage() { mainText = F("x"); subText = F("BACK TO MAIN"); } + break; + case PARAM_MAIN_FACTORY_RESET: + if (app.selected_sub_param == 0) { + mainText = F("DEL"); + subText = F("FACTORY RESET"); + } else { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } + break; } drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } -- 2.39.5 From 5729eef037d31981e1829a2d436899670540594a Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:00:47 +0000 Subject: [PATCH 03/25] Factory Reset (#13) Fixes https://github.com/awonak/alt-gravity/issues/1 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/13 --- firmware/Gravity/Gravity.ino | 9 ++++++++ firmware/Gravity/app_state.h | 24 -------------------- firmware/Gravity/display.h | 39 ++++++++++++++++++++++++++++++++- firmware/Gravity/save_state.cpp | 12 +++++++++- firmware/Gravity/save_state.h | 6 +++-- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 374ee03..06934f9 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -203,6 +203,12 @@ void HandleEncoderPressed() { InitGravity(app); } } + if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { + if (app.selected_sub_param == 0) { // Reset + stateManager.factoryReset(); + InitGravity(app); + } + } } // Only mark dirty and reset selected_sub_param when leaving editing mode. stateManager.markDirty(); @@ -277,6 +283,9 @@ void editMainParameter(int val) { case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); break; + case PARAM_MAIN_FACTORY_RESET: + updateSelection(app.selected_sub_param, val, 2); + break; } } diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index e7d9ab5..0ddd014 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -38,28 +38,4 @@ static Channel& GetSelectedChannel() { return app.channel[app.selected_channel - 1]; } -enum ParamsMainPage : uint8_t { - PARAM_MAIN_TEMPO, - PARAM_MAIN_SOURCE, - PARAM_MAIN_PULSE, - PARAM_MAIN_ENCODER_DIR, - PARAM_MAIN_SAVE_DATA, - PARAM_MAIN_LOAD_DATA, - PARAM_MAIN_RESET_STATE, - PARAM_MAIN_LAST, -}; - -enum ParamsChannelPage : uint8_t { - PARAM_CH_MOD, - PARAM_CH_PROB, - PARAM_CH_DUTY, - PARAM_CH_OFFSET, - PARAM_CH_SWING, - PARAM_CH_EUC_STEPS, - PARAM_CH_EUC_HITS, - PARAM_CH_CV1_DEST, - PARAM_CH_CV2_DEST, - PARAM_CH_LAST, -}; - #endif // APP_STATE_H \ No newline at end of file diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index cf37631..1823b75 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -96,6 +96,33 @@ constexpr uint8_t CHANNEL_BOXES_Y = 50; constexpr uint8_t CHANNEL_BOX_WIDTH = 18; constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; +// Menu items for editing global parameters. +enum ParamsMainPage : uint8_t { + PARAM_MAIN_TEMPO, + PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, + PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_SAVE_DATA, + PARAM_MAIN_LOAD_DATA, + PARAM_MAIN_RESET_STATE, + PARAM_MAIN_FACTORY_RESET, + PARAM_MAIN_LAST, +}; + +// Menu items for editing channel parameters. +enum ParamsChannelPage : uint8_t { + PARAM_CH_MOD, + PARAM_CH_PROB, + PARAM_CH_DUTY, + PARAM_CH_OFFSET, + PARAM_CH_SWING, + PARAM_CH_EUC_STEPS, + PARAM_CH_EUC_HITS, + PARAM_CH_CV1_DEST, + PARAM_CH_CV2_DEST, + PARAM_CH_LAST, +}; + // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { gravity.display.setFont(font); @@ -278,13 +305,23 @@ void DisplayMainPage() { mainText = F("x"); subText = F("BACK TO MAIN"); } + break; + case PARAM_MAIN_FACTORY_RESET: + if (app.selected_sub_param == 0) { + mainText = F("DEL"); + subText = F("FACTORY RESET"); + } else { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } + break; } drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 052779b..16ef342 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -26,7 +26,8 @@ bool StateManager::initialize(AppState& app) { return loadData(app, MAX_SAVE_SLOTS); } else { // EEPROM does not contain save data for this firmware & version. - // Initialize eeprom and save default patter to all save slots. + // Erase EEPROM and initialize state. Save default pattern to all save slots. + factoryReset(); reset(app); _saveMetadata(); // MAX_SAVE_SLOTS slot is reserved for transient state. @@ -82,6 +83,15 @@ void StateManager::markDirty() { _lastChangeTime = millis(); } +// Erases all data in the EEPROM by writing 0 to every address. +void StateManager::factoryReset() { + noInterrupts(); + for (unsigned int i = 0 ; i < EEPROM.length() ; i++) { + EEPROM.write(i, 0); + } + interrupts(); +} + bool StateManager::_isDataValid() { Metadata load_meta; EEPROM.get(0, load_meta); diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 354253a..29ae845 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -19,8 +19,8 @@ struct AppState; // Define the constants for the current firmware. -const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 7; +const char SKETCH_NAME[] = "AltGravity"; +const byte SKETCH_VERSION = 1; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; @@ -52,6 +52,8 @@ class StateManager { void update(const AppState& app); // Indicate that state has changed and we should save. void markDirty(); + // Erase all data stored in the EEPROM. + void factoryReset(); // This struct holds the data that identifies the firmware version. struct Metadata { -- 2.39.5 From 1bf90e16742d6e789534bf6e5dc12e0e7d5e76e4 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:01:18 +0000 Subject: [PATCH 04/25] Mute channel when shift + play pressed (#14) Fixes https://github.com/awonak/alt-gravity/issues/2 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/14 --- firmware/Gravity/Gravity.ino | 25 ++++++++++++++++++++----- firmware/Gravity/channel.h | 11 +++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 06934f9..b76a61b 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -2,7 +2,7 @@ * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Alt firmware version of Gravity by Sitka Instruments. - * @version v2.0.1 - June 2025 awonak - Full rewrite + * @version v2.0.1 - June 2025 awonak - Full rewrite * @version v1.0 - August 2023 Oleksiy H - Initial release * @date 2025-07-04 * @@ -25,7 +25,7 @@ * quantization of features like duty cycle (pulse width) or offset. * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm * generator. - * + * * ENCODER: * Press: change between selecting a parameter and editing the parameter. * Hold & Rotate: change current selected output channel. @@ -33,17 +33,17 @@ * BTN1: * Play/pause - start or stop the internal clock. * - * BTN2: + * BTN2: * Shift - hold and rotate encoder to change current selected output channel. * * EXT: * External clock input. When Gravity is set to INTERNAL clock mode, this * input is used to reset clocks. - * + * * CV1: * CV2: * External analog input used to provide modulation to any channel parameter. - * + * */ #include @@ -168,6 +168,21 @@ void HandleExtClockTick() { // void HandlePlayPressed() { + // Check if SHIFT is pressed to mute all/current channel. + if (gravity.shift_button.On()) { + if (app.selected_channel == 0) { + // Mute all channels + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + app.channel[i].toggleMute(); + } + } else { + // Mute selected channel + auto& ch = GetSelectedChannel(); + ch.toggleMute(); + } + return; + } + gravity.clock.IsPaused() ? gravity.clock.Start() : gravity.clock.Stop(); diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 8036781..8bcf999 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -161,6 +161,8 @@ class Channel { byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; } byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; } + void toggleMute() { mute = !mute; } + /** * @brief Processes a clock tick and determines if the output should be high or low. * Note: this method is called from an ISR and must be kept as simple as possible. @@ -168,6 +170,12 @@ class Channel { * @param output The output object to be modified. */ void processClockTick(uint32_t tick, DigitalOutput& output) { + // Mute check + if (mute) { + output.Low(); + return; + } + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); // Conditionally apply swing on down beats. @@ -298,6 +306,9 @@ class Channel { // Euclidean pattern Pattern pattern; + // Mute channel flag + bool mute; + // Pre-calculated pulse values for ISR performance uint16_t _duty_pulses; uint16_t _offset_pulses; -- 2.39.5 From 4f04137f67059f4b2f58f4bbb4dfb5150a7768c8 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:27:32 +0000 Subject: [PATCH 05/25] Add global/hardware settings to metadata EEPROM (#15) Settings like Encoder Direction and Display Orientation should persist when resetting channel state. Fixes https://github.com/awonak/alt-gravity/issues/7 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/15 --- firmware/Gravity/Gravity.ino | 4 ++-- firmware/Gravity/save_state.cpp | 42 ++++++++++++++++++++++++++------- firmware/Gravity/save_state.h | 5 +++- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index b76a61b..3b388d4 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -196,8 +196,8 @@ void HandleEncoderPressed() { if (app.selected_channel == 0) { // main page // TODO: rewrite as switch if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { - bool reversed = app.selected_sub_param == 1; - gravity.encoder.SetReverseDirection(reversed); + app.encoder_reversed = app.selected_sub_param == 1; + gravity.encoder.SetReverseDirection(app.encoder_reversed); } if (app.selected_param == PARAM_MAIN_SAVE_DATA) { if (app.selected_sub_param < MAX_SAVE_SLOTS) { diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 16ef342..8aa1f50 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -16,6 +16,7 @@ #include "app_state.h" // Calculate the starting address for EepromData, leaving space for metadata. +static const int METADATA_START_ADDR = 0; static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} @@ -24,12 +25,14 @@ bool StateManager::initialize(AppState& app) { if (_isDataValid()) { // Load data from the transient slot. return loadData(app, MAX_SAVE_SLOTS); - } else { - // EEPROM does not contain save data for this firmware & version. + } + // EEPROM does not contain save data for this firmware & version. + else { // Erase EEPROM and initialize state. Save default pattern to all save slots. factoryReset(); + // Initialize eeprom and save default patter to all save slots. + _saveMetadata(app); reset(app); - _saveMetadata(); // MAX_SAVE_SLOTS slot is reserved for transient state. for (int i = 0; i <= MAX_SAVE_SLOTS; i++) { app.selected_save_slot = i; @@ -43,10 +46,12 @@ bool StateManager::loadData(AppState& app, byte slot_index) { if (slot_index >= MAX_SAVE_SLOTS) return false; _loadState(app, slot_index); + _loadMetadata(app); return true; } +// Save app state to user specified save slot. void StateManager::saveData(const AppState& app) { if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; @@ -54,17 +59,18 @@ void StateManager::saveData(const AppState& app) { _isDirty = false; } +// Save transient state if it has changed and enough time has passed since last save. void StateManager::update(const AppState& app) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { // MAX_SAVE_SLOTS slot is reserved for transient state. _saveState(app, MAX_SAVE_SLOTS); + _saveMetadata(app); _isDirty = false; } } 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; @@ -75,6 +81,9 @@ void StateManager::reset(AppState& app) { app.channel[i].Init(); } + // Load global settings from Metadata + _loadMetadata(app); + _isDirty = false; } @@ -86,7 +95,7 @@ void StateManager::markDirty() { // Erases all data in the EEPROM by writing 0 to every address. void StateManager::factoryReset() { noInterrupts(); - for (unsigned int i = 0 ; i < EEPROM.length() ; i++) { + for (unsigned int i = 0; i < EEPROM.length(); i++) { EEPROM.write(i, 0); } interrupts(); @@ -94,7 +103,7 @@ void StateManager::factoryReset() { bool StateManager::_isDataValid() { Metadata load_meta; - EEPROM.get(0, load_meta); + EEPROM.get(METADATA_START_ADDR, load_meta); bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); bool version_match = (load_meta.version == SKETCH_VERSION); return name_match && version_match; @@ -114,6 +123,10 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_pulse = static_cast(app.selected_pulse); save_data.selected_save_slot = app.selected_save_slot; + // TODO: break this out into a separate function. Save State should be + // broken out into global / per-channel save methods. When saving via + // "update" only save state for the current channel since other channels + // will not have changed when saving user edits. for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; auto& save_ch = save_data.channel_data[i]; @@ -141,7 +154,6 @@ void StateManager::_loadState(AppState& app, byte slot_index) { // Restore app state from loaded data. 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); @@ -165,11 +177,23 @@ void StateManager::_loadState(AppState& app, byte slot_index) { interrupts(); } -void StateManager::_saveMetadata() { +void StateManager::_saveMetadata(const AppState& app) { noInterrupts(); Metadata current_meta; strcpy(current_meta.sketch_name, SKETCH_NAME); current_meta.version = SKETCH_VERSION; - EEPROM.put(0, current_meta); + + // Global user settings + current_meta.encoder_reversed = app.encoder_reversed; + + EEPROM.put(METADATA_START_ADDR, current_meta); interrupts(); } + +void StateManager::_loadMetadata(AppState& app) { + noInterrupts(); + Metadata metadata; + EEPROM.get(METADATA_START_ADDR, metadata); + app.encoder_reversed = metadata.encoder_reversed; + interrupts(); +} \ No newline at end of file diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 29ae845..bda7c4f 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -59,6 +59,8 @@ class StateManager { struct Metadata { byte version; char sketch_name[16]; + // Additional global/hardware settings + bool encoder_reversed; }; struct ChannelState { byte base_clock_mod_index; @@ -85,7 +87,8 @@ class StateManager { private: bool _isDataValid(); - void _saveMetadata(); + void _saveMetadata(const AppState& app); + void _loadMetadata(AppState& app); void _saveState(const AppState& app, byte slot_index); void _loadState(AppState& app, byte slot_index); -- 2.39.5 From 01f32407f6594405a6c3539194bad655be179b30 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 20 Jul 2025 17:53:03 -0700 Subject: [PATCH 06/25] bump version --- firmware/Gravity/save_state.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 91e1e8f..659a6e6 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -20,7 +20,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. -- 2.39.5 From 1c0fb86bc102c303907de39642d5ca2bfaf1e88d Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 00:00:49 +0000 Subject: [PATCH 07/25] Reverse the order of clock mod options. (#16) This now matches original Gravity behavior. Also, now when applying CV mod positive voltages increase clock mod instead of reducing it. Also fix pulse out, which wasn't previously updated when CLOCK_MOD was moved to program mem. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/16 --- clock.h | 3 --- firmware/Gravity/Gravity.ino | 5 ++--- firmware/Gravity/channel.h | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/clock.h b/clock.h index 9cb2ad8..4008e0d 100644 --- a/clock.h +++ b/clock.h @@ -50,9 +50,6 @@ class Clock { void Init() { NeoSerial.begin(31250); - // Static pin definition for pulse out. - pinMode(PULSE_OUT_PIN, OUTPUT); - // Initialize the clock library uClock.init(); uClock.setClockMode(uClock.INTERNAL_CLOCK); diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 3b388d4..c0f9481 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -135,13 +135,12 @@ void HandleIntClockTick(uint32_t tick) { break; } - const uint32_t pulse_high_ticks = CLOCK_MOD_PULSES[clock_index]; + const uint16_t pulse_high_ticks = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_index]); const uint32_t pulse_low_ticks = tick + max((pulse_high_ticks / 2), 1L); if (tick % pulse_high_ticks == 0) { gravity.pulse.High(); - } - if (pulse_low_ticks % pulse_high_ticks == 0) { + } else if (pulse_low_ticks % pulse_high_ticks == 0) { gravity.pulse.Low(); } } diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 8bcf999..f30b4c8 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -34,28 +34,28 @@ static const byte MOD_CHOICE_SIZE = 25; // Negative numbers are multipliers, positive are divisors. static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = { - // Multipliers - -24, -16, -12, -8, -6, -4, -3, -2, - // Internal Clock Unity - 1, // Divisors - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128}; + 128, 64, 32, 24, 16, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, + // Internal Clock Unity (quarter note) + 1, + // Multipliers + -2, -3, -4, -6, -8, -12, -16, -24}; // This represents the number of clock pulses for a 96 PPQN clock source // that match the above div/mult mods. static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { - // Multiplier Pulses (96 / X) - 4, 6, 8, 12, 16, 24, 32, 48, + // Divisor Pulses (96 * X) + 12288, 6144, 3072, 2304, 1536, 1152, 1056, 960, 864, 768, 672, 576, 480, 384, 288, 192, // Internal Clock Pulses 96, - // Divisor Pulses (96 * X) - 192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288}; + // Multiplier Pulses (96 / X) + 48, 32, 24, 16, 12, 8, 6, 4}; -static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // x1 or 96 PPQN. +static const byte DEFAULT_CLOCK_MOD_INDEX = 16; // x1 or 96 PPQN. -static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = 0; -static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = 4; -static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = 8; +static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 1; +static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 6; +static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 9; class Channel { public: -- 2.39.5 From b0accdc83a6625b7a8c9333c9e174b98d3653559 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:12:45 +0000 Subject: [PATCH 08/25] Fix Initial Transient State (#17) There was an off-by-one error that was not properly loading transient state from the designated memory slot. Also fixes setting the last saved/loaded slot indicator with metadata. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/17 --- firmware/Gravity/save_state.cpp | 25 +++++++++++++++---------- firmware/Gravity/save_state.h | 5 +++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 8aa1f50..08c811b 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -24,7 +24,7 @@ StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} bool StateManager::initialize(AppState& app) { if (_isDataValid()) { // Load data from the transient slot. - return loadData(app, MAX_SAVE_SLOTS); + return loadData(app, TRANSIENT_SLOT); } // EEPROM does not contain save data for this firmware & version. else { @@ -33,17 +33,17 @@ bool StateManager::initialize(AppState& app) { // Initialize eeprom and save default patter to all save slots. _saveMetadata(app); reset(app); - // MAX_SAVE_SLOTS slot is reserved for transient state. - for (int i = 0; i <= MAX_SAVE_SLOTS; i++) { - app.selected_save_slot = i; + for (int i = 0; i < MAX_SAVE_SLOTS; i++) { _saveState(app, i); } + _saveState(app, TRANSIENT_SLOT); return false; } } bool StateManager::loadData(AppState& app, byte slot_index) { - if (slot_index >= MAX_SAVE_SLOTS) return false; + // Check if slot_index is within max range + 1 for transient. + if (slot_index >= MAX_SAVE_SLOTS + 1) return false; _loadState(app, slot_index); _loadMetadata(app); @@ -53,7 +53,8 @@ bool StateManager::loadData(AppState& app, byte slot_index) { // Save app state to user specified save slot. void StateManager::saveData(const AppState& app) { - if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + // Check if slot_index is within max range + 1 for transient. + if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; _saveState(app, app.selected_save_slot); _isDirty = false; @@ -62,8 +63,7 @@ void StateManager::saveData(const AppState& app) { // Save transient state if it has changed and enough time has passed since last save. void StateManager::update(const AppState& app) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { - // MAX_SAVE_SLOTS slot is reserved for transient state. - _saveState(app, MAX_SAVE_SLOTS); + _saveState(app, TRANSIENT_SLOT); _saveMetadata(app); _isDirty = false; } @@ -110,7 +110,8 @@ bool StateManager::_isDataValid() { } void StateManager::_saveState(const AppState& app, byte slot_index) { - if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + // Check if slot_index is within max range + 1 for transient. + if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; noInterrupts(); static EepromData save_data; @@ -121,7 +122,6 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); save_data.selected_pulse = static_cast(app.selected_pulse); - save_data.selected_save_slot = app.selected_save_slot; // TODO: break this out into a separate function. Save State should be // broken out into global / per-channel save methods. When saving via @@ -147,6 +147,9 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { } void StateManager::_loadState(AppState& app, byte slot_index) { + // Check if slot_index is within max range + 1 for transient. + if (slot_index >= MAX_SAVE_SLOTS + 1) return; + noInterrupts(); static EepromData load_data; int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); @@ -184,6 +187,7 @@ void StateManager::_saveMetadata(const AppState& app) { current_meta.version = SKETCH_VERSION; // Global user settings + current_meta.selected_save_slot = app.selected_save_slot; current_meta.encoder_reversed = app.encoder_reversed; EEPROM.put(METADATA_START_ADDR, current_meta); @@ -194,6 +198,7 @@ void StateManager::_loadMetadata(AppState& app) { noInterrupts(); Metadata metadata; EEPROM.get(METADATA_START_ADDR, metadata); + app.selected_save_slot = metadata.selected_save_slot; app.encoder_reversed = metadata.encoder_reversed; interrupts(); } \ No newline at end of file diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index bda7c4f..ec75f03 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -23,7 +23,8 @@ const char SKETCH_NAME[] = "AltGravity"; const byte SKETCH_VERSION = 1; // Number of available save slots. -const byte MAX_SAVE_SLOTS = 10; +const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. +const byte TRANSIENT_SLOT = 10; // Transient slot index to persist state when powered off. // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -60,6 +61,7 @@ class StateManager { byte version; char sketch_name[16]; // Additional global/hardware settings + byte selected_save_slot; bool encoder_reversed; }; struct ChannelState { @@ -81,7 +83,6 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; - byte selected_save_slot; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; -- 2.39.5 From c5bddef66d0ddd4017339ddd26862e79e25c9d1c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:16:32 +0000 Subject: [PATCH 09/25] Show loading bootsplash with firmware name and version (#18) Bootsplash is displayed before EEPROM erase, which is a slow operation. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/18 --- firmware/Gravity/Gravity.ino | 6 ++++++ firmware/Gravity/display.h | 17 +++++++++++++++++ firmware/Gravity/save_state.cpp | 4 ++-- firmware/Gravity/save_state.h | 6 +++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index c0f9481..e58e8c5 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -64,6 +64,10 @@ void setup() { // Start Gravity. gravity.Init(); + // Show bootsplash when initializing firmware. + Bootsplash(); + delay(2000); + // Initialize the state manager. This will load settings from EEPROM stateManager.initialize(app); InitGravity(app); @@ -219,7 +223,9 @@ void HandleEncoderPressed() { } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Reset + Bootsplash(); stateManager.factoryReset(); + stateManager.reset(app); InitGravity(app); } } diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 1823b75..39ad8fd 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -469,4 +469,21 @@ void UpdateDisplay() { } while (gravity.display.nextPage()); } +void Bootsplash() { + gravity.display.firstPage(); + do { + int textWidth; + gravity.display.setFont(TEXT_FONT); + + textWidth = gravity.display.getStrWidth(SKETCH_NAME); + gravity.display.drawStr(24 + (textWidth / 2), 24, SKETCH_NAME); + + textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); + gravity.display.drawStr(24 + (textWidth / 2), 36, SEMANTIC_VERSION); + + textWidth = gravity.display.getStrWidth("LOADING...."); + gravity.display.drawStr(34 + (textWidth / 2), 48, "LOADING...."); + } while (gravity.display.nextPage()); +} + #endif // DISPLAY_H diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 08c811b..3b29239 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -105,7 +105,7 @@ bool StateManager::_isDataValid() { Metadata load_meta; EEPROM.get(METADATA_START_ADDR, load_meta); bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); - bool version_match = (load_meta.version == SKETCH_VERSION); + bool version_match = (strcmp(load_meta.version, SEMANTIC_VERSION) == 0); return name_match && version_match; } @@ -184,7 +184,7 @@ void StateManager::_saveMetadata(const AppState& app) { noInterrupts(); Metadata current_meta; strcpy(current_meta.sketch_name, SKETCH_NAME); - current_meta.version = SKETCH_VERSION; + strcpy(current_meta.version, SEMANTIC_VERSION); // Global user settings current_meta.selected_save_slot = app.selected_save_slot; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index ec75f03..91e1e8f 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -19,8 +19,8 @@ struct AppState; // Define the constants for the current firmware. -const char SKETCH_NAME[] = "AltGravity"; -const byte SKETCH_VERSION = 1; +const char SKETCH_NAME[] = "ALT GRAVITY"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. @@ -58,8 +58,8 @@ class StateManager { // This struct holds the data that identifies the firmware version. struct Metadata { - byte version; char sketch_name[16]; + char version[16]; // Additional global/hardware settings byte selected_save_slot; bool encoder_reversed; -- 2.39.5 From ec34bc3a7bba67d9da002c1b6228e778a3213114 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 23 Jul 2025 03:32:16 +0000 Subject: [PATCH 10/25] Fix metadata loading issues with Initialization and refactor Factory Reset. (#19) Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/19 --- firmware/Gravity/Gravity.ino | 5 ++- firmware/Gravity/display.h | 6 ++-- firmware/Gravity/save_state.cpp | 54 ++++++++++++++++++--------------- firmware/Gravity/save_state.h | 5 ++- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index e58e8c5..122cbf2 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -222,10 +222,9 @@ void HandleEncoderPressed() { } } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { - if (app.selected_sub_param == 0) { // Reset + if (app.selected_sub_param == 0) { // Erase Bootsplash(); - stateManager.factoryReset(); - stateManager.reset(app); + stateManager.factoryReset(app); InitGravity(app); } } diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 39ad8fd..dc67447 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -476,13 +476,13 @@ void Bootsplash() { gravity.display.setFont(TEXT_FONT); textWidth = gravity.display.getStrWidth(SKETCH_NAME); - gravity.display.drawStr(24 + (textWidth / 2), 24, SKETCH_NAME); + gravity.display.drawStr(16 + (textWidth / 2), 20, SKETCH_NAME); textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); - gravity.display.drawStr(24 + (textWidth / 2), 36, SEMANTIC_VERSION); + gravity.display.drawStr(16 + (textWidth / 2), 32, SEMANTIC_VERSION); textWidth = gravity.display.getStrWidth("LOADING...."); - gravity.display.drawStr(34 + (textWidth / 2), 48, "LOADING...."); + gravity.display.drawStr(26 + (textWidth / 2), 44, "LOADING...."); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 3b29239..674115f 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -23,20 +23,16 @@ StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} bool StateManager::initialize(AppState& app) { if (_isDataValid()) { - // Load data from the transient slot. - return loadData(app, TRANSIENT_SLOT); + // Load global settings. + _loadMetadata(app); + // Load app data from the transient slot. + _loadState(app, TRANSIENT_SLOT); + return true; } // EEPROM does not contain save data for this firmware & version. else { // Erase EEPROM and initialize state. Save default pattern to all save slots. - factoryReset(); - // Initialize eeprom and save default patter to all save slots. - _saveMetadata(app); - reset(app); - for (int i = 0; i < MAX_SAVE_SLOTS; i++) { - _saveState(app, i); - } - _saveState(app, TRANSIENT_SLOT); + factoryReset(app); return false; } } @@ -45,8 +41,11 @@ bool StateManager::loadData(AppState& app, byte slot_index) { // Check if slot_index is within max range + 1 for transient. if (slot_index >= MAX_SAVE_SLOTS + 1) return false; + // Load the state data from the specified EEPROM slot and update the app state save slot. _loadState(app, slot_index); - _loadMetadata(app); + app.selected_save_slot = slot_index; + // Persist this change in the global metadata. + _saveMetadata(app); return true; } @@ -57,6 +56,7 @@ void StateManager::saveData(const AppState& app) { if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; _saveState(app, app.selected_save_slot); + _saveMetadata(app); _isDirty = false; } @@ -70,12 +70,12 @@ void StateManager::update(const AppState& app) { } void StateManager::reset(AppState& app) { - app.tempo = Clock::DEFAULT_TEMPO; - app.selected_param = 0; - app.selected_channel = 0; - app.selected_source = Clock::SOURCE_INTERNAL; - app.selected_pulse = Clock::PULSE_PPQN_24; - app.selected_save_slot = 0; + AppState default_app; + app.tempo = default_app.tempo; + app.selected_param = default_app.selected_param; + app.selected_channel = default_app.selected_channel; + app.selected_source = default_app.selected_source; + app.selected_pulse = default_app.selected_pulse; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -93,19 +93,27 @@ void StateManager::markDirty() { } // Erases all data in the EEPROM by writing 0 to every address. -void StateManager::factoryReset() { +void StateManager::factoryReset(AppState& app) { noInterrupts(); for (unsigned int i = 0; i < EEPROM.length(); i++) { EEPROM.write(i, 0); } + // Initialize eeprom and save default patter to all save slots. + _saveMetadata(app); + reset(app); + for (int i = 0; i < MAX_SAVE_SLOTS; i++) { + app.selected_save_slot = i; + _saveState(app, i); + } + _saveState(app, TRANSIENT_SLOT); interrupts(); } bool StateManager::_isDataValid() { - Metadata load_meta; - EEPROM.get(METADATA_START_ADDR, load_meta); - bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); - bool version_match = (strcmp(load_meta.version, SEMANTIC_VERSION) == 0); + Metadata metadata; + EEPROM.get(METADATA_START_ADDR, metadata); + bool name_match = (strcmp(metadata.sketch_name, SKETCH_NAME) == 0); + bool version_match = (strcmp(metadata.version, SEMANTIC_VERSION) == 0); return name_match && version_match; } @@ -117,7 +125,6 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { static EepromData save_data; 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); @@ -161,7 +168,6 @@ void StateManager::_loadState(AppState& app, byte slot_index) { app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); app.selected_pulse = static_cast(load_data.selected_pulse); - app.selected_save_slot = slot_index; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& ch = app.channel[i]; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 91e1e8f..cd7fcba 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -20,7 +20,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. @@ -54,7 +54,7 @@ class StateManager { // Indicate that state has changed and we should save. void markDirty(); // Erase all data stored in the EEPROM. - void factoryReset(); + void factoryReset(AppState& app); // This struct holds the data that identifies the firmware version. struct Metadata { @@ -78,7 +78,6 @@ class StateManager { // 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; -- 2.39.5 From c7a3277b5fc9e2ae10987f3f972acb498cc9256f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 07:53:41 -0700 Subject: [PATCH 11/25] Memory improvements in bootsplash and StateManager --- firmware/Gravity/Gravity.ino | 7 ++++--- firmware/Gravity/app_state.h | 8 ++++---- firmware/Gravity/display.h | 23 ++++++++++++----------- firmware/Gravity/save_state.cpp | 15 +++++++++++++-- firmware/Gravity/save_state.h | 20 +++++++++----------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 122cbf2..18c784e 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -203,13 +203,13 @@ void HandleEncoderPressed() { gravity.encoder.SetReverseDirection(app.encoder_reversed); } if (app.selected_param == PARAM_MAIN_SAVE_DATA) { - if (app.selected_sub_param < MAX_SAVE_SLOTS) { + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param; stateManager.saveData(app); } } if (app.selected_param == PARAM_MAIN_LOAD_DATA) { - if (app.selected_sub_param < MAX_SAVE_SLOTS) { + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param; stateManager.loadData(app, app.selected_save_slot); InitGravity(app); @@ -223,6 +223,7 @@ void HandleEncoderPressed() { } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Erase + // Show bootsplash during slow erase operation. Bootsplash(); stateManager.factoryReset(app); InitGravity(app); @@ -297,7 +298,7 @@ void editMainParameter(int val) { break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: - updateSelection(app.selected_sub_param, val, MAX_SAVE_SLOTS + 1); + updateSelection(app.selected_sub_param, val, StateManager::MAX_SAVE_SLOTS + 1); break; case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 0ddd014..b53d314 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -19,9 +19,7 @@ // 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; + Channel channel[Gravity::OUTPUT_COUNT]; byte selected_param = 0; byte selected_sub_param = 0; // Temporary value for editing params. byte selected_channel = 0; // 0=tempo, 1-6=output channel @@ -29,7 +27,9 @@ struct AppState { byte selected_save_slot = 0; // The currently active save slot. Clock::Source selected_source = Clock::SOURCE_INTERNAL; Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; - Channel channel[Gravity::OUTPUT_COUNT]; + bool editing_param = false; + bool encoder_reversed = false; + bool refresh_screen = true; }; extern AppState app; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index dc67447..37b900b 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -214,10 +214,10 @@ void swingDivisionMark() { // Human friendly display value for save slot. String displaySaveSlot(int slot) { - if (slot >= 0 && slot < MAX_SAVE_SLOTS / 2) { + if (slot >= 0 && slot < StateManager::MAX_SAVE_SLOTS / 2) { return String("A") + String(slot + 1); - } else if (slot >= MAX_SAVE_SLOTS / 2 && slot <= MAX_SAVE_SLOTS) { - return String("B") + String(slot - (MAX_SAVE_SLOTS / 2) + 1); + } else if (slot >= StateManager::MAX_SAVE_SLOTS / 2 && slot <= StateManager::MAX_SAVE_SLOTS) { + return String("B") + String(slot - (StateManager::MAX_SAVE_SLOTS / 2) + 1); } } @@ -283,7 +283,7 @@ void DisplayMainPage() { break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: - if (app.selected_sub_param == MAX_SAVE_SLOTS) { + if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) { mainText = F("x"); subText = F("BACK TO MAIN"); } else { @@ -465,7 +465,7 @@ void UpdateDisplay() { DisplayChannelPage(); } // Global channel select UI. - DisplaySelectedChannel(); + DisplaySelectedChannel(); } while (gravity.display.nextPage()); } @@ -473,16 +473,17 @@ void Bootsplash() { gravity.display.firstPage(); do { int textWidth; + String loadingText = F("LOADING...."); gravity.display.setFont(TEXT_FONT); - textWidth = gravity.display.getStrWidth(SKETCH_NAME); - gravity.display.drawStr(16 + (textWidth / 2), 20, SKETCH_NAME); + textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME); + gravity.display.drawStr(16 + (textWidth / 2), 20, StateManager::SKETCH_NAME); - textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); - gravity.display.drawStr(16 + (textWidth / 2), 32, SEMANTIC_VERSION); + textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION); + gravity.display.drawStr(16 + (textWidth / 2), 32, StateManager::SEMANTIC_VERSION); - textWidth = gravity.display.getStrWidth("LOADING...."); - gravity.display.drawStr(26 + (textWidth / 2), 44, "LOADING...."); + textWidth = gravity.display.getStrWidth(loadingText.c_str()); + gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str()); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 674115f..be4db95 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -15,9 +15,20 @@ #include "app_state.h" +// Define the constants for the current firmware. +const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; + +// Number of available save slots. +const byte StateManager::MAX_SAVE_SLOTS = 10; +const byte StateManager::TRANSIENT_SLOT = 10; + +// Define the minimum amount of time between EEPROM writes. +const unsigned long StateManager::SAVE_DELAY_MS = 2000; + // Calculate the starting address for EepromData, leaving space for metadata. -static const int METADATA_START_ADDR = 0; -static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); +const int StateManager::METADATA_START_ADDR = 0; +const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index cd7fcba..b1a94d6 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -18,17 +18,6 @@ // Forward-declare AppState to avoid circular dependencies. struct AppState; -// Define the constants for the current firmware. -const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; - -// Number of available save slots. -const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. -const byte TRANSIENT_SLOT = 10; // Transient slot index to persist state when powered off. - -// Define the minimum amount of time between EEPROM writes. -static const unsigned long SAVE_DELAY_MS = 2000; - /** * @brief Manages saving and loading of the application state to and from EEPROM. * The number of user slots is defined by MAX_SAVE_SLOTS, and one additional slot @@ -39,6 +28,11 @@ static const unsigned long SAVE_DELAY_MS = 2000; */ class StateManager { public: + static const char SKETCH_NAME[]; + static const char SEMANTIC_VERSION[]; + static const byte MAX_SAVE_SLOTS; + static const byte TRANSIENT_SLOT; + StateManager(); // Populate the AppState instance with values from EEPROM if they exist. @@ -92,6 +86,10 @@ class StateManager { void _saveState(const AppState& app, byte slot_index); void _loadState(AppState& app, byte slot_index); + static const unsigned long SAVE_DELAY_MS; + static const int METADATA_START_ADDR; + static const int EEPROM_DATA_START_ADDR; + bool _isDirty; unsigned long _lastChangeTime; }; -- 2.39.5 From 65dde4d62e8ff3ddc063d2181d03e013daf0b917 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 15:07:15 +0000 Subject: [PATCH 12/25] Reorganization of library structure to better match Arduino spec (#20) Note, this will also require to you "uninstall and reinstall" the Arduino library due to the library file location changes. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/20 --- examples/clock_mod/clock_mod.ino | 2 +- firmware/Gravity/Gravity.ino | 4 +- firmware/Gravity/app_state.h | 2 +- firmware/Gravity/channel.h | 2 +- firmware/Gravity/save_state.cpp | 2 +- firmware/Gravity/save_state.h | 2 +- library.properties | 10 ++ analog_input.h => src/analog_input.h | 0 button.h => src/button.h | 0 clock.h => src/clock.h | 2 +- digital_output.h => src/digital_output.h | 0 encoder.h => src/encoder.h | 0 gravity.cpp => src/libGravity.cpp | 4 +- gravity.h => src/libGravity.h | 2 +- peripherials.h => src/peripherials.h | 0 {uClock => src/uClock}/platforms/avr.h | 0 uClock.cpp => src/uClock/uClock.cpp | 2 +- uClock.h => src/uClock/uClock.h | 0 uClock/uClock.h | 180 ----------------------- 19 files changed, 22 insertions(+), 192 deletions(-) create mode 100644 library.properties rename analog_input.h => src/analog_input.h (100%) rename button.h => src/button.h (100%) rename clock.h => src/clock.h (99%) rename digital_output.h => src/digital_output.h (100%) rename encoder.h => src/encoder.h (100%) rename gravity.cpp => src/libGravity.cpp (97%) rename gravity.h => src/libGravity.h (98%) rename peripherials.h => src/peripherials.h (100%) rename {uClock => src/uClock}/platforms/avr.h (100%) rename uClock.cpp => src/uClock/uClock.cpp (99%) rename uClock.h => src/uClock/uClock.h (100%) delete mode 100755 uClock/uClock.h diff --git a/examples/clock_mod/clock_mod.ino b/examples/clock_mod/clock_mod.ino index 8033c0d..cba7091 100644 --- a/examples/clock_mod/clock_mod.ino +++ b/examples/clock_mod/clock_mod.ino @@ -17,7 +17,7 @@ * */ -#include "gravity.h" +#include // Firmware state variables. struct Channel { diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 18c784e..0a033aa 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -2,7 +2,7 @@ * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Alt firmware version of Gravity by Sitka Instruments. - * @version v2.0.1 - June 2025 awonak - Full rewrite + * @version v2.0.0 - June 2025 awonak - Full rewrite * @version v1.0 - August 2023 Oleksiy H - Initial release * @date 2025-07-04 * @@ -46,7 +46,7 @@ * */ -#include +#include #include "app_state.h" #include "channel.h" diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index b53d314..90712df 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -12,7 +12,7 @@ #ifndef APP_STATE_H #define APP_STATE_H -#include +#include #include "channel.h" diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index f30b4c8..b668f2f 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -13,7 +13,7 @@ #define CHANNEL_H #include -#include +#include #include "euclidean.h" diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index be4db95..96a1f48 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index b1a94d6..8f25dd1 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -13,7 +13,7 @@ #define SAVE_STATE_H #include -#include +#include // Forward-declare AppState to avoid circular dependencies. struct AppState; diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..77fe46f --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=libGravity +version=2.0.0beta2 +author=Adam Wonak +maintainer=awonak +sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module +category=Other +license=MIT +url=https://github.com/awonak/libGravity +architectures=avr +depends=uClock,RotaryEncoder,U8g2 \ No newline at end of file diff --git a/analog_input.h b/src/analog_input.h similarity index 100% rename from analog_input.h rename to src/analog_input.h diff --git a/button.h b/src/button.h similarity index 100% rename from button.h rename to src/button.h diff --git a/clock.h b/src/clock.h similarity index 99% rename from clock.h rename to src/clock.h index 4008e0d..613667a 100644 --- a/clock.h +++ b/src/clock.h @@ -15,7 +15,7 @@ #include #include "peripherials.h" -#include "uClock.h" +#include "uClock/uClock.h" // MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 diff --git a/digital_output.h b/src/digital_output.h similarity index 100% rename from digital_output.h rename to src/digital_output.h diff --git a/encoder.h b/src/encoder.h similarity index 100% rename from encoder.h rename to src/encoder.h diff --git a/gravity.cpp b/src/libGravity.cpp similarity index 97% rename from gravity.cpp rename to src/libGravity.cpp index 23d79f8..c4ae4bc 100644 --- a/gravity.cpp +++ b/src/libGravity.cpp @@ -1,5 +1,5 @@ /** - * @file gravity.cpp + * @file libGravity.cpp * @author Adam Wonak (https://github.com/awonak) * @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @version 0.1 @@ -9,7 +9,7 @@ * */ -#include "gravity.h" +#include "libGravity.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. diff --git a/gravity.h b/src/libGravity.h similarity index 98% rename from gravity.h rename to src/libGravity.h index 00539fb..daf5192 100644 --- a/gravity.h +++ b/src/libGravity.h @@ -1,5 +1,5 @@ /** - * @file gravity.h + * @file libGravity.h * @author Adam Wonak (https://github.com/awonak) * @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @version 0.1 diff --git a/peripherials.h b/src/peripherials.h similarity index 100% rename from peripherials.h rename to src/peripherials.h diff --git a/uClock/platforms/avr.h b/src/uClock/platforms/avr.h similarity index 100% rename from uClock/platforms/avr.h rename to src/uClock/platforms/avr.h diff --git a/uClock.cpp b/src/uClock/uClock.cpp similarity index 99% rename from uClock.cpp rename to src/uClock/uClock.cpp index 1003930..94c432e 100755 --- a/uClock.cpp +++ b/src/uClock/uClock.cpp @@ -32,7 +32,7 @@ * DEALINGS IN THE SOFTWARE. */ #include "uClock.h" -#include "uClock/platforms/avr.h" +#include "platforms/avr.h" // // Platform specific timer setup/control diff --git a/uClock.h b/src/uClock/uClock.h similarity index 100% rename from uClock.h rename to src/uClock/uClock.h diff --git a/uClock/uClock.h b/uClock/uClock.h deleted file mode 100755 index d8670b0..0000000 --- a/uClock/uClock.h +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * @file uClock.h - * Project BPM clock generator for Arduino - * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) - * @version 2.2.1 - * @author Romulo Silva - * @date 10/06/2017 - * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#ifndef __U_CLOCK_H__ -#define __U_CLOCK_H__ - -#include -#include - -namespace umodular { namespace clock { - -#define MIN_BPM 1 -#define MAX_BPM 400 - -#define PHASE_FACTOR 16 -#define PLL_X 220 - -#define SECS_PER_MIN (60UL) -#define SECS_PER_HOUR (3600UL) -#define SECS_PER_DAY (SECS_PER_HOUR * 24L) - -class uClockClass { - - public: - enum ClockMode { - INTERNAL_CLOCK = 0, - EXTERNAL_CLOCK - }; - - enum ClockState { - PAUSED = 0, - STARTING, - STARTED - }; - - enum PPQNResolution { - PPQN_1 = 1, - PPQN_2 = 2, - PPQN_4 = 4, - PPQN_8 = 8, - PPQN_12 = 12, - PPQN_24 = 24, - PPQN_48 = 48, - PPQN_96 = 96, - PPQN_384 = 384, - PPQN_480 = 480, - PPQN_960 = 960 - }; - - ClockState clock_state; - - uClockClass(); - - void setOnOutputPPQN(void (*callback)(uint32_t tick)) { - onOutputPPQNCallback = callback; - } - - void setOnSync24(void (*callback)(uint32_t tick)) { - onSync24Callback = callback; - } - - void setOnClockStart(void (*callback)()) { - onClockStartCallback = callback; - } - - void setOnClockStop(void (*callback)()) { - onClockStopCallback = callback; - } - - void init(); - void setOutputPPQN(PPQNResolution resolution); - void setInputPPQN(PPQNResolution resolution); - - void handleTimerInt(); - void handleExternalClock(); - void resetCounters(); - - // external class control - void start(); - void stop(); - void pause(); - void setTempo(float bpm); - float getTempo(); - - // for software timer implementation(fallback for no board support) - void run(); - - // external timming control - void setClockMode(ClockMode tempo_mode); - ClockMode getClockMode(); - void clockMe(); - // for smooth slave tempo calculate display you should raise the - // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. - // note: this doesn't impact on sync time, only display time getTempo() - // if you dont want to use it, it is default set it to 1 for memory save - void setExtIntervalBuffer(uint8_t buffer_size); - - // elapsed time support - uint8_t getNumberOfSeconds(uint32_t time); - uint8_t getNumberOfMinutes(uint32_t time); - uint8_t getNumberOfHours(uint32_t time); - uint8_t getNumberOfDays(uint32_t time); - uint32_t getNowTimer(); - uint32_t getPlayTime(); - - uint32_t bpmToMicroSeconds(float bpm); - - private: - float inline freqToBpm(uint32_t freq); - float inline constrainBpm(float bpm); - void calculateReferencedata(); - - void (*onOutputPPQNCallback)(uint32_t tick); - void (*onSync24Callback)(uint32_t tick); - void (*onClockStartCallback)(); - void (*onClockStopCallback)(); - - // clock input/output control - PPQNResolution output_ppqn = PPQN_96; - PPQNResolution input_ppqn = PPQN_24; - // output and internal counters, ticks and references - uint32_t tick; - uint32_t int_clock_tick; - uint8_t mod_clock_counter; - uint16_t mod_clock_ref; - - uint8_t mod_sync24_counter; - uint16_t mod_sync24_ref; - uint32_t sync24_tick; - - // external clock control - volatile uint32_t ext_clock_us; - volatile uint32_t ext_clock_tick; - volatile uint32_t ext_interval; - uint32_t last_interval; - uint32_t sync_interval; - - float tempo; - uint32_t start_timer; - ClockMode clock_mode; - - volatile uint32_t * ext_interval_buffer = nullptr; - uint8_t ext_interval_buffer_size; - uint16_t ext_interval_idx; -}; - -} } // end namespace umodular::clock - -extern umodular::clock::uClockClass uClock; - -extern "C" { - extern volatile uint32_t _millis; -} - -#endif /* __U_CLOCK_H__ */ -- 2.39.5 From d1c8ee16a497566824d579606097e3f2c68c10eb Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 08:35:05 -0700 Subject: [PATCH 13/25] EXT will reset clocks in MIDI clock mode. Add reset behavior for EXT clock input when MIDI clock source is selected. Fixes: https://git.pinkduck.xyz/awonak/libGravity/issues/22 --- firmware/Gravity/Gravity.ino | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 0a033aa..ebc0fe3 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -37,10 +37,12 @@ * Shift - hold and rotate encoder to change current selected output channel. * * EXT: - * External clock input. When Gravity is set to INTERNAL clock mode, this - * input is used to reset clocks. + * External clock input. When Gravity is set to INTERNAL or MIDI clock + * source, this input is used to reset clocks. * * CV1: + * External analog input used to provide modulation to any channel parameter. + * * CV2: * External analog input used to provide modulation to any channel parameter. * @@ -155,13 +157,16 @@ void HandleIntClockTick(uint32_t tick) { } void HandleExtClockTick() { - if (gravity.clock.InternalSource()) { - // Use EXT as Reset when internally clocked. - ResetOutputs(); - gravity.clock.Reset(); - } else { - // Register clock tick. - gravity.clock.Tick(); + switch (app.selected_source) { + case Clock::SOURCE_INTERNAL: + case Clock::SOURCE_EXTERNAL_MIDI: + // Use EXT as Reset when not used for clock source. + ResetOutputs(); + gravity.clock.Reset(); + break; + default: + // Register EXT cv clock tick. + gravity.clock.Tick(); } app.refresh_screen = true; } -- 2.39.5 From dd7217d04ef1a95a0bec565fcded10bd0667b76f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:27:24 -0700 Subject: [PATCH 14/25] Fix euclidean hit mod --- firmware/Gravity/channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index b668f2f..df88785 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -255,7 +255,7 @@ class Channel { int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); pattern.SetSteps(base_euc_steps + step_mod); - int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, pattern.GetSteps()); pattern.SetHits(base_euc_hits + hit_mod); // After all cvmod values are updated, recalculate clock pulse modifiers. -- 2.39.5 From 19473db67e761c7f96a3ff0d4e33e87dfb568ca1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:38:34 -0700 Subject: [PATCH 15/25] bump version in code --- firmware/Gravity/save_state.cpp | 2 +- library.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 96a1f48..e701c4f 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA3"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; diff --git a/library.properties b/library.properties index 77fe46f..1390c78 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=libGravity -version=2.0.0beta2 +version=2.0.0beta3 author=Adam Wonak maintainer=awonak sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module -- 2.39.5 From b6402380c0866051623be5c790e2fb74b2a5ffac Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 26 Jul 2025 18:51:18 -0700 Subject: [PATCH 16/25] fixed bug in cv mod of clock multiplication upper range. --- firmware/Gravity/channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index df88785..114e7fd 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -238,7 +238,7 @@ class Channel { } int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); - cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); + cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, MOD_CHOICE_SIZE - 1); int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); cvmod_probability = constrain(base_probability + prob_mod, 0, 100); -- 2.39.5 From fc17afc9a1dbe5427e587a271716d3e14c56aee6 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 23:57:10 +0000 Subject: [PATCH 17/25] Remove Reset State (#26) This feature is essentially overlapping with loading default save slots. I need the few bytes it affords me. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/26 --- firmware/Gravity/Gravity.ino | 9 --------- firmware/Gravity/display.h | 14 ++------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index ebc0fe3..1944947 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -220,12 +220,6 @@ void HandleEncoderPressed() { InitGravity(app); } } - if (app.selected_param == PARAM_MAIN_RESET_STATE) { - if (app.selected_sub_param == 0) { // Reset - stateManager.reset(app); - InitGravity(app); - } - } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Erase // Show bootsplash during slow erase operation. @@ -305,9 +299,6 @@ void editMainParameter(int val) { case PARAM_MAIN_LOAD_DATA: updateSelection(app.selected_sub_param, val, StateManager::MAX_SAVE_SLOTS + 1); break; - case PARAM_MAIN_RESET_STATE: - updateSelection(app.selected_sub_param, val, 2); - break; case PARAM_MAIN_FACTORY_RESET: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 37b900b..d71c801 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -47,7 +47,7 @@ const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = * https://stncrn.github.io/u8g2-unifont-helper/ * "%/0123456789ABCDEFILNORSTUVXx" */ -const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") = +const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") PROGMEM = "\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL" "\64B\214\30\22\223\220)Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14" "\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:fW\207\320\210\60\42\304\204\30D\247" @@ -104,7 +104,6 @@ enum ParamsMainPage : uint8_t { PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_SAVE_DATA, PARAM_MAIN_LOAD_DATA, - PARAM_MAIN_RESET_STATE, PARAM_MAIN_FACTORY_RESET, PARAM_MAIN_LAST, }; @@ -297,15 +296,6 @@ void DisplayMainPage() { : F("LOAD FROM SLOT"); } break; - case PARAM_MAIN_RESET_STATE: - if (app.selected_sub_param == 0) { - mainText = F("RST"); - subText = F("RESET ALL"); - } else { - mainText = F("x"); - subText = F("BACK TO MAIN"); - } - break; case PARAM_MAIN_FACTORY_RESET: if (app.selected_sub_param == 0) { mainText = F("DEL"); @@ -321,7 +311,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } -- 2.39.5 From 872af30fbcbe8c89d9872b5f6efd254ca963ef5c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 23:59:24 +0000 Subject: [PATCH 18/25] Refactor CV Mod (#24) Move cv mod calculation to processClockTick. This is less ideas because it is an ISR, but it saves a significant amount of memory. Performance doesn't seem to take much of a hit. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/24 --- firmware/Gravity/Gravity.ino | 13 --- firmware/Gravity/channel.h | 190 +++++++++++++------------------- firmware/Gravity/display.h | 16 +-- firmware/Gravity/save_state.cpp | 14 +-- 4 files changed, 95 insertions(+), 138 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 1944947..4b19f2c 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -91,19 +91,6 @@ void loop() { // Process change in state of inputs and outputs. gravity.Process(); - // 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 < Gravity::OUTPUT_COUNT; i++) { - auto& ch = app.channel[i]; - // Only apply CV to the channel when the current channel has cv - // mod configured. - if (ch.isCvModActive()) { - ch.applyCvMod(cv1, cv2); - } - } - // Check for dirty state eligible to be saved. stateManager.update(app); diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 114e7fd..29a9ff3 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -70,14 +70,6 @@ class Channel { base_duty_cycle = 50; base_offset = 0; base_swing = 50; - base_euc_steps = 1; - base_euc_hits = 1; - - cvmod_clock_mod_index = base_clock_mod_index; - cvmod_probability = base_probability; - cvmod_duty_cycle = base_duty_cycle; - cvmod_offset = base_offset; - cvmod_swing = base_swing; cv1_dest = CV_DEST_NONE; cv2_dest = CV_DEST_NONE; @@ -88,78 +80,100 @@ class Channel { _recalculatePulses(); } + bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } + // Setters (Set the BASE value) void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); - if (!isCvModActive()) { - cvmod_clock_mod_index = base_clock_mod_index; - _recalculatePulses(); - } } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); - if (!isCvModActive()) { - cvmod_probability = base_probability; - _recalculatePulses(); - } } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); - if (!isCvModActive()) { - cvmod_duty_cycle = base_duty_cycle; - _recalculatePulses(); - } } void setOffset(int off) { base_offset = constrain(off, 0, 99); - if (!isCvModActive()) { - cvmod_offset = base_offset; - _recalculatePulses(); - } } void setSwing(int val) { base_swing = constrain(val, 50, 95); - if (!isCvModActive()) { - cvmod_swing = base_swing; - _recalculatePulses(); - } } // Euclidean void setSteps(int val) { - base_euc_steps = constrain(val, 1, MAX_PATTERN_LEN); - if (cv1_dest != CV_DEST_EUC_STEPS && cv2_dest != CV_DEST_EUC_STEPS) { - pattern.SetSteps(val); - } + pattern.SetSteps(val); } void setHits(int val) { - base_euc_hits = constrain(val, 1, base_euc_steps); - if (cv1_dest != CV_DEST_EUC_HITS && cv2_dest != CV_DEST_EUC_HITS) { - pattern.SetHits(val); - } + pattern.SetHits(val); } - void setCv1Dest(CvDestination dest) { cv1_dest = dest; } - void setCv2Dest(CvDestination dest) { cv2_dest = dest; } + void setCv1Dest(CvDestination dest) { + cv1_dest = dest; + _recalculatePulses(); + } + void setCv2Dest(CvDestination dest) { + cv2_dest = dest; + _recalculatePulses(); + } CvDestination getCv1Dest() const { return cv1_dest; } CvDestination getCv2Dest() const { return cv2_dest; } // Getters (Get the BASE value for editing or cv modded value for display) + int getProbability() const { return base_probability; } + int getDutyCycle() const { return base_duty_cycle; } + int getOffset() const { return base_offset; } + int getSwing() const { return base_swing; } + int getClockMod() const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex()]); } + int getClockModIndex() const { return base_clock_mod_index; } + byte getSteps() const { return pattern.GetSteps(); } + byte getHits() const { return pattern.GetHits(); } - int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } - int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } - int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } - int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex(withCvMod)]); } - int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } - bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } + // Getters that calculate the value with CV modulation applied. + int getClockModIndexWithMod(int cv1_val, int cv2_val) { + int clock_mod_index = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); + return constrain(base_clock_mod_index + clock_mod_index, 0, MOD_CHOICE_SIZE - 1); + } - byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; } - byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; } + int getClockModWithMod(int cv1_val, int cv2_val) { + int clock_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); + return pgm_read_word_near(&CLOCK_MOD[getClockModIndexWithMod(cv1_val, cv2_val)]); + } + + int getProbabilityWithMod(int cv1_val, int cv2_val) { + int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); + return constrain(base_probability + prob_mod, 0, 100); + } + + int getDutyCycleWithMod(int cv1_val, int cv2_val) { + int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); + return constrain(base_duty_cycle + duty_mod, 1, 99); + } + + int getOffsetWithMod(int cv1_val, int cv2_val) { + int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); + return constrain(base_offset + offset_mod, 0, 99); + } + + int getSwingWithMod(int cv1_val, int cv2_val) { + int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); + return constrain(base_swing + swing_mod, 50, 95); + } + + byte getStepsWithMod(int cv1_val, int cv2_val) { + int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + return constrain(pattern.GetSteps() + step_mod, 1, MAX_PATTERN_LEN); + } + + byte getHitsWithMod(int cv1_val, int cv2_val) { + // The number of hits is dependent on the modulated number of steps. + byte modulated_steps = getStepsWithMod(cv1_val, cv2_val); + int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, modulated_steps); + return constrain(pattern.GetHits() + hit_mod, 1, modulated_steps); + } void toggleMute() { mute = !mute; } @@ -176,6 +190,13 @@ class Channel { return; } + if (isCvModActive()) _recalculatePulses(); + + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); + int cvmod_clock_mod_index = getClockModIndexWithMod(cv1, cv2); + int cvmod_probability = getProbabilityWithMod(cv1, cv2); + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); // Conditionally apply swing on down beats. @@ -211,56 +232,6 @@ class Channel { output.Low(); } } - /** - * @brief Calculate and store cv modded values using bipolar mapping. - * Default to base value if not the current CV destination. - * - * @param cv1_val analog input reading for cv1 - * @param cv2_val analog input reading for cv2 - * - */ - void applyCvMod(int cv1_val, int cv2_val) { - // Note: This is optimized for cpu performance. This method is called - // from the main loop and stores the cv mod values. This reduces CPU - // cycles inside the internal clock interrupt, which is preferrable. - // However, if RAM usage grows too much, we have an opportunity to - // refactor this to store just the CV read values, and calculate the - // cv mod value per channel inside the getter methods by passing cv - // values. This would reduce RAM usage, but would introduce a - // significant CPU cost, which may have undesirable performance issues. - if (!isCvModActive()) { - cvmod_clock_mod_index = base_clock_mod_index; - cvmod_probability = base_clock_mod_index; - cvmod_duty_cycle = base_clock_mod_index; - cvmod_offset = base_clock_mod_index; - cvmod_swing = base_clock_mod_index; - return; - } - - int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); - cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, MOD_CHOICE_SIZE - 1); - - int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); - cvmod_probability = constrain(base_probability + prob_mod, 0, 100); - - int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); - cvmod_duty_cycle = constrain(base_duty_cycle + duty_mod, 1, 99); - - int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); - cvmod_offset = constrain(base_offset + offset_mod, 0, 99); - - int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); - cvmod_swing = constrain(base_swing + swing_mod, 50, 95); - - int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); - pattern.SetSteps(base_euc_steps + step_mod); - - int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, pattern.GetSteps()); - pattern.SetHits(base_euc_hits + hit_mod); - - // After all cvmod values are updated, recalculate clock pulse modifiers. - _recalculatePulses(); - } private: int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { @@ -270,13 +241,19 @@ class Channel { } void _recalculatePulses() { - const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); - _duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); - _offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); + int clock_mod_index = getClockModIndexWithMod(cv1, cv2); + int duty_cycle = getDutyCycleWithMod(cv1, cv2); + int offset = getOffsetWithMod(cv1, cv2); + int swing = getSwingWithMod(cv1, cv2); + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_mod_index]); + _duty_pulses = max((long)((mod_pulses * (100L - duty_cycle)) / 100L), 1L); + _offset_pulses = (long)((mod_pulses * (100L - offset)) / 100L); // Calculate the down beat swing amount. - if (cvmod_swing > 50) { - int shifted_swing = cvmod_swing - 50; + if (swing > 50) { + int shifted_swing = swing - 50; _swing_pulse_amount = (long)((mod_pulses * (100L - shifted_swing)) / 100L); } else { _swing_pulse_amount = 0; @@ -289,15 +266,6 @@ class Channel { byte base_duty_cycle; byte base_offset; byte base_swing; - byte base_euc_steps; - byte base_euc_hits; - - // Base value with cv mod applied. - byte cvmod_clock_mod_index; - byte cvmod_probability; - byte cvmod_duty_cycle; - byte cvmod_offset; - byte cvmod_swing; // CV mod configuration CvDestination cv1_dest; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d71c801..d83de64 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -328,10 +328,12 @@ void DisplayChannelPage() { // When editing a param, just show the base value. When not editing show // the value with cv mod. bool withCvMod = !app.editing_param; + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); switch (app.selected_param) { case PARAM_CH_MOD: { - int mod_value = ch.getClockMod(withCvMod); + int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2): ch.getClockMod(); if (mod_value > 1) { mainText = F("/"); mainText += String(mod_value); @@ -344,30 +346,30 @@ void DisplayChannelPage() { break; } case PARAM_CH_PROB: - mainText = String(ch.getProbability(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getProbabilityWithMod(cv1, cv2) : ch.getProbability()) + F("%"); subText = F("HIT CHANCE"); break; case PARAM_CH_DUTY: - mainText = String(ch.getDutyCycle(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getDutyCycleWithMod(cv1, cv2) : ch.getDutyCycle()) + F("%"); subText = F("PULSE WIDTH"); break; case PARAM_CH_OFFSET: - mainText = String(ch.getOffset(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getOffsetWithMod(cv1, cv2) : ch.getOffset()) + F("%"); subText = F("SHIFT HIT"); break; case PARAM_CH_SWING: ch.getSwing() == 50 ? mainText = F("OFF") - : mainText = String(ch.getSwing(withCvMod)) + F("%"); + : mainText = String(withCvMod ? ch.getSwingWithMod(cv1, cv2) : ch.getSwing()) + F("%"); subText = "DOWN BEAT"; swingDivisionMark(); break; case PARAM_CH_EUC_STEPS: - mainText = String(ch.getSteps(withCvMod)); + mainText = String(withCvMod ? ch.getStepsWithMod(cv1, cv2) : ch.getSteps()); subText = "EUCLID STEPS"; break; case PARAM_CH_EUC_HITS: - mainText = String(ch.getHits(withCvMod)); + mainText = String(withCvMod ? ch.getHitsWithMod(cv1, cv2) : ch.getHits()); subText = "EUCLID HITS"; break; case PARAM_CH_CV1_DEST: diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index e701c4f..f3f77c1 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -148,13 +148,13 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; auto& save_ch = save_data.channel_data[i]; - 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.base_swing = ch.getSwing(false); - save_ch.base_euc_steps = ch.getSteps(false); - save_ch.base_euc_hits = ch.getHits(false); + save_ch.base_clock_mod_index = ch.getClockModIndex(); + save_ch.base_probability = ch.getProbability(); + save_ch.base_duty_cycle = ch.getDutyCycle(); + save_ch.base_offset = ch.getOffset(); + save_ch.base_swing = ch.getSwing(); + save_ch.base_euc_steps = ch.getSteps(); + save_ch.base_euc_hits = ch.getHits(); save_ch.cv1_dest = static_cast(ch.getCv1Dest()); save_ch.cv2_dest = static_cast(ch.getCv2Dest()); } -- 2.39.5 From 1161da38c1e5e9d09352b52e137f04ecfa4b6453 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 00:25:06 +0000 Subject: [PATCH 19/25] Add menu options for using cv input as Clock Run/Reset (#25) Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/25 --- firmware/Gravity/Gravity.ino | 35 ++++++++++++++++++++++++++++++++- firmware/Gravity/app_state.h | 2 ++ firmware/Gravity/display.h | 34 ++++++++++++++++++++++++++++++-- firmware/Gravity/save_state.cpp | 8 +++++++- firmware/Gravity/save_state.h | 2 ++ src/analog_input.h | 14 +++++++++++++ src/digital_output.h | 1 - 7 files changed, 91 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 4b19f2c..8618f46 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -42,7 +42,7 @@ * * CV1: * External analog input used to provide modulation to any channel parameter. - * + * * CV2: * External analog input used to provide modulation to any channel parameter. * @@ -91,6 +91,9 @@ void loop() { // Process change in state of inputs and outputs. gravity.Process(); + // Check if cv run or reset is active and read cv. + CheckRunReset(gravity.cv1, gravity.cv2); + // Check for dirty state eligible to be saved. stateManager.update(app); @@ -158,6 +161,27 @@ void HandleExtClockTick() { app.refresh_screen = true; } +void CheckRunReset(AnalogInput& cv1, AnalogInput& cv2) { + // Clock Run + if (app.cv_run == 1 || app.cv_run == 2) { + const int val = (app.cv_run == 1) ? cv1.Read() : cv2.Read(); + if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) { + gravity.clock.Start(); + app.refresh_screen = true; + } else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) { + gravity.clock.Stop(); + ResetOutputs(); + app.refresh_screen = true; + } + } + + // Clock Reset + if ((app.cv_reset == 1 && cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) || + (app.cv_reset == 2 && cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) { + gravity.clock.Reset(); + } +} + // // UI handlers for encoder and buttons. // @@ -263,6 +287,14 @@ void editMainParameter(int val) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); app.tempo = gravity.clock.Tempo(); break; + case PARAM_MAIN_RUN: + updateSelection(app.selected_sub_param, val, 3); + app.cv_run = app.selected_sub_param; + break; + case PARAM_MAIN_RESET: + updateSelection(app.selected_sub_param, val, 3); + app.cv_reset = app.selected_sub_param; + break; case PARAM_MAIN_SOURCE: { byte source = static_cast(app.selected_source); updateSelection(source, val, Clock::SOURCE_LAST); @@ -279,6 +311,7 @@ void editMainParameter(int val) { } break; } + // These changes are applied upon encoder button press. case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 90712df..0f06f02 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -25,6 +25,8 @@ struct AppState { byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_swing = 0; byte selected_save_slot = 0; // The currently active save slot. + byte cv_run = 0; + byte cv_reset = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; bool editing_param = false; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d83de64..d5a5f6e 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -100,6 +100,8 @@ constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; enum ParamsMainPage : uint8_t { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, + PARAM_MAIN_RUN, + PARAM_MAIN_RESET, PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_SAVE_DATA, @@ -259,6 +261,34 @@ void DisplayMainPage() { break; } break; + case PARAM_MAIN_RUN: + mainText = F("RUN"); + switch (app.cv_run) { + case 0: + subText = F("NONE"); + break; + case 1: + subText = F("CV 1"); + break; + case 2: + subText = F("CV 2"); + break; + } + break; + case PARAM_MAIN_RESET: + mainText = F("RST"); + switch (app.cv_reset) { + case 0: + subText = F("NONE"); + break; + case 1: + subText = F("CV 1"); + break; + case 2: + subText = F("CV 2"); + break; + } + break; case PARAM_MAIN_PULSE: mainText = F("OUT"); switch (app.selected_pulse) { @@ -311,7 +341,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -457,7 +487,7 @@ void UpdateDisplay() { DisplayChannelPage(); } // Global channel select UI. - DisplaySelectedChannel(); + DisplaySelectedChannel(); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index f3f77c1..4950d7e 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA3"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA4"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; @@ -87,6 +87,8 @@ void StateManager::reset(AppState& app) { app.selected_channel = default_app.selected_channel; app.selected_source = default_app.selected_source; app.selected_pulse = default_app.selected_pulse; + app.cv_run = default_app.cv_run; + app.cv_reset = default_app.cv_reset; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -140,6 +142,8 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); save_data.selected_pulse = static_cast(app.selected_pulse); + save_data.cv_run = app.cv_run; + save_data.cv_reset = app.cv_reset; // TODO: break this out into a separate function. Save State should be // broken out into global / per-channel save methods. When saving via @@ -179,6 +183,8 @@ void StateManager::_loadState(AppState& app, byte slot_index) { app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); app.selected_pulse = static_cast(load_data.selected_pulse); + app.cv_run = load_data.cv_run; + app.cv_reset = load_data.cv_reset; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& ch = app.channel[i]; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 8f25dd1..34bfffe 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -76,6 +76,8 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; + byte cv_run; + byte cv_reset; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; diff --git a/src/analog_input.h b/src/analog_input.h index 496899b..2be670a 100644 --- a/src/analog_input.h +++ b/src/analog_input.h @@ -19,6 +19,8 @@ const int CALIBRATED_HIGH = 512; class AnalogInput { public: + static const int GATE_THRESHOLD = 0; + AnalogInput() {} ~AnalogInput() {} @@ -74,6 +76,18 @@ class AnalogInput { */ inline float Voltage() { return ((read_ / 512.0) * 5.0); } + /** + * Checks for a rising edge transition across a threshold. + * + * @param threshold The value that the input must cross. + * @return True if the value just crossed the threshold from below, false otherwise. + */ + inline bool IsRisingEdge(int16_t threshold) const { + bool was_high = old_read_ > threshold; + bool is_high = read_ > threshold; + return is_high && !was_high; + } + private: uint8_t pin_; int16_t read_; diff --git a/src/digital_output.h b/src/digital_output.h index 9c4cfc8..ccb6f04 100644 --- a/src/digital_output.h +++ b/src/digital_output.h @@ -82,7 +82,6 @@ class DigitalOutput { unsigned long last_triggered_; uint8_t trigger_duration_; uint8_t cv_pin_; - uint8_t led_pin_; bool on_; void update(uint8_t state) { -- 2.39.5 From 7c0262840385bb517a37b57fa117245e74b29c00 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 00:26:20 +0000 Subject: [PATCH 20/25] Add more EXT clock source options (#23) Fixes https://github.com/awonak/alt-gravity/issues/12 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/23 --- firmware/Gravity/display.h | 6 ++++++ src/clock.h | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d5a5f6e..faeb4d1 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -256,6 +256,12 @@ void DisplayMainPage() { case Clock::SOURCE_EXTERNAL_PPQN_4: subText = F("4 PPQN"); break; + case Clock::SOURCE_EXTERNAL_PPQN_2: + subText = F("2 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_1: + subText = F("1 PPQN"); + break; case Clock::SOURCE_EXTERNAL_MIDI: subText = F("MIDI"); break; diff --git a/src/clock.h b/src/clock.h index 613667a..ea39214 100644 --- a/src/clock.h +++ b/src/clock.h @@ -35,6 +35,8 @@ class Clock { SOURCE_INTERNAL, SOURCE_EXTERNAL_PPQN_24, SOURCE_EXTERNAL_PPQN_4, + SOURCE_EXTERNAL_PPQN_2, + SOURCE_EXTERNAL_PPQN_1, SOURCE_EXTERNAL_MIDI, SOURCE_LAST, }; @@ -96,6 +98,14 @@ class Clock { uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_4); break; + case SOURCE_EXTERNAL_PPQN_2: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_2); + break; + case SOURCE_EXTERNAL_PPQN_1: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_1); + break; case SOURCE_EXTERNAL_MIDI: uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_24); -- 2.39.5 From c5965aa1f7aeaa830f55c3f824f4f43d10b8f36c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 18:45:21 -0700 Subject: [PATCH 21/25] bug fix - need to recalculate pulses when mod duty and swing are changed. --- firmware/Gravity/channel.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 29a9ff3..fd97537 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -86,6 +86,7 @@ class Channel { void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); + _recalculatePulses(); } void setProbability(int prob) { @@ -94,13 +95,16 @@ class Channel { void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); + _recalculatePulses(); } void setOffset(int off) { base_offset = constrain(off, 0, 99); + _recalculatePulses(); } void setSwing(int val) { base_swing = constrain(val, 50, 95); + _recalculatePulses(); } // Euclidean -- 2.39.5 From 6ada2aba30fef81a4b182737280208e8be2ad742 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 02:47:59 +0000 Subject: [PATCH 22/25] Add option to rotate the display (#27) I needed to cut the bootsplash to make room for adding this features. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/27 --- firmware/Gravity/Gravity.ino | 59 +++++++++++++++++---------------- firmware/Gravity/app_state.h | 1 + firmware/Gravity/display.h | 30 ++++------------- firmware/Gravity/save_state.cpp | 3 +- firmware/Gravity/save_state.h | 5 +-- library.properties | 2 +- src/clock.h | 9 ++--- 7 files changed, 46 insertions(+), 63 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 8618f46..aef4ab8 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -66,10 +66,6 @@ void setup() { // Start Gravity. gravity.Init(); - // Show bootsplash when initializing firmware. - Bootsplash(); - delay(2000); - // Initialize the state manager. This will load settings from EEPROM stateManager.initialize(app); InitGravity(app); @@ -213,31 +209,34 @@ void HandleEncoderPressed() { // Check if leaving editing mode should apply a selection. if (app.editing_param) { if (app.selected_channel == 0) { // main page - // TODO: rewrite as switch - if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { - app.encoder_reversed = app.selected_sub_param == 1; - gravity.encoder.SetReverseDirection(app.encoder_reversed); - } - if (app.selected_param == PARAM_MAIN_SAVE_DATA) { - if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { - app.selected_save_slot = app.selected_sub_param; - stateManager.saveData(app); - } - } - if (app.selected_param == PARAM_MAIN_LOAD_DATA) { - if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { - app.selected_save_slot = app.selected_sub_param; - stateManager.loadData(app, app.selected_save_slot); - InitGravity(app); - } - } - if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { - if (app.selected_sub_param == 0) { // Erase - // Show bootsplash during slow erase operation. - Bootsplash(); - stateManager.factoryReset(app); - InitGravity(app); - } + switch (app.selected_param) { + case PARAM_MAIN_ENCODER_DIR: + app.encoder_reversed = app.selected_sub_param == 1; + gravity.encoder.SetReverseDirection(app.encoder_reversed); + break; + case PARAM_MAIN_ROTATE_DISP: + app.rotate_display = app.selected_sub_param == 1; + gravity.display.setFlipMode(app.rotate_display ? 1 : 0); + break; + case PARAM_MAIN_SAVE_DATA: + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.saveData(app); + } + break; + case PARAM_MAIN_LOAD_DATA: + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.loadData(app, app.selected_save_slot); + InitGravity(app); + } + break; + case PARAM_MAIN_FACTORY_RESET: + if (app.selected_sub_param == 0) { // Erase + stateManager.factoryReset(app); + InitGravity(app); + } + break; } } // Only mark dirty and reset selected_sub_param when leaving editing mode. @@ -313,6 +312,7 @@ void editMainParameter(int val) { } // These changes are applied upon encoder button press. case PARAM_MAIN_ENCODER_DIR: + case PARAM_MAIN_ROTATE_DISP: updateSelection(app.selected_sub_param, val, 2); break; case PARAM_MAIN_SAVE_DATA: @@ -381,6 +381,7 @@ void InitGravity(AppState& app) { gravity.clock.SetTempo(app.tempo); gravity.clock.SetSource(app.selected_source); gravity.encoder.SetReverseDirection(app.encoder_reversed); + gravity.display.setFlipMode(app.rotate_display ? 1 : 0); } void ResetOutputs() { diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 0f06f02..c02f8a3 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -31,6 +31,7 @@ struct AppState { Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; bool editing_param = false; bool encoder_reversed = false; + bool rotate_display = false; bool refresh_screen = true; }; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index faeb4d1..7c4abfa 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -104,6 +104,7 @@ enum ParamsMainPage : uint8_t { PARAM_MAIN_RESET, PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_ROTATE_DISP, PARAM_MAIN_SAVE_DATA, PARAM_MAIN_LOAD_DATA, PARAM_MAIN_FACTORY_RESET, @@ -256,9 +257,6 @@ void DisplayMainPage() { case Clock::SOURCE_EXTERNAL_PPQN_4: subText = F("4 PPQN"); break; - case Clock::SOURCE_EXTERNAL_PPQN_2: - subText = F("2 PPQN"); - break; case Clock::SOURCE_EXTERNAL_PPQN_1: subText = F("1 PPQN"); break; @@ -316,6 +314,10 @@ void DisplayMainPage() { mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); break; + case PARAM_MAIN_ROTATE_DISP: + mainText = F("ROT"); + subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("FLIPPED"); + break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) { @@ -347,7 +349,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("ROTATE DISP"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -369,7 +371,7 @@ void DisplayChannelPage() { switch (app.selected_param) { case PARAM_CH_MOD: { - int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2): ch.getClockMod(); + int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2) : ch.getClockMod(); if (mod_value > 1) { mainText = F("/"); mainText += String(mod_value); @@ -497,22 +499,4 @@ void UpdateDisplay() { } while (gravity.display.nextPage()); } -void Bootsplash() { - gravity.display.firstPage(); - do { - int textWidth; - String loadingText = F("LOADING...."); - gravity.display.setFont(TEXT_FONT); - - textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME); - gravity.display.drawStr(16 + (textWidth / 2), 20, StateManager::SKETCH_NAME); - - textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION); - gravity.display.drawStr(16 + (textWidth / 2), 32, StateManager::SEMANTIC_VERSION); - - textWidth = gravity.display.getStrWidth(loadingText.c_str()); - gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str()); - } while (gravity.display.nextPage()); -} - #endif // DISPLAY_H diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 4950d7e..ff00005 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA4"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "2.0.0"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; @@ -212,6 +212,7 @@ void StateManager::_saveMetadata(const AppState& app) { // Global user settings current_meta.selected_save_slot = app.selected_save_slot; current_meta.encoder_reversed = app.encoder_reversed; + current_meta.rotate_display = app.rotate_display; EEPROM.put(METADATA_START_ADDR, current_meta); interrupts(); diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 34bfffe..29226e7 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -52,11 +52,12 @@ class StateManager { // This struct holds the data that identifies the firmware version. struct Metadata { - char sketch_name[16]; - char version[16]; + char sketch_name[12]; + char version[5]; // Additional global/hardware settings byte selected_save_slot; bool encoder_reversed; + bool rotate_display; }; struct ChannelState { byte base_clock_mod_index; diff --git a/library.properties b/library.properties index 1390c78..bce41dd 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=libGravity -version=2.0.0beta3 +version=2.0.0 author=Adam Wonak maintainer=awonak sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module diff --git a/src/clock.h b/src/clock.h index ea39214..9bc695c 100644 --- a/src/clock.h +++ b/src/clock.h @@ -35,7 +35,6 @@ class Clock { SOURCE_INTERNAL, SOURCE_EXTERNAL_PPQN_24, SOURCE_EXTERNAL_PPQN_4, - SOURCE_EXTERNAL_PPQN_2, SOURCE_EXTERNAL_PPQN_1, SOURCE_EXTERNAL_MIDI, SOURCE_LAST, @@ -43,9 +42,9 @@ class Clock { enum Pulse { PULSE_NONE, - PULSE_PPQN_1, - PULSE_PPQN_4, PULSE_PPQN_24, + PULSE_PPQN_4, + PULSE_PPQN_1, PULSE_LAST, }; @@ -98,10 +97,6 @@ class Clock { uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_4); break; - case SOURCE_EXTERNAL_PPQN_2: - uClock.setClockMode(uClock.EXTERNAL_CLOCK); - uClock.setInputPPQN(uClock.PPQN_2); - break; case SOURCE_EXTERNAL_PPQN_1: uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_1); -- 2.39.5 From 4bcd6180733a84f4d9a108ec6e76af38710e00a7 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:18:45 -0700 Subject: [PATCH 23/25] Add skeleton app to examples --- examples/skeleton/skeleton.ino | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 examples/skeleton/skeleton.ino diff --git a/examples/skeleton/skeleton.ino b/examples/skeleton/skeleton.ino new file mode 100644 index 0000000..eb07186 --- /dev/null +++ b/examples/skeleton/skeleton.ino @@ -0,0 +1,118 @@ +/** + * @file skeleton.ino + * @author YOUR_NAME () + * @brief Skeleton app for Sitka Instruments Gravity. + * @version v1.0.0 - August 2025 + * @date 2025-08-12 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + * Skeleton app for basic structure of a new firmware for Sitka Instruments + * Gravity using the libGravity library. + * + * ENCODER: + * Press: change between selecting a parameter and editing the parameter. + * Hold & Rotate: change current selected output channel. + * + * BTN1: + * Play/pause - start or stop the internal clock. + * + * BTN2: + * Shift - hold and rotate encoder to change current selected output channel. + * + * EXT: + * External clock input. When Gravity is set to INTERNAL or MIDI clock + * source, this input is used to reset clocks. + * + * CV1: + * External analog input used to provide modulation to any channel parameter. + * + * CV2: + * External analog input used to provide modulation to any channel parameter. + * + */ + +#include + + + +// Global state for settings and app behavior. +struct AppState { + int tempo = Clock::DEFAULT_TEMPO; + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + // Add app specific state variables here. +}; + +AppState app; + +// +// Arduino setup and loop. +// + +void setup() { + // Start Gravity. + gravity.Init(); + + // Clock handlers. + gravity.clock.AttachIntHandler(HandleIntClockTick); + gravity.clock.AttachExtHandler(HandleExtClockTick); + + // Encoder rotate and press handlers. + gravity.encoder.AttachPressHandler(HandleEncoderPressed); + gravity.encoder.AttachRotateHandler(HandleRotate); + gravity.encoder.AttachPressRotateHandler(HandlePressedRotate); + + // Button press handlers. + gravity.play_button.AttachPressHandler(HandlePlayPressed); +} + +void loop() { + // Process change in state of inputs and outputs. + gravity.Process(); + + // Non-ISR loop behavior. +} + +// +// Firmware handlers for clocks. +// + +void HandleIntClockTick(uint32_t tick) { + bool refresh = false; + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + // Process each output tick handlers. + } +} + +void HandleExtClockTick() { + switch (app.selected_source) { + case Clock::SOURCE_INTERNAL: + case Clock::SOURCE_EXTERNAL_MIDI: + // Use EXT as Reset when not used for clock source. + gravity.clock.Reset(); + break; + default: + // Register EXT cv clock tick. + gravity.clock.Tick(); + } +} + +// +// UI handlers for encoder and buttons. +// + +void HandlePlayPressed() { +} + +void HandleEncoderPressed() { +} + +void HandleRotate(int val) { +} + +void HandlePressedRotate(int val) { +} + +// +// Application logic goes here. +// -- 2.39.5 From b5029bde888ef0b5d31a81bfbe85785b262bdb2f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:19:06 -0700 Subject: [PATCH 24/25] add skeleton app to examples --- examples/skeleton/skeleton.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/skeleton/skeleton.ino b/examples/skeleton/skeleton.ino index eb07186..a71b878 100644 --- a/examples/skeleton/skeleton.ino +++ b/examples/skeleton/skeleton.ino @@ -2,8 +2,8 @@ * @file skeleton.ino * @author YOUR_NAME () * @brief Skeleton app for Sitka Instruments Gravity. - * @version v1.0.0 - August 2025 - * @date 2025-08-12 + * @version vX.Y.Z - MONTH YEAR YOUR_NAME + * @date YYYY-MM-DD * * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * -- 2.39.5 From 3f670fa9f78b69327ce6b37abf4c8d2ae76267d7 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:42:02 -0700 Subject: [PATCH 25/25] Update docs and example firmware --- README.md | 19 +++++++++++++++---- .../calibrate_analog/calibrate_analog.ino | 4 ++-- .../calibrate_analog2/calibrate_analog2.ino | 4 ++-- examples/hardware_test/hardware_test.ino | 16 ++++++++-------- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 569aeed..7afaabf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Sitka Instruments Gravity Firmware Abstraction -This library helps make writing firmware easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library. +This library helps make writing firmware for the [Sitka Instruments Gravity](https://sitkainstruments.com/gravity/) eurorack module easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library. + +The latest releases of all Sitka Instruments Gravity firmware builds can be found on the [Updater](https://sitkainstruments.com/gravity/updater/) page. You can use this page to flash the latest build directly to the Arduino Nano on the back of your module. ## Installation @@ -17,13 +19,14 @@ Common directory locations: * [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. * [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. +* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt. ## Example Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation. ```cpp -#include "gravity.h" +#include "libGravity.h" byte idx = 0; bool reversed = false; @@ -75,11 +78,11 @@ void HandlePlayPressed() { } } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (selected_param == 0) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); } else if (selected_param == 1) { - reversed = (dir == DIRECTION_DECREMENT); + reversed = (val < 0); } } @@ -111,6 +114,14 @@ void UpdateDisplay() { } ``` +**Builing New Firmware Using libGravity** + +When starting a new firmware sketch you can use the [skeleton](examples/skeleton/skeleton.ino) app as a place to start. + +**Building New Firmware from scratch** + +If you do not want to use the libGravity hardware abstraction library and want to roll your own vanilla firmware, take a look at the [peripherials.h](src/peripherials.h) file for the pinout definitions used by the module. + ### Build for release ``` diff --git a/examples/calibrate_analog/calibrate_analog.ino b/examples/calibrate_analog/calibrate_analog.ino index c3e6a5f..388494f 100644 --- a/examples/calibrate_analog/calibrate_analog.ino +++ b/examples/calibrate_analog/calibrate_analog.ino @@ -17,7 +17,7 @@ * TODO: Store the calibration value in EEPROM. */ -#include "gravity.h" +#include "libGravity.h" #define TEXT_FONT u8g2_font_profont11_tf #define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t @@ -43,7 +43,7 @@ void NextCalibrationPoint() { selected_param = (selected_param + 1) % 6; } -void CalibrateCV(Direction dir, int val) { +void CalibrateCV(int val) { AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; switch (selected_param % 3) { case 0: diff --git a/examples/calibrate_analog2/calibrate_analog2.ino b/examples/calibrate_analog2/calibrate_analog2.ino index ac5d772..48958d2 100644 --- a/examples/calibrate_analog2/calibrate_analog2.ino +++ b/examples/calibrate_analog2/calibrate_analog2.ino @@ -14,7 +14,7 @@ * */ -#include "gravity.h" +#include "libGravity.h" #define TEXT_FONT u8g2_font_profont11_tf @@ -39,7 +39,7 @@ void NextCalibrationPoint() { selected_param = (selected_param + 1) % 2; } -void CalibrateCV(Direction dir, int val) { +void CalibrateCV(int val) { // AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; AnalogInput* cv = &gravity.cv1; switch (selected_param % 2) { diff --git a/examples/hardware_test/hardware_test.ino b/examples/hardware_test/hardware_test.ino index 5f3d345..9ba6b90 100644 --- a/examples/hardware_test/hardware_test.ino +++ b/examples/hardware_test/hardware_test.ino @@ -1,4 +1,4 @@ -#include "gravity.h" +#include "libGravity.h" byte idx = 0; bool reversed = false; @@ -33,28 +33,28 @@ void IntClock(uint32_t tick) { if (tick % 12 == 0 && ! freeze) { gravity.outputs[idx].Low(); if (reversed) { - idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1; + idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1; } else { - idx = (idx + 1) % OUTPUT_COUNT; + idx = (idx + 1) % Gravity::OUTPUT_COUNT; } gravity.outputs[idx].High(); } } void HandlePlayPressed() { - gravity.clock.Pause(); + gravity.clock.Stop(); if (gravity.clock.IsPaused()) { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { gravity.outputs[i].Low(); } } } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (selected_param == 0) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); } else if (selected_param == 1) { - reversed = (dir == DIRECTION_DECREMENT); + reversed = (val < 0); } } @@ -80,7 +80,7 @@ void UpdateDisplay() { gravity.display.print("Direction: "); gravity.display.print((reversed) ? "Backward" : "Forward"); - gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1); + gravity.display.drawStr(0, selected_param * 10, "x"); gravity.display.display(); } \ No newline at end of file -- 2.39.5