#include #include #include // --- Raw RP2040 GPIO Pins for Screen --- #define TFT_RESET 26 #define TFT_CS 27 #define TFT_DC 28 #define HW_MOSI 3 #define HW_SCK 2 // --- Single Push Button Definition (GPIO 4 / D9) --- #define BTN_PIN 4 // --- Raw RP2040 GPIO Pins for Multiplexer --- #define MUX_SIG 29 #define MUX_S0 6 #define MUX_S1 5 #define MUX_S2 0 #define MUX_S3 1 #define NUM_CHANNELS 16 int channelValues[NUM_CHANNELS]; int mappedValues[NUM_CHANNELS]; int lastMappedValues[NUM_CHANNELS]; float mmValues[NUM_CHANNELS]; float lastMmValues[NUM_CHANNELS]; #define ST7735_DARKGREY 0x4208 // ========================================================================= // --- PRE-AMPLIFIER WINDOW DESIGN --- // Maps your exact 3 to 290 physical Velostat output range across all 16 pins // ========================================================================= const float AMP_FLOOR = 3.0; const float AMP_CEIL = 290.0; // ========================================================================= struct Scan { int y[NUM_CHANNELS]; }; Scan library[5]; int savedTotal = 0; int currentIdx = 0; int currentState = 0; // 0=Live, 1=Library, 2=ActionMenu int lastState = -1; int menuOption = 0; const float TINE_PITCH = 3.0; const float MAX_TRAVEL = 70.0; // Scaled to your 70mm physical travel track Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RESET); int currentX[NUM_CHANNELS]; // --- Function Declarations --- void handleButton(); void executeSingleClick(); void executeLongPress(); void drawHeader(String title); void refreshLibraryHeader(); void drawFooter(String label); void drawStaticGrid(); void updateContour(int* yValues); void updateNumericMatrix(bool forceRedraw); void drawActionMenu(bool drawAll); void executeMenuAction(); void drawProgressBar(unsigned long time); void flashMessage(String msg); void setup() { Serial.begin(115200); SPI.setMOSI(HW_MOSI); SPI.setSCK(HW_SCK); SPI.begin(); pinMode(MUX_S0, OUTPUT); pinMode(MUX_S1, OUTPUT); pinMode(MUX_S2, OUTPUT); pinMode(MUX_S3, OUTPUT); // High-impedance reading setup for multiplexer bus line pinMode(MUX_SIG, INPUT_PULLDOWN); // Configure single authorized operational menu button pinMode(BTN_PIN, INPUT_PULLUP); // CRITICAL FAULT PROTECTION: Explicitly force any other potential button GPIO pins // into safe, high-impedance INPUT floating states. This isolates and totally ignores // the electrical short-circuit bleeding on your board. pinMode(7, INPUT); // Guarding adjacent pins against cross-talk shorts pinMode(8, INPUT); tft.initR(INITR_18BLACKTAB); tft.setRotation(3); // Inverted landscape view tft.fillScreen(ST7735_BLACK); int fixedWidth = 160; int xStep = (fixedWidth - 12) / (NUM_CHANNELS - 1); for (int i = 0; i < NUM_CHANNELS; i++) { currentX[i] = 6 + (i * xStep); mappedValues[i] = 65; lastMappedValues[i] = 65; mmValues[i] = 0.0; lastMmValues[i] = -1.0; } } // --- Patched Address Line Switching --- inline int readMux(int channel) { digitalWrite(MUX_S0, bitRead(channel, 0)); digitalWrite(MUX_S1, bitRead(channel, 1)); digitalWrite(MUX_S2, bitRead(channel, 2)); digitalWrite(MUX_S3, bitRead(channel, 3)); // Settling delay allowing internal multiplexer hardware gates to open cleanly delayMicroseconds(30); return analogRead(MUX_SIG); } void loop() { bool stateChanged = (currentState != lastState); if (stateChanged) { tft.fillScreen(ST7735_BLACK); if (currentState == 0) { drawHeader("PROFILER: 16-PIN ACTIVE LIVE"); drawFooter("1x: SAVE PROFILE | HOLD: MENU"); drawStaticGrid(); updateNumericMatrix(true); } else if (currentState == 1) { refreshLibraryHeader(); drawFooter("1x: NEXT | 2x: LIVE | H: MENU"); drawStaticGrid(); updateNumericMatrix(true); } else if (currentState == 2) { drawActionMenu(true); } lastState = currentState; } // Live Processing Mode across all 16 channels sequentially if (currentState == 0) { for (int i = 0; i < NUM_CHANNELS; i++) { int currentRaw = readMux(i); channelValues[i] = currentRaw; // Restrict incoming reading inside calibrated micro-window limits float rawFloat = (float)currentRaw; if (rawFloat < AMP_FLOOR) rawFloat = AMP_FLOOR; if (rawFloat > AMP_CEIL) rawFloat = AMP_CEIL; // Linear translation scalar float normalizedScalar = (rawFloat - AMP_FLOOR) / (AMP_CEIL - AMP_FLOOR); // Clean baseline drop guard to eliminate low-end jitter trails if (normalizedScalar < 0.04) { normalizedScalar = 0.0; } // Map scale conversion factor directly to viewport display rows (65 to 22) mappedValues[i] = 65 - (int)(normalizedScalar * (65.0 - 22.0)); } // Compute metric physical conversions from data array maps for (int i = 0; i < NUM_CHANNELS; i++) { mmValues[i] = map(mappedValues[i], 65, 22, 0 * 100, MAX_TRAVEL * 100) / 100.0; } updateContour(mappedValues); updateNumericMatrix(false); } else if (currentState == 1 && stateChanged) { updateContour(library[currentIdx].y); for(int i = 0; i < NUM_CHANNELS; i++) { mmValues[i] = map(library[currentIdx].y[i], 65, 22, 0 * 100, MAX_TRAVEL * 100) / 100.0; } updateNumericMatrix(true); } handleButton(); // Pacing clock step matching physical response limits of the capacitor array delay(15); } // --- Graphical Driver Utilities --- void drawHeader(String title) { tft.setCursor(6, 4); tft.setTextColor(ST7735_WHITE); tft.setTextSize(1); tft.print(title); tft.drawLine(0, 15, tft.width(), 15, ST7735_GREEN); } void refreshLibraryHeader() { tft.fillRect(0, 0, tft.width(), 14, ST7735_BLACK); String libTitle = "LIBRARY: SCAN " + String(currentIdx + 1) + " OF " + String(savedTotal); drawHeader(libTitle); } void drawFooter(String label) { tft.fillRect(0, tft.height() - 11, tft.width(), 11, ST7735_BLACK); tft.setCursor(6, tft.height() - 9); tft.setTextColor(ST7735_DARKGREY); tft.setTextSize(1); tft.print(label); } void drawStaticGrid() { for (int y = 25; y <= 65; y += 13) { for (int x = 6; x < tft.width() - 4; x += 8) tft.drawPixel(x, y, tft.color565(45, 45, 45)); } tft.drawLine(0, 72, tft.width(), 72, ST7735_BLUE); } void updateContour(int* yValues) { // 1. Wipe old slots clean using black columns to avoid ghosting for (int i = 0; i < NUM_CHANNELS; i++) { if (lastMappedValues[i] != yValues[i]) { tft.fillRect(currentX[i] - 3, 18, 7, 50, ST7735_BLACK); } } // 2. Erase vector traces from the historical frame for (int i = 0; i < NUM_CHANNELS - 1; i++) { tft.drawLine(currentX[i], lastMappedValues[i], currentX[i+1], lastMappedValues[i+1], ST7735_BLACK); } // 3. Re-dot background reference grid arrays for (int y = 25; y <= 65; y += 13) { for (int i = 0; i < NUM_CHANNELS; i++) { for (int px = currentX[i] - 4; px <= currentX[i] + 4; px += 8) { if (px > 0 && px < tft.width()) tft.drawPixel(px, y, tft.color565(45, 45, 45)); } } } // 4. Draw clean real-time connection lines for (int i = 0; i < NUM_CHANNELS - 1; i++) { tft.drawLine(currentX[i], yValues[i], currentX[i+1], yValues[i+1], ST7735_YELLOW); } // 5. Render active red node coordinates for (int i = 0; i < NUM_CHANNELS; i++) { tft.fillCircle(currentX[i], yValues[i], 2, ST7735_RED); lastMappedValues[i] = yValues[i]; } } void updateNumericMatrix(bool forceRedraw) { tft.setTextSize(1); for (int i = 0; i < NUM_CHANNELS; i++) { int row = (i < 8) ? 0 : 1; int col = i % 8; int xPos = 4 + (col * 20); int yPos = 78 + (row * 22); if (forceRedraw) { tft.setCursor(xPos, yPos); tft.setTextColor(ST7735_CYAN); if((i+1) < 10) tft.print("0"); tft.print(i+1); } if (abs(mmValues[i] - lastMmValues[i]) >= 0.1 || forceRedraw) { tft.fillRect(xPos, yPos + 9, 18, 8, ST7735_BLACK); tft.setCursor(xPos, yPos + 9); tft.setTextColor(ST7735_WHITE); tft.print((int)mmValues[i]); lastMmValues[i] = mmValues[i]; } } } // --- Authorized Single-Button Clock System --- void handleButton() { static unsigned long btnPressedTime = 0; static bool isPressed = false; int btnState = digitalRead(BTN_PIN); if (btnState == LOW && !isPressed) { isPressed = true; btnPressedTime = millis(); delay(10); } if (btnState == LOW && isPressed) { unsigned long holdDuration = millis() - btnPressedTime; if (holdDuration > 200) drawProgressBar(holdDuration); if (holdDuration > 800) { isPressed = false; executeLongPress(); while(digitalRead(BTN_PIN) == LOW); tft.fillRect(30, tft.height() / 2 - 8, tft.width() - 60, 16, ST7735_BLACK); if (currentState != 2) { drawStaticGrid(); updateNumericMatrix(true); } } } if (btnState == HIGH && isPressed) { unsigned long duration = millis() - btnPressedTime; isPressed = false; if (duration < 500) executeSingleClick(); tft.fillRect(30, tft.height() / 2 - 8, tft.width() - 60, 16, ST7735_BLACK); if (currentState != 2) { drawStaticGrid(); updateNumericMatrix(true); } delay(10); } } void executeSingleClick() { if (currentState == 0) { if (savedTotal < 5) { for (int i = 0; i < NUM_CHANNELS; i++) library[savedTotal].y[i] = mappedValues[i]; savedTotal++; flashMessage("SCAN CAPTURED"); lastState = -1; } else { flashMessage("LIBRARY FULL"); lastState = -1; } } else if (currentState == 1 && savedTotal > 0) { currentIdx = (currentIdx + 1) % savedTotal; refreshLibraryHeader(); tft.fillRect(0, 16, tft.width(), 55, ST7735_BLACK); drawStaticGrid(); updateContour(library[currentIdx].y); updateNumericMatrix(true); } else if (currentState == 2) { menuOption = (menuOption + 1) % 3; drawActionMenu(false); } } void executeLongPress() { if (currentState == 0) { if (savedTotal > 0) { currentState = 1; currentIdx = 0; } else { flashMessage("NO SCANS LOGGED"); lastState = -1; } } else if (currentState == 1) { // FIXED: Changed =-- to == currentState = 2; menuOption = 0; } else if (currentState == 2) { executeMenuAction(); } } void drawActionMenu(bool drawAll) { if (drawAll) { drawHeader("SYSTEM ACTION MENU"); drawFooter("Click: NEXT ITEM | Hold: SELECT"); } String options[] = {"[ CLEAR CHIP MEMORY ]", "[ STREAM TO PC BRDG ]", "[ ESCAPE MENU ]"}; for(int i = 0; i < 3; i++) { tft.setCursor(35, 30 + (i * 14)); tft.fillRect(20, 28 + (i * 14), 12, 10, ST7735_BLACK); if (menuOption == i) { tft.setTextColor(ST7735_YELLOW); tft.print("> "); } else { tft.print(" "); } tft.setTextColor(menuOption == i ? ST7735_YELLOW : ST7735_WHITE); tft.print(options[i]); } } void executeMenuAction() { if (menuOption == 0) { savedTotal = 0; currentIdx = 0; flashMessage("MEMORY WIPED"); currentState = 0; } else if (menuOption == 1) { Serial.println("START_DATA"); delay(50); for(int i = 0; i < savedTotal; i++) { Serial.print("SCAN:"); Serial.print(i); for(int j = 0; j < NUM_CHANNELS; j++) { float xPos = j * TINE_PITCH; float mmY = map(library[i].y[j], 65, 22, 0 * 100, MAX_TRAVEL * 100) / 100.0; Serial.print(","); Serial.print(xPos, 2); Serial.print(","); Serial.print(mmY, 2); } Serial.println(); delay(10); } Serial.println("END_DATA"); flashMessage("DATA STREAM COMPLETE"); currentState = 1; } else if (menuOption == 2) { currentState = 1; } lastState = -1; } void drawProgressBar(unsigned long time) { int width = map(time, 0, 800, 0, tft.width() - 60); if (width > tft.width() - 60) width = tft.width() - 60; tft.drawRect(30, tft.height() / 2 - 6, tft.width() - 60, 12, ST7735_WHITE); tft.fillRect(32, tft.height() / 2 - 4, width, 8, ST7735_BLUE); } void flashMessage(String msg) { tft.fillScreen(ST7735_BLACK); tft.setTextColor(ST7735_WHITE); tft.setTextSize(1); tft.setCursor((tft.width() - (msg.length() * 6)) / 2, tft.height() / 2 - 4); tft.print(msg); delay(800); }