From 60a7a7a349a90d339788e4ed0a261ec62a42ed5c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 15:21:03 -0700 Subject: [PATCH 01/15] fix example code --- examples/clock_mod/clock_mod.ino | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/clock_mod/clock_mod.ino b/examples/clock_mod/clock_mod.ino index e9458ea..8033c0d 100644 --- a/examples/clock_mod/clock_mod.ino +++ b/examples/clock_mod/clock_mod.ino @@ -33,8 +33,8 @@ struct AppState { bool editing_param = false; int selected_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel - Source selected_source = SOURCE_INTERNAL; - Channel channel[OUTPUT_COUNT]; + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Channel channel[Gravity::OUTPUT_COUNT]; }; AppState app; @@ -123,7 +123,7 @@ void loop() { // void HandleIntClockTick(uint32_t tick) { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& channel = app.channel[i]; auto& output = gravity.outputs[i]; @@ -178,7 +178,7 @@ void HandleEncoderPressed() { app.refresh_screen = true; } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (!app.editing_param) { // Navigation Mode const int max_param = (app.selected_channel == 0) ? PARAM_MAIN_LAST : PARAM_CH_LAST; @@ -188,17 +188,17 @@ void HandleRotate(Direction dir, int val) { if (app.selected_channel == 0) { editMainParameter(val); } else { - editChannelParameter(dir, val); + editChannelParameter(val); } } app.refresh_screen = true; } -void HandlePressedRotate(Direction dir, int val) { - if (dir == DIRECTION_INCREMENT && app.selected_channel < OUTPUT_COUNT) { +void HandlePressedRotate(int val) { + if (val > 0 && app.selected_channel < Gravity::OUTPUT_COUNT) { app.selected_channel++; - } else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) { + } else if (val < 0 && app.selected_channel > 0) { app.selected_channel--; } app.selected_param = 0; @@ -216,21 +216,21 @@ void editMainParameter(int val) { case PARAM_MAIN_SOURCE: { int source = static_cast(app.selected_source); - updateSelection(source, val, SOURCE_LAST); - app.selected_source = static_cast(source); + updateSelection(source, val, Clock::SOURCE_LAST); + app.selected_source = static_cast(source); gravity.clock.SetSource(app.selected_source); break; } } } -void editChannelParameter(Direction dir, int val) { +void editChannelParameter(int val) { auto& ch = GetSelectedChannel(); switch (static_cast(app.selected_param)) { case PARAM_CH_MOD: - if (dir == DIRECTION_INCREMENT && ch.clock_mod_index < MOD_CHOICE_SIZE - 1) { + if (val > 0 && ch.clock_mod_index < MOD_CHOICE_SIZE - 1) { ch.clock_mod_index++; - } else if (dir == DIRECTION_DECREMENT && ch.clock_mod_index > 0) { + } else if (val < 0 && ch.clock_mod_index > 0) { ch.clock_mod_index--; } break; @@ -265,7 +265,7 @@ Channel& GetSelectedChannel() { } void ResetOutputs() { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { gravity.outputs[i].Low(); } } @@ -311,7 +311,7 @@ void DisplayMainPage() { if (app.selected_param == 0) { // Serial MIID is too unstable to display bpm in real time. - if (app.selected_source == SOURCE_EXTERNAL_MIDI) { + if (app.selected_source == Clock::SOURCE_EXTERNAL_MIDI) { sprintf(mainText, "%s", "EXT"); } else { sprintf(mainText, "%d", gravity.clock.Tempo()); @@ -319,19 +319,19 @@ void DisplayMainPage() { subText = "BPM"; } else if (app.selected_param == 1) { switch (app.selected_source) { - case SOURCE_INTERNAL: + case Clock::SOURCE_INTERNAL: sprintf(mainText, "%s", "INT"); subText = "Clock"; break; - case SOURCE_EXTERNAL_PPQN_24: + case Clock::SOURCE_EXTERNAL_PPQN_24: sprintf(mainText, "%s", "EXT"); subText = "24 PPQN"; break; - case SOURCE_EXTERNAL_PPQN_4: + case Clock::SOURCE_EXTERNAL_PPQN_4: sprintf(mainText, "%s", "EXT"); subText = "4 PPQN"; break; - case SOURCE_EXTERNAL_MIDI: + case Clock::SOURCE_EXTERNAL_MIDI: sprintf(mainText, "%s", "EXT"); subText = "MIDI"; break; @@ -399,7 +399,7 @@ void DisplaySelectedChannel() { gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2); gravity.display.drawVLine(SCREEN_WIDTH - 2, boxY, boxHeight); - for (int i = 0; i < OUTPUT_COUNT + 1; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT + 1; i++) { // Draw box frame or filled selected box. gravity.display.setDrawColor(1); (app.selected_channel == i) From 385ce85da3b37db8f9db23f0c62902fe69484d21 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 13 Jul 2025 12:00:37 -0700 Subject: [PATCH 02/15] add const for pulse clock mod choices. update git ignore and readme for build command. --- .gitignore | 3 ++- README.md | 5 +++++ firmware/Gravity/Gravity.ino | 6 +++--- firmware/Gravity/channel.h | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 41cc7c8..af53804 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ docs .vscode -.DS_Store \ No newline at end of file +.DS_Store +build/* \ No newline at end of file diff --git a/README.md b/README.md index f244487..569aeed 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,8 @@ void UpdateDisplay() { } ``` +### Build for release + +``` +$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/ +``` \ No newline at end of file diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 34de653..374ee03 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -125,13 +125,13 @@ void HandleIntClockTick(uint32_t tick) { int clock_index; switch (app.selected_pulse) { case Clock::PULSE_PPQN_24: - clock_index = 0; + clock_index = PULSE_PPQN_24_CLOCK_MOD_INDEX; break; case Clock::PULSE_PPQN_4: - clock_index = 4; + clock_index = PULSE_PPQN_4_CLOCK_MOD_INDEX; break; case Clock::PULSE_PPQN_1: - clock_index = 7; + clock_index = PULSE_PPQN_1_CLOCK_MOD_INDEX; break; } diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 6f46b28..8036781 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -53,6 +53,10 @@ static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // 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; + class Channel { public: Channel() { From 5729eef037d31981e1829a2d436899670540594a Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:00:47 +0000 Subject: [PATCH 03/15] 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 { From 1bf90e16742d6e789534bf6e5dc12e0e7d5e76e4 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:01:18 +0000 Subject: [PATCH 04/15] 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; From 4f04137f67059f4b2f58f4bbb4dfb5150a7768c8 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:27:32 +0000 Subject: [PATCH 05/15] 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); From 01f32407f6594405a6c3539194bad655be179b30 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 20 Jul 2025 17:53:03 -0700 Subject: [PATCH 06/15] 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. From 1c0fb86bc102c303907de39642d5ca2bfaf1e88d Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 00:00:49 +0000 Subject: [PATCH 07/15] 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: From b0accdc83a6625b7a8c9333c9e174b98d3653559 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:12:45 +0000 Subject: [PATCH 08/15] 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]; }; From c5bddef66d0ddd4017339ddd26862e79e25c9d1c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:16:32 +0000 Subject: [PATCH 09/15] 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; From ec34bc3a7bba67d9da002c1b6228e778a3213114 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 23 Jul 2025 03:32:16 +0000 Subject: [PATCH 10/15] 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; From c7a3277b5fc9e2ae10987f3f972acb498cc9256f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 07:53:41 -0700 Subject: [PATCH 11/15] 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; }; From 65dde4d62e8ff3ddc063d2181d03e013daf0b917 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 15:07:15 +0000 Subject: [PATCH 12/15] 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__ */ From d1c8ee16a497566824d579606097e3f2c68c10eb Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 08:35:05 -0700 Subject: [PATCH 13/15] 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; } From dd7217d04ef1a95a0bec565fcded10bd0667b76f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:27:24 -0700 Subject: [PATCH 14/15] 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. From 19473db67e761c7f96a3ff0d4e33e87dfb568ca1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:38:34 -0700 Subject: [PATCH 15/15] 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