diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino new file mode 100644 index 0000000..85dac98 --- /dev/null +++ b/RP2040_Tracker.ino @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include + +// --- HARDWARE CONFIGURATION --- +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 + +// Pin Definitions for Raspberry Pi Pico (RP2040) +#define PIN_SDA 4 +#define PIN_SCL 5 + +#define ENC_CLK 12 +#define ENC_DT 13 +#define ENC_SW 14 + +// NeoPixel Pin (any GPIO is fine, I've chosen 16) +#define PIN_NEOPIXEL 16 +#define NUM_PIXELS 64 // For 8x8 WS2812B matrix + +// --- TRACKER DATA --- +#define NUM_STEPS 16 + +struct Step { + int8_t note; // MIDI Note (0-127), -1 for OFF +}; + +Step sequence[NUM_STEPS]; + +// --- STATE --- +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800); + +int currentStep = 0; +bool isEditing = false; +int scrollOffset = 0; + +// Encoder State +volatile int encoderDelta = 0; +static uint8_t prevNextCode = 0; +static uint16_t store = 0; + +// Button State +bool lastButtonState = HIGH; +unsigned long lastDebounceTime = 0; + +// --- 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(ENC_DT)) prevNextCode |= 0x02; + if (digitalRead(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 setup() { + Serial.begin(115200); + Serial.println(F("Starting.")); + + // 1. Setup Encoder + pinMode(ENC_CLK, INPUT_PULLUP); + pinMode(ENC_DT, INPUT_PULLUP); + pinMode(ENC_SW, INPUT_PULLUP); + + attachInterrupt(digitalPinToInterrupt(ENC_CLK), readEncoder, CHANGE); + attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE); + + // 2. Setup Display + Wire.setSDA(PIN_SDA); + Wire.setSCL(PIN_SCL); + Wire.begin(); + + // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally + if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { + Serial.println(F("SSD1306 allocation failed")); + for(;;); // Don't proceed, loop forever + } + + // 3. Setup NeoPixel Matrix + pixels.begin(); + pixels.setBrightness(40); // Set brightness to a medium-low value (0-255) + pixels.clear(); + pixels.show(); + + // 4. Init Sequence + for(int i=0; i 127) newNote = 127; + sequence[currentStep].note = newNote; + } else { + // Move Cursor + currentStep += (delta > 0 ? 1 : -1); + if (currentStep < 0) currentStep = NUM_STEPS - 1; + if (currentStep >= NUM_STEPS) currentStep = 0; + + // Adjust Scroll to keep cursor in view + if (currentStep < scrollOffset) scrollOffset = currentStep; + if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5; + } + } + + // Handle Button + int reading = digitalRead(ENC_SW); + if (reading != lastButtonState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (reading == LOW) { // Button Pressed + // Wait for release to toggle mode + while(digitalRead(ENC_SW) == LOW); + isEditing = !isEditing; + } + } + lastButtonState = reading; +} + +void drawUI() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + // Header + display.print(F("TRACKER ")); + display.print(isEditing ? F("[EDIT]") : F("[NAV]")); + display.println(); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + + // Steps + int y = 10; + for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) { + + // Draw Cursor + if (i == currentStep) { + display.fillRect(0, y, 128, 8, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text + } else { + display.setTextColor(SSD1306_WHITE); + } + + display.setCursor(2, y); + + // Step Number + if (i < 10) display.print(F("0")); + display.print(i); + display.print(F(" | ")); + + // Note Value + int n = sequence[i].note; + if (n == -1) { + display.print(F("---")); + } else { + // Basic Note to String conversion + const char* noteNames[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"}; + display.print(noteNames[n % 12]); + display.print(n / 12 - 1); // Octave + } + + y += 9; + } + + display.display(); +} + +// Helper to convert X,Y to pixel index for an 8x8 matrix. +// Assumes row-major wiring (NOT serpentine). +// If your matrix is wired differently, you'll need to change this function. +int getPixelIndex(int x, int y) { + return y * 8 + x; +} + +void updateLeds() { + pixels.clear(); // Clear buffer + + for (int s = 0; s < NUM_STEPS; s++) { + int blockX = (s % 4) * 2; + int blockY = (s / 4) * 2; + + uint32_t color; + + if (s == currentStep) { + if (isEditing) { + color = pixels.Color(50, 0, 0); // Dim Red for editing + } else { + color = pixels.Color(40, 40, 40); // Dim White for current step + } + } else { + if (sequence[s].note != -1) { + color = pixels.Color(0, 0, 50); // Dim Blue for step with note + } else { + color = 0; // Off + } + } + + // Set the 4 pixels for the 2x2 block + pixels.setPixelColor(getPixelIndex(blockX, blockY), color); + pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), color); + pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), color); + pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), color); + } + + pixels.show(); +} + +void loop() { + handleInput(); + drawUI(); + updateLeds(); + delay(10); // Small delay to prevent screen tearing/excessive refresh +} \ No newline at end of file