#include #include "UIThread.h" #include "SharedState.h" #include #include #include #include #include "synth_engine.h" #include extern SynthEngine* globalSynth; #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C // I2C Pins (GP4/GP5) #define PIN_SDA 4 #define PIN_SCL 5 // Encoder Pins #define PIN_ENC_CLK 12 #define PIN_ENC_DT 13 #define PIN_ENC_SW 14 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); volatile int8_t encoderDelta = 0; static uint8_t prevNextCode = 0; static uint16_t store = 0; // Button state static int lastButtonReading = HIGH; static int currentButtonState = HIGH; static unsigned long lastDebounceTime = 0; static bool buttonClick = false; void handleInput(); void drawUI(); // --- ENCODER INTERRUPT --- // Robust Rotary Encoder reading void readEncoder() { static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0}; prevNextCode <<= 2; if (digitalRead(PIN_ENC_DT)) prevNextCode |= 0x02; if (digitalRead(PIN_ENC_CLK)) prevNextCode |= 0x01; prevNextCode &= 0x0f; // If valid state if (rot_enc_table[prevNextCode]) { store <<= 4; store |= prevNextCode; if ((store & 0xff) == 0x2b) encoderDelta--; if ((store & 0xff) == 0x17) encoderDelta++; } } void saveGridToEEPROM() { if (!globalSynth) return; uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; globalSynth->exportGrid(buf); EEPROM.write(0, 'N'); EEPROM.write(1, 'S'); for (size_t i = 0; i < sizeof(buf); i++) { EEPROM.write(2 + i, buf[i]); } EEPROM.commit(); } void loadGridFromEEPROM() { if (!globalSynth) return; if (EEPROM.read(0) == 'N' && EEPROM.read(1) == 'S') { uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; for (size_t i = 0; i < sizeof(buf); i++) { buf[i] = EEPROM.read(2 + i); } globalSynth->importGrid(buf); } else { globalSynth->loadPreset(1); // Default to preset 1 } } void setupUI() { Wire.setSDA(PIN_SDA); Wire.setSCL(PIN_SCL); Wire.begin(); pinMode(PIN_ENC_CLK, INPUT_PULLUP); pinMode(PIN_ENC_DT, INPUT_PULLUP); pinMode(PIN_ENC_SW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_ENC_CLK), readEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(PIN_ENC_DT), readEncoder, CHANGE); if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); for(;;); } display.clearDisplay(); display.display(); // Initialize EEPROM EEPROM.begin(512); // Check for safety clear (Button held on startup) if (digitalRead(PIN_ENC_SW) == LOW) { display.setCursor(0, 0); display.setTextColor(SSD1306_WHITE); display.println(F("CLEARING DATA...")); display.display(); EEPROM.write(0, 0); // Invalidate magic EEPROM.commit(); delay(1000); } loadGridFromEEPROM(); } void handleInput() { // Handle Encoder Rotation int rotation = 0; noInterrupts(); rotation = encoderDelta; encoderDelta = 0; interrupts(); if (rotation != 0) { switch (currentState) { case UI_MENU: menuSelection += rotation; while (menuSelection < 0) menuSelection += NUM_MENU_ITEMS; menuSelection %= NUM_MENU_ITEMS; break; case UI_EDIT_SCALE_TYPE: currentScaleIndex += rotation; while (currentScaleIndex < 0) currentScaleIndex += NUM_SCALES; currentScaleIndex %= NUM_SCALES; break; case UI_EDIT_SCALE_KEY: currentKeyIndex += rotation; while (currentKeyIndex < 0) currentKeyIndex += NUM_KEYS; currentKeyIndex %= NUM_KEYS; break; case UI_EDIT_WAVETABLE: currentWavetableIndex += rotation; while (currentWavetableIndex < 0) currentWavetableIndex += NUM_WAVETABLES; currentWavetableIndex %= NUM_WAVETABLES; break; } } // Handle Button Click int reading = digitalRead(PIN_ENC_SW); if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > 50) { if (reading != currentButtonState) { currentButtonState = reading; if (currentButtonState == LOW) { buttonClick = true; } } } lastButtonReading = reading; if (buttonClick) { buttonClick = false; if (currentState == UI_MENU) { currentState = MENU_ITEMS[menuSelection].editState; } else { currentState = UI_MENU; } } } void drawUI() { display.clearDisplay(); if (globalSynth) { // Copy grid state to local buffer to minimize lock time struct MiniCell { uint8_t type; uint8_t rotation; float value; }; MiniCell gridCopy[SynthEngine::GRID_W][SynthEngine::GRID_H]; { SynthLockGuard lock(globalSynth->gridMutex); for(int x=0; xgrid[x][y].type; gridCopy[x][y].rotation = (uint8_t)globalSynth->grid[x][y].rotation; gridCopy[x][y].value = globalSynth->grid[x][y].value; } } } int cellW = 10; int cellH = 5; int marginX = (SCREEN_WIDTH - (SynthEngine::GRID_W * cellW)) / 2; int marginY = (SCREEN_HEIGHT - (SynthEngine::GRID_H * cellH)) / 2; for(int x=0; x= SynthEngine::GridCell::FIXED_OSCILLATOR && type <= SynthEngine::GridCell::GATE_INPUT) { // Sources: Filled rect display.fillRect(cx - 1, cy - 1, 3, 3, SSD1306_WHITE); } else if (type != SynthEngine::GridCell::WIRE) { // Processors: Hollow rect display.drawRect(cx - 1, cy - 1, 3, 3, SSD1306_WHITE); } } } } } display.display(); } void checkSerial() { static int state = 0; // 0: Header, 1: Data static int headerIdx = 0; static const char* header = "NSGRID"; static int loadHeaderIdx = 0; static const char* loadHeader = "NSLOAD"; static uint8_t buffer[SynthEngine::SERIALIZED_GRID_SIZE]; static int bufferIdx = 0; while (Serial.available()) { uint8_t b = Serial.read(); if (state == 0) { if (b == header[headerIdx]) { headerIdx++; if (headerIdx == 6) { state = 1; bufferIdx = 0; headerIdx = 0; loadHeaderIdx = 0; } } else { headerIdx = 0; if (b == 'N') headerIdx = 1; } if (state == 0) { if (b == loadHeader[loadHeaderIdx]) { loadHeaderIdx++; if (loadHeaderIdx == 6) { if (globalSynth) { uint8_t buf[SynthEngine::SERIALIZED_GRID_SIZE]; globalSynth->exportGrid(buf); Serial.write("NSGRID", 6); Serial.write(buf, sizeof(buf)); Serial.flush(); } loadHeaderIdx = 0; headerIdx = 0; } } else { loadHeaderIdx = 0; if (b == 'N') loadHeaderIdx = 1; } } } else if (state == 1) { buffer[bufferIdx++] = b; if (bufferIdx == SynthEngine::SERIALIZED_GRID_SIZE) { if (globalSynth) { globalSynth->importGrid(buffer); saveGridToEEPROM(); Serial.println(F("OK: Grid Received")); } state = 0; bufferIdx = 0; } } } } void loopUI() { handleInput(); checkSerial(); drawUI(); delay(20); // Prevent excessive screen refresh }