Lots of changes and optimizations

- add reverse encoder menu option and save state
- improve usage of EncoderDir in ISR with pointer to instance and static isr() method.
- reduce u8g2 memory usage by using single page buffer
- improve save state behavor by using a mutex flag and update check with debounce in main loop
- make saving to EEPROM safer by wrapping put calls with noInterrupts()
This commit is contained in:
2025-06-15 19:20:16 -07:00
parent 0cef942f2c
commit 8644a3e752
7 changed files with 124 additions and 61 deletions

View File

@ -34,7 +34,9 @@ class EncoderDir {
public: public:
EncoderDir() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3), EncoderDir() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3),
button_(ENCODER_SW_PIN) {} button_(ENCODER_SW_PIN) {
_instance = this;
}
~EncoderDir() {} ~EncoderDir() {}
// Set to true if the encoder read direction should be reversed. // Set to true if the encoder read direction should be reversed.
@ -81,15 +83,19 @@ class EncoderDir {
} }
} }
// Read the encoder state and update the read position. static void isr() {
void UpdateEncoder() { // If the instance has been created, call its tick() method.
encoder_.tick(); if (_instance) {
_instance->encoder_.tick();
}
} }
private: private:
static EncoderDir* _instance;
int previous_pos_; int previous_pos_;
bool rotated_while_held_; bool rotated_while_held_;
bool reversed_ = true; bool reversed_ = false;
RotaryEncoder encoder_; RotaryEncoder encoder_;
Button button_; Button button_;
@ -115,15 +121,18 @@ class EncoderDir {
change *= 2; change *= 2;
} }
if (reversed_) {
change = -(change);
}
return change; return change;
} }
inline Direction rotate_(int dir, bool reversed) { inline Direction rotate_(int dir, bool reversed) {
switch (dir) { switch (dir) {
case 1: case 1:
return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT;
case -1:
return (reversed) ? DIRECTION_DECREMENT : DIRECTION_INCREMENT; return (reversed) ? DIRECTION_DECREMENT : DIRECTION_INCREMENT;
case -1:
return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT;
default: default:
return DIRECTION_UNCHANGED; return DIRECTION_UNCHANGED;
} }

View File

@ -30,6 +30,7 @@ StateManager stateManager;
enum ParamsMainPage { enum ParamsMainPage {
PARAM_MAIN_TEMPO, PARAM_MAIN_TEMPO,
PARAM_MAIN_SOURCE, PARAM_MAIN_SOURCE,
PARAM_MAIN_ENCODER_DIR,
PARAM_MAIN_RESET_STATE, PARAM_MAIN_RESET_STATE,
PARAM_MAIN_LAST, PARAM_MAIN_LAST,
}; };
@ -139,6 +140,9 @@ void loop() {
app.channel[i].applyCvMod(cv1, cv2); app.channel[i].applyCvMod(cv1, cv2);
} }
// Check for dirty state eligible to be saved.
stateManager.update(app);
if (app.refresh_screen) { if (app.refresh_screen) {
UpdateDisplay(); UpdateDisplay();
} }
@ -194,19 +198,23 @@ void HandleEncoderPressed() {
// Check if leaving editing mode should apply a selection. // Check if leaving editing mode should apply a selection.
if (app.editing_param) { if (app.editing_param) {
if (app.selected_channel == 0) { // main page if (app.selected_channel == 0) { // main page
if (app.selected_param == PARAM_MAIN_ENCODER_DIR) {
bool reversed = app.selected_sub_param == 1;
gravity.encoder.SetReverseDirection(reversed);
}
// Reset state // Reset state
if (app.selected_param == PARAM_MAIN_RESET_STATE) { if (app.selected_param == PARAM_MAIN_RESET_STATE) {
if (app.selected_sub_param == 0) { // Reset if (app.selected_sub_param == 0) { // Reset
stateManager.reset(app); stateManager.reset(app);
// stateManager.initialize(app);
InitAppState(app); InitAppState(app);
} }
} }
} }
// Only mark dirty when leaving editing mode.
stateManager.markDirty();
} }
app.selected_sub_param = 0; app.selected_sub_param = 0;
app.editing_param = !app.editing_param; app.editing_param = !app.editing_param;
stateManager.save(app);
app.refresh_screen = true; app.refresh_screen = true;
} }
@ -223,7 +231,6 @@ void HandleRotate(Direction dir, int val) {
editChannelParameter(val); editChannelParameter(val);
} }
} }
stateManager.save(app);
app.refresh_screen = true; app.refresh_screen = true;
} }
@ -234,7 +241,7 @@ void HandlePressedRotate(Direction dir, int val) {
app.selected_channel--; app.selected_channel--;
} }
app.selected_param = 0; app.selected_param = 0;
stateManager.save(app); stateManager.markDirty();
app.refresh_screen = true; app.refresh_screen = true;
} }
@ -255,6 +262,9 @@ void editMainParameter(int val) {
gravity.clock.SetSource(app.selected_source); gravity.clock.SetSource(app.selected_source);
break; break;
} }
case PARAM_MAIN_ENCODER_DIR:
updateSelection(app.selected_sub_param, val, 2);
break;
case PARAM_MAIN_RESET_STATE: case PARAM_MAIN_RESET_STATE:
updateSelection(app.selected_sub_param, val, 2); updateSelection(app.selected_sub_param, val, 2);
break; break;
@ -303,6 +313,7 @@ void updateSelection(int& param, int change, int maxValue) {
void InitAppState(AppState& app) { void InitAppState(AppState& app) {
gravity.clock.SetTempo(app.tempo); gravity.clock.SetTempo(app.tempo);
gravity.clock.SetSource(app.selected_source); gravity.clock.SetSource(app.selected_source);
gravity.encoder.SetReverseDirection(app.encoder_reversed);
} }
Channel& GetSelectedChannel() { Channel& GetSelectedChannel() {
@ -384,6 +395,10 @@ void DisplayMainPage() {
break; break;
} }
break; break;
case PARAM_MAIN_ENCODER_DIR:
sprintf(mainText, "%s", "DIR");
subText = app.selected_sub_param == 0 ? "DEFAULT" : "REVERSED";
break;
case PARAM_MAIN_RESET_STATE: case PARAM_MAIN_RESET_STATE:
sprintf(mainText, "%s", "RST"); sprintf(mainText, "%s", "RST");
subText = app.selected_sub_param == 0 ? "RESET ALL" : "BACK"; subText = app.selected_sub_param == 0 ? "RESET ALL" : "BACK";
@ -394,7 +409,7 @@ void DisplayMainPage() {
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT); drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
// Draw Main Page menu items // Draw Main Page menu items
const char* menu_items[PARAM_MAIN_LAST] = {"TEMPO", "SOURCE", "RESET"}; const char* menu_items[PARAM_MAIN_LAST] = {"TEMPO", "SOURCE", "ENCODER DIR", "RESET"};
drawMenuItems(menu_items, PARAM_MAIN_LAST); drawMenuItems(menu_items, PARAM_MAIN_LAST);
} }

View File

@ -8,6 +8,7 @@
// Global state for settings and app behavior. // Global state for settings and app behavior.
struct AppState { struct AppState {
int tempo = Clock::DEFAULT_TEMPO; int tempo = Clock::DEFAULT_TEMPO;
bool encoder_reversed = false;
bool refresh_screen = true; bool refresh_screen = true;
bool editing_param = false; bool editing_param = false;
int selected_param = 0; int selected_param = 0;

View File

@ -4,13 +4,16 @@
#include "app_state.h" #include "app_state.h"
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
bool StateManager::initialize(AppState& app) { bool StateManager::initialize(AppState& app) {
if (isDataValid()) { if (isDataValid()) {
EepromData load_data; static EepromData load_data;
EEPROM.get(sizeof(Metadata), load_data); EEPROM.get(sizeof(Metadata), load_data);
// Restore main app state // Restore main app state
app.tempo = load_data.tempo; app.tempo = load_data.tempo;
app.encoder_reversed = load_data.encoder_reversed;
app.selected_param = load_data.selected_param; app.selected_param = load_data.selected_param;
app.selected_channel = load_data.selected_channel; app.selected_channel = load_data.selected_channel;
app.selected_source = static_cast<Clock::Source>(load_data.selected_source); app.selected_source = static_cast<Clock::Source>(load_data.selected_source);
@ -30,41 +33,21 @@ bool StateManager::initialize(AppState& app) {
return true; return true;
} else { } else {
writeMetadata(); reset(app);
save(app); // Save the initial default state
return false; return false;
} }
} }
void StateManager::save(const AppState& app) { void StateManager::save(const AppState& app) {
EepromData save_data; // Ensure interrupts do not cause corrupt data writes.
noInterrupts();
// Populate main app state _save_worker(app);
save_data.tempo = app.tempo; interrupts();
save_data.selected_param = app.selected_param;
save_data.selected_channel = app.selected_channel;
save_data.selected_source = static_cast<byte>(app.selected_source);
// Loop through and populate each channel's state
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
const auto& ch = app.channel[i];
auto& saved_ch_state = save_data.channel_data[i];
// Use the getters with 'withCvMod = false' to get the base values
saved_ch_state.base_clock_mod_index = ch.getClockModIndex(false);
saved_ch_state.base_probability = ch.getProbability(false);
saved_ch_state.base_duty_cycle = ch.getDutyCycle(false);
saved_ch_state.base_offset = ch.getOffset(false);
saved_ch_state.cv_source = static_cast<byte>(ch.getCvSource());
saved_ch_state.cv_destination = static_cast<byte>(ch.getCvDestination());
}
// Write the entire state struct to EEPROM
EEPROM.put(sizeof(Metadata), save_data);
} }
void StateManager::reset(AppState& app) { void StateManager::reset(AppState& app) {
app.tempo = Clock::DEFAULT_TEMPO; app.tempo = Clock::DEFAULT_TEMPO;
app.encoder_reversed = false;
app.selected_param = 0; app.selected_param = 0;
app.selected_channel = 0; app.selected_channel = 0;
app.selected_source = Clock::SOURCE_INTERNAL; app.selected_source = Clock::SOURCE_INTERNAL;
@ -73,8 +56,25 @@ void StateManager::reset(AppState& app) {
app.channel[i].Init(); app.channel[i].Init();
} }
writeMetadata(); noInterrupts();
_metadata_worker(); // Write the new metadata
_save_worker(app); // Write the new (default) app state
interrupts();
_isDirty = false;
}
void StateManager::update(const AppState& app) {
// Check if a save is pending and if enough time has passed.
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
save(app); save(app);
_isDirty = false; // Clear the flag, we are now "clean".
}
}
void StateManager::markDirty() {
_isDirty = true;
_lastChangeTime = millis();
} }
bool StateManager::isDataValid() { bool StateManager::isDataValid() {
@ -85,9 +85,35 @@ bool StateManager::isDataValid() {
return nameMatch && versionMatch; return nameMatch && versionMatch;
} }
void StateManager::writeMetadata() { void StateManager::_save_worker(const AppState& app) {
Metadata save_meta; static EepromData save_data;
strcpy(save_meta.sketchName, CURRENT_SKETCH_NAME);
save_meta.version = CURRENT_SKETCH_VERSION; // Populate main app state
EEPROM.put(0, save_meta); 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<byte>(app.selected_source);
// Loop through and populate each channel's state
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
const auto& ch = app.channel[i];
auto& save_ch = save_data.channel_data[i];
// Use the getters with 'withCvMod = false' to get the base values
save_ch.base_clock_mod_index = ch.getClockModIndex(false);
save_ch.base_probability = ch.getProbability(false);
save_ch.base_duty_cycle = ch.getDutyCycle(false);
save_ch.base_offset = ch.getOffset(false);
save_ch.cv_source = static_cast<byte>(ch.getCvSource());
save_ch.cv_destination = static_cast<byte>(ch.getCvDestination());
}
EEPROM.put(sizeof(Metadata), save_data);
}
void StateManager::_metadata_worker() {
Metadata currentMeta;
strcpy(currentMeta.sketchName, CURRENT_SKETCH_NAME);
currentMeta.version = CURRENT_SKETCH_VERSION;
EEPROM.put(0, currentMeta);
} }

View File

@ -16,15 +16,20 @@ const float CURRENT_SKETCH_VERSION = 0.2f;
*/ */
class StateManager { class StateManager {
public: public:
StateManager();
bool initialize(AppState& app); bool initialize(AppState& app);
void save(const AppState& app);
void reset(AppState& app); void reset(AppState& app);
// Call from main loop, check if state has changed and needs to be saved.
void update(const AppState& app);
// Indicate that state has changed and we should save.
void markDirty();
private: private:
// This struct holds the data that identifies the firmware version. // This struct holds the data that identifies the firmware version.
struct Metadata { struct Metadata {
char sketchName[16]; char sketchName[16];
float version; byte version;
}; };
struct ChannelState { struct ChannelState {
byte base_clock_mod_index; byte base_clock_mod_index;
@ -37,14 +42,23 @@ class StateManager {
// This struct holds all the parameters we want to save. // This struct holds all the parameters we want to save.
struct EepromData { struct EepromData {
int tempo; int tempo;
bool encoder_reversed;
byte selected_param; byte selected_param;
byte selected_channel; byte selected_channel;
byte selected_source; byte selected_source;
ChannelState channel_data[Gravity::OUTPUT_COUNT]; ChannelState channel_data[Gravity::OUTPUT_COUNT];
}; };
void save(const AppState& app);
bool isDataValid(); bool isDataValid();
void writeMetadata(); void _save_worker(const AppState& app);
void _metadata_worker();
bool _isDirty;
unsigned long _lastChangeTime;
static const unsigned long SAVE_DELAY_MS = 2000;
}; };
#endif // SAVE_STATE_H #endif // SAVE_STATE_H

View File

@ -11,6 +11,9 @@
#include "gravity.h" #include "gravity.h"
// Initialize the static pointer for the EncoderDir class to null.
EncoderDir* EncoderDir::_instance = nullptr;
void Gravity::Init() { void Gravity::Init() {
initClock(); initClock();
initInputs(); initInputs();
@ -68,18 +71,13 @@ void Gravity::Process() {
} }
} }
void ReadEncoder() {
gravity.encoder.UpdateEncoder();
}
// Define Encoder pin ISR.
// Pin Change Interrupt on Port C (D17/A3).
ISR(PCINT2_vect) {
ReadEncoder();
};
// Pin Change Interrupt on Port D (D4). // Pin Change Interrupt on Port D (D4).
ISR(PCINT2_vect) {
EncoderDir::isr();
};
// Pin Change Interrupt on Port C (D17/A3).
ISR(PCINT1_vect) { ISR(PCINT1_vect) {
ReadEncoder(); EncoderDir::isr();
}; };
// Singleton // Singleton

View File

@ -29,7 +29,7 @@ class Gravity {
// Polling check for state change of inputs and outputs. // Polling check for state change of inputs and outputs.
void Process(); void Process();
U8G2_SSD1306_128X64_NONAME_2_HW_I2C display; // OLED display object. U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object.
Clock clock; // Clock source wrapper. Clock clock; // Clock source wrapper.
DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object.
EncoderDir encoder; // Rotary encoder with button instance EncoderDir encoder; // Rotary encoder with button instance