// ESP32 core 3.x defines these names in pins_arduino.h. // pin_config.h uses the same names for board constants. #ifdef SDMMC_CLK #undef SDMMC_CLK #endif #ifdef SDMMC_CMD #undef SDMMC_CMD #endif #include "pin_config.h" #include #include #include #include #include "Arduino_DriveBus_Library.h" #include "Arduino_GFX_Library.h" #include "SensorQMI8658.hpp" #include "SensorPCF85063.hpp" // ============================================================ // POWER / BATTERY // ============================================================ #include "XPowersLib.h" XPowersPMU power; bool powerReady = false; bool batteryConnected = false; bool batteryCharging = false; int batteryPercent = 0; int batteryVoltage = 0; int vbusVoltage = 0; int systemVoltage = 0; unsigned long lastBatteryUpdate = 0; const unsigned long BATTERY_UPDATE_INTERVAL = 10000; // ============================================================ // DIAGNOSTICS // ============================================================ unsigned long sensorSampleCount = 0; unsigned long lastHeartbeat = 0; const unsigned long HEARTBEAT_INTERVAL = 10000; // ============================================================ // AUDIO // ============================================================ #include "ESP_I2S.h" #include "esp_check.h" #include "es8311.h" I2SClass i2s; #define AUDIO_SAMPLE_RATE 16000 #define AUDIO_VOLUME 85 #define AUDIO_I2C_NUM 0 #define AUDIO_SCLK 41 #define AUDIO_LRCK 45 #define AUDIO_DSDIN 40 #define AUDIO_ASDOUT 42 #define AUDIO_MCLK 16 #define AUDIO_PA_CTRL 46 // ============================================================ // DISPLAY // ============================================================ Arduino_DataBus *bus = new Arduino_ESP32QSPI( LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3 ); Arduino_GFX *gfx = new Arduino_CO5300( bus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 22, 0, 0, 0 ); // ============================================================ // LCD POWER RAIL (DSI_PWR_EN) // ============================================================ #ifndef LCD_EN #define LCD_EN 13 #endif const int LCD_EN_PIN = LCD_EN; const int LCD_CS_PIN = LCD_CS; // ============================================================ // SCREEN ON / OFF (HARDWARE POWER GATE) // ============================================================ bool screenIsOn = true; unsigned long screenOnTime = 0; // Prevent accidental redraws while the panel is powered down. bool screenWakeRequested = false; const unsigned long HOME_SCREEN_TIMEOUT = 15000; const unsigned long IM_SCREEN_TIMEOUT = 15000; const unsigned long GAME_SCREEN_TIMEOUT = 15000; const unsigned long CALC_SCREEN_TIMEOUT = 15000; // ============================================================ // TOUCH // ============================================================ std::shared_ptr IIC_Bus = std::make_shared(IIC_SDA, IIC_SCL, &Wire); void drawHomeScreen(); void processHomeScreenTouch(int x, int y); void startCalc(); void drawCalc(); void processCalcTouch(int x, int y); void screenOn(); void screenOff(); void screenHardwareOn(); void screenHardwareOff(); void screenWake(); void screenKeepOn(); void screenAutoOffCheck(); void drawBatteryIcon(); void Arduino_IIC_Touch_Interrupt(void); std::unique_ptr FT3168( new Arduino_FT3x68( IIC_Bus, FT3168_DEVICE_ADDRESS, DRIVEBUS_DEFAULT_VALUE, TP_INT, Arduino_IIC_Touch_Interrupt ) ); void Arduino_IIC_Touch_Interrupt(void) { FT3168->IIC_Interrupt_Flag = true; } // ============================================================ // QMI8658 // ============================================================ SensorQMI8658 qmi; IMUdata acc; // ============================================================ // RTC // ============================================================ SensorPCF85063 rtc; // ============================================================ // PHYSICAL BUTTON // ============================================================ #define RESET_BUTTON_PIN 0 #define BUTTON_PRESSED LOW const unsigned long BUTTON_DEBOUNCE = 50; const unsigned long BUTTON_DOUBLE_PRESS_WINDOW = 700; bool lastButtonReading = HIGH; bool buttonState = HIGH; unsigned long lastButtonChange = 0; unsigned long firstButtonPressTime = 0; bool waitingForButtonSecondPress = false; // ============================================================ // COLORS // ============================================================ #define COLOR_BG RGB565_BLACK #define COLOR_GREEN RGB565_GREEN #define COLOR_WHITE RGB565_WHITE #define COLOR_CYAN RGB565_CYAN #define COLOR_YELLOW RGB565_YELLOW #define COLOR_RED RGB565_RED #define COLOR_ORANGE RGB565_ORANGE #define COLOR_BLUE RGB565_BLUE #define COLOR_PURPLE RGB565_MAGENTA // ============================================================ // SEIZURE DETECTION // ============================================================ const unsigned long SENSOR_SAMPLE_INTERVAL = 1; const float MOTION_DELTA_THRESHOLD = 0.12f; const int MOTION_COUNT_THRESHOLD = 35; const unsigned long SEIZURE_WINDOW = 1000; const unsigned long SEIZURE_CONFIRM_TIME = 700; const unsigned long ALERT_DURATION = 10000; const unsigned long ALERT_COOLDOWN = 5000; // ============================================================ // SEIZURE STATE // ============================================================ enum MonitorState { MONITORING, POSSIBLE_SEIZURE, ALERT, COOLDOWN }; MonitorState monitorState = MONITORING; unsigned long possibleStart = 0; unsigned long alertStart = 0; unsigned long cooldownStart = 0; // ============================================================ // MOVEMENT ANALYSIS // ============================================================ float total_acc = 0.0f; float previous_acc = 1.0f; float motion_delta = 0.0f; unsigned long motionWindowStart = 0; int motionCount = 0; // ============================================================ // DISPLAY TIMING // ============================================================ unsigned long lastDisplay = 0; unsigned long lastClockDisplay = 0; unsigned long lastSerial = 0; unsigned long lastAlertScreen = 0; int lastDisplayedHour = -1; int lastDisplayedMinute = -1; // ============================================================ // TOUCH / GAME // ============================================================ enum GameState { GAME_NONE, GAME_QUESTION, GAME_REVEAL, GAME_RESULT }; GameState gameState = GAME_NONE; bool gameActive = false; unsigned long lastTouchTime = 0; unsigned long firstTapTime = 0; bool waitingForSecondTap = false; const unsigned long DOUBLE_TAP_WINDOW = 600; const unsigned long TOUCH_DEBOUNCE = 100; int touchX = 0; int touchY = 0; int score = 0; int rounds = 0; int selectedColor = 0; // ============================================================ // GAME COLORS // ============================================================ struct GameColor { const char *name; uint16_t color; }; GameColor gameColors[] = { { "RED", RGB565_RED }, { "GREEN", RGB565_GREEN }, { "BLUE", RGB565_BLUE }, { "YELLOW", RGB565_YELLOW }, { "PURPLE", RGB565_MAGENTA }, { "CYAN", RGB565_CYAN } }; const int GAME_COLOR_COUNT = sizeof(gameColors) / sizeof(gameColors[0]); // ============================================================ // HOME SCREEN // ============================================================ #define COLOR_SOFT_PINK 0xfe19 #define COLOR_PINK_DARK 0xD828 #define COLOR_ICON_GREEN 0x16dd // this is actually a baby blue #define COLOR_ICON_CYAN 0xe864 // this is actually red #define COLOR_ICON_BLUE 0x0CAE enum ScreenState { SCREEN_HOME, SCREEN_MONITORING, SCREEN_GAME, SCREEN_CALC }; ScreenState currentScreen = SCREEN_HOME; const int ICON_SIZE = 100; const int ICON_GAP = 25; const int ICON_Y = 140; const int ICON_LABEL_Y = 250; int iconX[3] = { 25, 155, 285 }; // ============================================================ // CALCULATOR // ============================================================ bool calcActive = false; float calcDisplay = 0.0f; float calcFirstOperand = 0.0f; char calcOperator = 0; bool calcNewInput = true; bool calcError = false; bool calcHasDecimal = false; float calcDecimalPlace = 0.1f; const int CALC_BTN_SIZE = 72; const int CALC_BTN_GAP = 6; const int CALC_BTN_START_Y = 120; const int CALC_BTN_START_X = 52; const int CALC_COLS = 4; const int CALC_ROWS = 4; const int CALC_ROWS_TOTAL = 5; const char *calcButtons[5][4] = { {"7", "8", "9", "/"}, {"4", "5", "6", "*"}, {"1", "2", "3", "-"}, {"0", ".", "=", "+"}, {"C", "C", "C", "C"} }; // ============================================================ // AUDIO / ES8311 // ============================================================ es8311_handle_t es_handle = NULL; bool audioReady = false; bool sirenActive = false; TaskHandle_t sirenTaskHandle = NULL; // ============================================================ // FORWARD DECLARATIONS // ============================================================ void updateMonitoringValues(); void updateClockDisplay(); void drawMonitoringScreen(); void drawPossibleSeizureScreen(); void drawAlertScreen(); void drawCooldownScreen(); void triggerAlert(); void resetMonitor(); void startGame(); void newGameRound(); void exitGame(); void drawGameQuestion(); void drawGameReveal(); void processMonitoringTouch(int x, int y); void processGameTouch(int x, int y); void processTouch(); bool readTouch(int &x, int &y); void checkResetButton(); void updateSeizureDetection(); void printSensorData(); void printPowerDiagnostics(); void i2cScan(); void systemHeartbeat(); bool initPower(); void updateBatteryData(); bool initAudio(); void sirenTask(void *parameter); void startSiren(); void stopSiren(); void enablePowerADC(); void disablePowerADC(); // ============================================================ // SCREEN ON / OFF HELPERS // ============================================================ // // These functions only control logical drawing. // The actual AMOLED power rail control is below. // void screenOn() { if (!screenIsOn) screenHardwareOn(); } void screenOff() { screenHardwareOff(); } // ============================================================ // HOME SCREEN ICONS // ============================================================ // Shadow + card base, shared by all three home icons void iconCard(int x, uint16_t cardColor) { gfx->fillRoundRect(x + 3, ICON_Y + 4, ICON_SIZE, ICON_SIZE, 14, COLOR_SOFT_PINK); gfx->fillRoundRect(x, ICON_Y, ICON_SIZE, ICON_SIZE, 14, cardColor); } // Thick line (used for the ECG trace) void iconLine(int x0, int y0, int x1, int y1, int w, uint16_t color) { int steps = max(abs(x1 - x0), abs(y1 - y0)); if (steps <= 0) { gfx->fillRect(x0, y0, w, w, color); return; } for (int i = 0; i <= steps; i++) { int x = x0 + ((x1 - x0) * i) / steps; int y = y0 + ((y1 - y0) * i) / steps; gfx->fillRect(x, y, w, w, color); } } // --- Monitor icon: medical monitor + ECG pulse --- void drawMonitorIcon(int x) { iconCard(x, COLOR_ICON_GREEN); // Screen gfx->fillRoundRect(x + 12, ICON_Y + 12, 76, 60, 8, 0x0838); // Status LED gfx->fillCircle(x + 21, ICON_Y + 21, 3, COLOR_YELLOW); // ECG trace iconLine(x + 18, ICON_Y + 44, x + 30, ICON_Y + 44, 3, 0xEFFF); iconLine(x + 30, ICON_Y + 44, x + 36, ICON_Y + 30, 3, 0xEFFF); iconLine(x + 36, ICON_Y + 30, x + 44, ICON_Y + 56, 3, 0xEFFF); iconLine(x + 44, ICON_Y + 56, x + 50, ICON_Y + 28, 3, 0xEFFF); iconLine(x + 50, ICON_Y + 28, x + 58, ICON_Y + 44, 3, 0xEFFF); iconLine(x + 58, ICON_Y + 44, x + 80, ICON_Y + 44, 3, 0xEFFF); // Stand gfx->fillRect(x + 44, ICON_Y + 72, 12, 8, 0xEFFF); gfx->fillRect(x + 34, ICON_Y + 78, 32, 5, 0xEFFF); } // --- Color icon: 8-segment color wheel --- void drawColorIcon(int x) { iconCard(x, COLOR_ICON_CYAN); int cx = x + 50; int cy = ICON_Y + 50; int r = 34; int h = 24; // Eight pie sectors gfx->fillTriangle(cx, cy, cx, cy - r, cx + h, cy - h, COLOR_RED); gfx->fillTriangle(cx, cy, cx + h, cy - h, cx + r, cy, COLOR_ORANGE); gfx->fillTriangle(cx, cy, cx + r, cy, cx + h, cy + h, COLOR_YELLOW); gfx->fillTriangle(cx, cy, cx + h, cy + h, cx, cy + r, COLOR_GREEN); gfx->fillTriangle(cx, cy, cx, cy + r, cx - h, cy + h, COLOR_CYAN); gfx->fillTriangle(cx, cy, cx - h, cy + h, cx - r, cy, COLOR_BLUE); gfx->fillTriangle(cx, cy, cx - r, cy, cx - h, cy - h, COLOR_PURPLE); gfx->fillTriangle(cx, cy, cx - h, cy - h, cx, cy - r, 0xFDBB); // Center hub gfx->fillCircle(cx, cy, 12, 0xEFFF); gfx->fillCircle(cx, cy, 5, COLOR_ICON_CYAN); } // --- Calc icon: mini calculator with screen and keys --- void drawCalcIcon(int x) { iconCard(x, COLOR_ICON_BLUE); // LCD screen gfx->fillRoundRect(x + 14, ICON_Y + 14, 72, 26, 6, 0xEFFF); gfx->setTextColor(COLOR_ICON_BLUE); gfx->setTextSize(1); gfx->setCursor(x + 56, ICON_Y + 19); gfx->println("123"); // 3x3 key grid, orange = key for (int r = 0; r < 3; r++) { for (int c = 0; c < 3; c++) { int bx = x + 16 + c * 22; int by = ICON_Y + 48 + r * 16; uint16_t color; color = (r == 2 && c == 2) ? COLOR_ORANGE : 0xEFFF; gfx->fillRoundRect(bx, by, 16, 10, 3, color); } } } // ============================================================ // HOME SCREEN // ============================================================ void drawHomeScreen() { if(!screenIsOn) return; gfx->fillScreen(COLOR_SOFT_PINK); gfx->setTextColor(0x410F); gfx->setTextSize(2); gfx->setCursor(42, 30); gfx->println("Piper Hamilton 405-412-4500"); gfx->setTextColor(0x6510); gfx->setTextSize(1); gfx->setCursor(130, 60); gfx->println("Tap an app"); // Icons drawMonitorIcon(iconX[0]); drawColorIcon(iconX[1]); drawCalcIcon(iconX[2]); // Labels gfx->setTextColor(0x410F); gfx->setTextSize(1); gfx->setCursor(iconX[0] + 10, ICON_LABEL_Y); gfx->println("Monitor"); gfx->setCursor(iconX[1] + 5, ICON_LABEL_Y); gfx->println("Color"); gfx->setCursor(iconX[2] + 25, ICON_LABEL_Y); gfx->println("Calc"); // Battery gfx->setTextColor(0x410F); gfx->setTextSize(2); gfx->setCursor(160, 460); if (batteryConnected) { gfx->print(batteryPercent); gfx->print("%"); } else { gfx->println("No Battery"); } // Time RTC_DateTime dt = rtc.getDateTime(); char tstr[16]; int h = dt.getHour(); int m = dt.getMinute(); int dh = (h == 0) ? 12 : (h > 12 ? h - 12 : h); snprintf(tstr, sizeof(tstr), "%d:%02d %s", dh, m, h < 12 ? "AM" : "PM"); gfx->setTextColor(0x410F); gfx->setTextSize(2); gfx->setCursor(150, 420); gfx->println(tstr); currentScreen = SCREEN_HOME; } // ============================================================ // HOME SCREEN TOUCH // ============================================================ void processHomeScreenTouch(int x, int y) { for (int i = 0; i < 3; i++) { bool hit = x >= iconX[i] && x <= iconX[i] + ICON_SIZE && y >= ICON_Y && y <= ICON_Y + ICON_SIZE; if (hit) { Serial.print("Home icon tapped: "); Serial.println(i); switch (i) { case 0: currentScreen = SCREEN_MONITORING; drawMonitoringScreen(); break; case 1: currentScreen = SCREEN_GAME; startGame(); break; case 2: currentScreen = SCREEN_CALC; startCalc(); break; } return; } } } // ============================================================ // CALCULATOR // ============================================================ void startCalc() { calcActive = true; calcDisplay = 0.0f; calcFirstOperand = 0.0f; calcOperator = 0; calcNewInput = true; calcHasDecimal = false; calcError = false; drawCalc(); Serial.println(">>> CALC STARTED <<<"); } void drawCalc() { if(!screenIsOn) return; gfx->fillScreen(COLOR_SOFT_PINK); gfx->setTextColor(0x410F); gfx->setTextSize(2); gfx->setCursor(160, 20); gfx->println("Calc"); gfx->fillRoundRect(20, 70, 370, 45, 8, 0x0A0A); gfx->setTextColor(0xEFFF); gfx->setTextSize(3); gfx->setCursor(40, 80); if (calcError) gfx->println("ERROR"); else gfx->println(calcDisplay, 2); for (int r = 0; r < CALC_ROWS_TOTAL; r++) { for (int c = 0; c < CALC_COLS; c++) { int bx = CALC_BTN_START_X + c * (CALC_BTN_SIZE + CALC_BTN_GAP); int by = CALC_BTN_START_Y + r * (CALC_BTN_SIZE + CALC_BTN_GAP); uint16_t bgColor; if (c == 3) bgColor = 0x0CAE; else if (r == 4) bgColor = 0xD828; else bgColor = 0xFDBB; gfx->fillRoundRect(bx, by, CALC_BTN_SIZE, CALC_BTN_SIZE, 10, bgColor); gfx->setTextColor(0x410F); gfx->setTextSize(2); gfx->setCursor(bx + 25, by + 30); gfx->println(calcButtons[r][c]); } } gfx->setTextColor(0x6510); gfx->setTextSize(1); gfx->setCursor(110, 480); gfx->println("GPIO0 = Home"); currentScreen = SCREEN_CALC; } void processCalcTouch(int x, int y) { unsigned long now = millis(); // ------------------------------------------------------------ // TOUCH DEBOUNCE // ------------------------------------------------------------ if (now - lastTouchTime < TOUCH_DEBOUNCE) return; lastTouchTime = now; // ------------------------------------------------------------ // FIND THE BUTTON THAT WAS PRESSED // ------------------------------------------------------------ for (int r = 0; r < CALC_ROWS_TOTAL; r++) { for (int c = 0; c < CALC_COLS; c++) { int bx = CALC_BTN_START_X + c * (CALC_BTN_SIZE + CALC_BTN_GAP); int by = CALC_BTN_START_Y + r * (CALC_BTN_SIZE + CALC_BTN_GAP); bool hit = x >= bx && x <= bx + CALC_BTN_SIZE && y >= by && y <= by + CALC_BTN_SIZE; if (!hit) continue; const char *btn = calcButtons[r][c]; char key = btn[0]; // ==================================================== // ERROR STATE // ==================================================== // Once we have an error, only C will do anything. if (calcError && key != 'C') return; // ==================================================== // CLEAR // ==================================================== if (key == 'C') { calcDisplay = 0.0f; calcFirstOperand = 0.0f; calcOperator = 0; calcNewInput = true; calcHasDecimal = false; calcDecimalPlace = 0.1f; calcError = false; drawCalc(); return; } // ==================================================== // NUMBER BUTTON // ==================================================== if (key >= '0' && key <= '9') { int digit = key - '0'; // ------------------------------------------------ // We're starting a new number. // // Example: // // 7 * 8 // ^ // 8 starts a new number. // ------------------------------------------------ if (calcNewInput) { calcDisplay = (float)digit; calcNewInput = false; calcHasDecimal = false; calcDecimalPlace = 0.1f; } else { // ------------------------------------------------ // Normal whole-number entry. // Example: 1 -> 12 -> 123 // ------------------------------------------------ if (!calcHasDecimal) { calcDisplay = calcDisplay * 10.0f + (float)digit; } // ------------------------------------------------ // Decimal-number entry. // // Example: // // 7 . 5 // // calcDisplay becomes 7.5 // ------------------------------------------------ else { calcDisplay += (float)digit * calcDecimalPlace; calcDecimalPlace *= 0.1f; } } drawCalc(); return; } // ==================================================== // DECIMAL POINT // ==================================================== if (key == '.') { // Don't allow two decimal points in one number. if (calcHasDecimal) return; // If "." is the first thing entered after an // operator, start with 0. // // Example: // // 7 * . 5 // // becomes 7 * 0.5 if (calcNewInput) { calcDisplay = 0.0f; calcNewInput = false; } calcHasDecimal = true; calcDecimalPlace = 0.1f; drawCalc(); return; } // ==================================================== // ARITHMETIC OPERATOR // ==================================================== if (key == '+' || key == '-' || key == '*' || key == '/') { // ------------------------------------------------ // If an operator was already pressed, but the user // hasn't entered the next number yet, simply // replace the operator. // // Example: // // 7 * + // // becomes: // // 7 + // ------------------------------------------------ if (calcOperator != 0 && calcNewInput) { calcOperator = key; drawCalc(); return; } // ------------------------------------------------ // FIRST OPERATOR // // Example: // // 7 * // // Save 7 as the first operand. // // IMPORTANT: // We DO NOT calculate 7 * 7 here. // ------------------------------------------------ if (calcOperator == 0) { calcFirstOperand = calcDisplay; } // ------------------------------------------------ // SECOND OPERATOR AFTER A NUMBER // // Example: // // 7 + 3 * // // First calculate 7 + 3 = 10. // Then remember that "*" is the next operation. // ------------------------------------------------ else { switch (calcOperator) { case '+': calcFirstOperand = calcFirstOperand + calcDisplay; break; case '-': calcFirstOperand = calcFirstOperand - calcDisplay; break; case '*': calcFirstOperand = calcFirstOperand * calcDisplay; break; case '/': if (calcDisplay == 0.0f) { calcError = true; drawCalc(); return; } calcFirstOperand = calcFirstOperand / calcDisplay; break; } calcDisplay = calcFirstOperand; } // Save the newly pressed operator. calcOperator = key; // The next digit begins a new number. calcNewInput = true; calcHasDecimal = false; calcDecimalPlace = 0.1f; drawCalc(); return; } // ==================================================== // EQUALS // ==================================================== if (key == '=') { // Nothing to calculate if there is no operator. if (calcOperator == 0) { drawCalc(); return; } // If the user presses "=" immediately after the // operator, don't calculate yet. // // Example: // // 7 * // = // // Just leave 7 on the display. if (calcNewInput) { drawCalc(); return; } // ------------------------------------------------ // Perform the pending calculation. // ------------------------------------------------ switch (calcOperator) { case '+': calcDisplay = calcFirstOperand + calcDisplay; break; case '-': calcDisplay = calcFirstOperand - calcDisplay; break; case '*': calcDisplay = calcFirstOperand * calcDisplay; break; case '/': if (calcDisplay == 0.0f) { calcError = true; drawCalc(); return; } calcDisplay = calcFirstOperand / calcDisplay; break; } // The result becomes the new first operand. calcFirstOperand = calcDisplay; // Operation is finished. calcOperator = 0; calcNewInput = true; calcHasDecimal = false; calcDecimalPlace = 0.1f; drawCalc(); return; } // If we get here, it wasn't a recognized calculator key. return; } } } // ============================================================ // BATTERY HELPERS // ============================================================ void enablePowerADC() { if (!powerReady) return; power.enableTemperatureMeasure(); power.enableBattDetection(); power.enableVbusVoltageMeasure(); power.enableBattVoltageMeasure(); power.enableSystemVoltageMeasure(); } void disablePowerADC() { if (!powerReady) return; power.disableTemperatureMeasure(); power.disableBattDetection(); power.disableVbusVoltageMeasure(); power.disableBattVoltageMeasure(); power.disableSystemVoltageMeasure(); } // ============================================================ // I2C SCANNER // ============================================================ void i2cScan() { Serial.println(); Serial.println("========== I2C SCAN =========="); uint8_t devicesFound = 0; for (uint8_t address = 1; address < 127; address++) { Wire.beginTransmission(address); uint8_t error = Wire.endTransmission(); if (error == 0) { Serial.print("I2C device found at 0x"); if (address < 16) Serial.print("0"); Serial.println(address, HEX); devicesFound++; } } if (devicesFound == 0) Serial.println("No I2C devices found."); else { Serial.print("I2C devices found: "); Serial.println(devicesFound); } Serial.println("=============================="); Serial.println(); } // ============================================================ // AXP2101 DIAGNOSTICS // ============================================================ void printPowerDiagnostics() { if (!powerReady) { Serial.println("POWER DIAGNOSTICS: PMU NOT READY"); return; } uint16_t status = power.status(); uint8_t status1 = (status >> 8) & 0xFF; uint8_t status2 = status & 0xFF; Serial.println(); Serial.println("========== AXP2101 =========="); Serial.print("Chip ID: 0x"); Serial.println(power.getChipID(), HEX); Serial.print("STATUS1: 0x"); if (status1 < 0x10) Serial.print("0"); Serial.println(status1, HEX); Serial.print("STATUS2: 0x"); if (status2 < 0x10) Serial.print("0"); Serial.println(status2, HEX); Serial.print("Battery connected: "); Serial.println(power.isBatteryConnect() ? "YES" : "NO"); Serial.print("Charging: "); Serial.println(power.isCharging() ? "YES" : "NO"); Serial.print("Discharging: "); Serial.println(power.isDischarge() ? "YES" : "NO"); Serial.print("Standby: "); Serial.println(power.isStandby() ? "YES" : "NO"); Serial.print("VBUS input: "); Serial.println(power.isVbusIn() ? "YES" : "NO"); Serial.print("VBUS good: "); Serial.println(power.isVbusGood() ? "YES" : "NO"); Serial.print("Battery voltage: "); Serial.print(power.getBattVoltage()); Serial.println(" mV"); Serial.print("VBUS voltage: "); Serial.print(power.getVbusVoltage()); Serial.println(" mV"); Serial.print("System voltage: "); Serial.print(power.getSystemVoltage()); Serial.println(" mV"); Serial.print("Battery percentage: "); if (power.isBatteryConnect()) { Serial.print(power.getBatteryPercent()); Serial.println("%"); } else Serial.println("N/A"); Serial.print("Charger status: "); Serial.println(power.getChargerStatus()); Serial.print("Thermal regulation: "); Serial.println(power.getThermalRegulationStatus() ? "YES" : "NO"); Serial.print("Current limit status: "); Serial.println(power.getCurrentLimitStatus() ? "YES" : "NO"); Serial.println("============================="); Serial.println(); } // ============================================================ // SYSTEM HEARTBEAT // ============================================================ void systemHeartbeat() { unsigned long now = millis(); if (now - lastHeartbeat < HEARTBEAT_INTERVAL) return; lastHeartbeat = now; Serial.println(); Serial.println("========== SYSTEM HEARTBEAT =========="); Serial.print("Uptime: "); Serial.print(now / 1000); Serial.println(" seconds"); Serial.print("Sensor samples: "); Serial.println(sensorSampleCount); Serial.print("Free heap: "); Serial.print(ESP.getFreeHeap()); Serial.println(" bytes"); Serial.print("Free PSRAM: "); Serial.print(ESP.getFreePsram()); Serial.println(" bytes"); Serial.print("Battery: "); if (batteryConnected) { Serial.print(batteryPercent); Serial.print("% / "); Serial.print(batteryVoltage); Serial.println(" mV"); } else Serial.println("NOT CONNECTED"); Serial.print("VBUS: "); Serial.print(vbusVoltage); Serial.println(" mV"); Serial.print("System voltage: "); Serial.print(systemVoltage); Serial.println(" mV"); Serial.print("Screen: "); Serial.println(screenIsOn ? "ON" : "OFF (panel gated)"); Serial.print("Monitor state: "); switch (monitorState) { case MONITORING: Serial.println("MONITORING"); break; case POSSIBLE_SEIZURE: Serial.println("POSSIBLE_SEIZURE"); break; case ALERT: Serial.println("ALERT"); break; case COOLDOWN: Serial.println("COOLDOWN"); break; } printPowerDiagnostics(); Serial.println("======================================="); Serial.println(); } // ============================================================ // INITIALIZE AXP2101 // ============================================================ bool initPower() { Serial.println("Initializing AXP2101 power management..."); Serial.print("PMU I2C SDA: "); Serial.println(IIC_SDA); Serial.print("PMU I2C SCL: "); Serial.println(IIC_SCL); Serial.print("PMU address: 0x"); Serial.println(AXP2101_SLAVE_ADDRESS, HEX); if (!power.begin(Wire, AXP2101_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) { Serial.println("AXP2101 initialization FAILED."); return false; } Serial.println("AXP2101 responded."); Serial.print("AXP2101 Chip ID: 0x"); Serial.println(power.getChipID(), HEX); power.disableTSPinMeasure(); power.disableIRQ(XPOWERS_AXP2101_ALL_IRQ); power.clearIrqStatus(); power.enableTemperatureMeasure(); power.enableBattDetection(); power.enableVbusVoltageMeasure(); power.enableBattVoltageMeasure(); power.enableSystemVoltageMeasure(); powerReady = true; Serial.println("AXP2101 power management ready."); printPowerDiagnostics(); return true; } // ============================================================ // UPDATE BATTERY DATA // ============================================================ void updateBatteryData() { if (!powerReady) return; unsigned long now = millis(); if (now - lastBatteryUpdate < BATTERY_UPDATE_INTERVAL) return; lastBatteryUpdate = now; batteryConnected = power.isBatteryConnect(); batteryCharging = power.isCharging(); batteryVoltage = power.getBattVoltage(); vbusVoltage = power.getVbusVoltage(); systemVoltage = power.getSystemVoltage(); if (batteryConnected) { batteryPercent = power.getBatteryPercent(); if (batteryPercent < 0) batteryPercent = 0; if (batteryPercent > 100) batteryPercent = 100; } else batteryPercent = 0; Serial.print("BATTERY: "); if (batteryConnected) { Serial.print(batteryPercent); Serial.print("% "); Serial.print(batteryVoltage); Serial.print("mV"); } else Serial.print("NOT CONNECTED"); Serial.print(" CHARGING="); Serial.print(batteryCharging ? "YES" : "NO"); Serial.print(" VBUS="); Serial.print(vbusVoltage); Serial.print("mV"); Serial.print(" VSYS="); Serial.print(systemVoltage); Serial.println("mV"); } // ============================================================ // DRAW BATTERY ICON // ============================================================ void drawBatteryIcon() { const int bx = 335; const int by = 20; const int bw = 48; const int bh = 22; gfx->fillRect(325, 10, 85, 40, COLOR_BG); gfx->drawRect(bx, by, bw, bh, COLOR_WHITE); gfx->fillRect(bx + bw, by + 6, 4, 10, COLOR_WHITE); int fillWidth = ((bw - 4) * batteryPercent) / 100; uint16_t fillColor = COLOR_GREEN; if (batteryPercent <= 20) fillColor = COLOR_RED; else if (batteryPercent <= 40) fillColor = COLOR_YELLOW; if (fillWidth > 0) gfx->fillRect(bx + 2, by + 2, fillWidth, bh - 4, fillColor); if (batteryCharging) { gfx->setTextColor(COLOR_YELLOW); gfx->setTextSize(2); gfx->setCursor(290, 18); gfx->print("+"); } gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(1); gfx->setCursor(335, 45); if (batteryConnected) { gfx->print(batteryPercent); gfx->print("%"); } else gfx->print("--%"); } // ============================================================ // AUDIO INITIALIZATION // ============================================================ bool initAudio() { Serial.println("Initializing ES8311 audio..."); i2s.setPins(AUDIO_SCLK, AUDIO_LRCK, AUDIO_DSDIN, AUDIO_ASDOUT, AUDIO_MCLK); if (!i2s.begin(I2S_MODE_STD, AUDIO_SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, I2S_STD_SLOT_BOTH)) { Serial.println("I2S initialization failed."); return false; } Wire.begin(IIC_SDA, IIC_SCL); pinMode(AUDIO_PA_CTRL, OUTPUT); digitalWrite(AUDIO_PA_CTRL, HIGH); es_handle = es8311_create(AUDIO_I2C_NUM, ES8311_ADDRRES_0); if (!es_handle) { Serial.println("ES8311 create failed."); return false; } const es8311_clock_config_t es_clk = { .mclk_inverted = false, .sclk_inverted = false, .mclk_from_mclk_pin = true, .mclk_frequency = AUDIO_SAMPLE_RATE * 256, .sample_frequency = AUDIO_SAMPLE_RATE }; if (es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16) != ESP_OK) { Serial.println("ES8311 init failed."); return false; } if (es8311_sample_frequency_config(es_handle, es_clk.mclk_frequency, es_clk.sample_frequency) != ESP_OK) { Serial.println("ES8311 sample rate configuration failed."); return false; } if (es8311_voice_volume_set(es_handle, AUDIO_VOLUME, NULL) != ESP_OK) { Serial.println("ES8311 volume configuration failed."); return false; } audioReady = true; Serial.println("ES8311 audio ready."); return true; } // ============================================================ // SIREN TASK // ============================================================ void sirenTask(void *parameter) { const int samplesPerTone = 1600; int16_t *buffer = (int16_t *)malloc(samplesPerTone * 2 * sizeof(int16_t)); if (!buffer) { Serial.println("Siren audio buffer allocation failed."); sirenTaskHandle = NULL; vTaskDelete(NULL); return; } float phase = 0.0f; while (true) { if (!sirenActive) { vTaskDelay(pdMS_TO_TICKS(50)); continue; } for (int i = 0; i < samplesPerTone; i++) { float frequency; if (((millis() / 350) % 2) == 0) frequency = 700.0f; else frequency = 1100.0f; phase += (2.0f * PI * frequency) / AUDIO_SAMPLE_RATE; if (phase >= 2.0f * PI) phase -= 2.0f * PI; int16_t sample = (int16_t)(sin(phase) * 11000.0f); buffer[i * 2] = sample; buffer[i * 2 + 1] = sample; } i2s.write((uint8_t *)buffer, samplesPerTone * 2 * sizeof(int16_t)); } } // ============================================================ // START / STOP SIREN // ============================================================ void startSiren() { if (!audioReady) return; sirenActive = true; } void stopSiren() { sirenActive = false; } // ============================================================ // MONITORING SCREEN // ============================================================ void drawMonitoringScreen() { if(!screenIsOn) return; gfx->fillScreen(COLOR_BG); gfx->setTextWrap(false); gfx->setTextColor(COLOR_GREEN); gfx->setTextSize(3); gfx->setCursor(40, 25); gfx->println("Monitoring"); drawBatteryIcon(); gfx->setTextColor(COLOR_YELLOW); gfx->setTextSize(3); gfx->setCursor(40, 90); gfx->print("TOTAL"); gfx->setTextColor(COLOR_CYAN); gfx->setTextSize(2); gfx->setCursor(40, 165); gfx->print("X:"); gfx->setCursor(40, 205); gfx->print("Y:"); gfx->setCursor(40, 245); gfx->print("Z:"); gfx->setTextColor(COLOR_GREEN); gfx->setTextSize(2); gfx->setCursor(40, 325); gfx->println("MONITORING"); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(3); gfx->setCursor(105, 365); gfx->println("--:-- --"); gfx->setTextSize(1); gfx->setCursor(40, 420); gfx->print("Motion threshold: "); gfx->print(MOTION_DELTA_THRESHOLD, 2); gfx->println(" G"); gfx->setCursor(40, 440); gfx->print("Detection: "); gfx->print(SEIZURE_CONFIRM_TIME); gfx->println(" ms"); lastDisplayedHour = -1; lastDisplayedMinute = -1; updateMonitoringValues(); updateClockDisplay(); currentScreen = SCREEN_MONITORING; } // ============================================================ // MONITORING VALUES // ============================================================ void updateMonitoringValues() { if(!screenIsOn) return; gfx->fillRect(130, 88, 230, 40, COLOR_BG); gfx->setTextColor(COLOR_YELLOW); gfx->setTextSize(3); gfx->setCursor(130, 90); gfx->print(total_acc, 2); gfx->println(" G"); gfx->fillRect(80, 160, 220, 30, COLOR_BG); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(2); gfx->setCursor(80, 165); gfx->print(acc.x, 2); gfx->println(" G"); gfx->fillRect(80, 200, 220, 30, COLOR_BG); gfx->setCursor(80, 205); gfx->print(acc.y, 2); gfx->println(" G"); gfx->fillRect(80, 240, 220, 30, COLOR_BG); gfx->setCursor(80, 245); gfx->print(acc.z, 2); gfx->println(" G"); drawBatteryIcon(); } // ============================================================ // CLOCK // ============================================================ void updateClockDisplay() { if (!screenIsOn) return; RTC_DateTime datetime = rtc.getDateTime(); int hour = datetime.getHour(); int minute = datetime.getMinute(); if (hour == lastDisplayedHour && minute == lastDisplayedMinute) return; lastDisplayedHour = hour; lastDisplayedMinute = minute; int displayHour = hour; if (displayHour == 0) displayHour = 12; else if (displayHour > 12) displayHour -= 12; const char *ampm = (hour < 12) ? "AM" : "PM"; char timeString[16]; snprintf(timeString, sizeof(timeString), "%d:%02d %s", displayHour, minute, ampm); gfx->fillRect(80, 355, 260, 50, COLOR_BG); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(3); gfx->setCursor(105, 365); gfx->println(timeString); } // ============================================================ // POSSIBLE SEIZURE SCREEN // ============================================================ void drawPossibleSeizureScreen() { if(!screenIsOn) return; gfx->fillScreen(COLOR_ORANGE); gfx->setTextColor(COLOR_BG); gfx->setTextSize(3); gfx->setCursor(35, 65); gfx->println("CHECKING"); gfx->setCursor(35, 115); gfx->println("MOVEMENT"); gfx->setTextSize(2); gfx->setCursor(45, 210); gfx->println("Possible seizure"); gfx->setCursor(45, 255); gfx->println("pattern detected"); gfx->setCursor(45, 320); gfx->print("Motion: "); gfx->print(motionCount); } // ============================================================ // ALERT SCREEN // ============================================================ void drawAlertScreen() { if(!screenIsOn) return; gfx->fillScreen(COLOR_RED); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(4); gfx->setCursor(35, 70); gfx->println("ALERT"); gfx->setCursor(35, 130); gfx->println("Seizure Detected"); gfx->setTextSize(2); gfx->setCursor(45, 225); gfx->println("Movement pattern"); gfx->setCursor(45, 265); gfx->println("requires attention"); gfx->setCursor(45, 335); gfx->println("Press GPIO0"); gfx->setCursor(45, 370); gfx->println("to acknowledge"); } // ============================================================ // COOLDOWN SCREEN // ============================================================ void drawCooldownScreen() { if(!screenIsOn) return; gfx->fillScreen(COLOR_BG); gfx->setTextColor(COLOR_YELLOW); gfx->setTextSize(3); gfx->setCursor(55, 90); gfx->println("COOLDOWN"); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(2); gfx->setCursor(45, 190); gfx->println("Returning to"); gfx->setCursor(45, 230); gfx->println("monitoring..."); drawBatteryIcon(); } // ============================================================ // SCREEN ON / OFF — HARDWARE POWER GATE // ============================================================ void screenHardwareOn() { if(screenIsOn) { screenOnTime = millis(); return; } Serial.println(">>> AMOLED WAKE <<<"); digitalWrite(LCD_EN_PIN, HIGH); delay(200); gfx->begin(); FT3168->begin(); FT3168->IIC_Write_Device_State( FT3168->Arduino_IIC_Touch::Device::TOUCH_POWER_MODE, FT3168->Arduino_IIC_Touch::Device_Mode::TOUCH_POWER_MONITOR); screenIsOn = true; screenOnTime = millis(); drawHomeScreen(); } void screenHardwareOff() { if (!screenIsOn) return; Serial.println(">>> AMOLED SLEEP <<<"); // // Stop panel activity first. // Some Arduino_GFX versions do not implement this, // so this is intentionally protected. // #ifdef ARDUINO_GFX_HAS_DISPLAY_OFF gfx->displayOff(); #endif delay(20); // // Disable panel communication and power. // digitalWrite(LCD_CS_PIN, HIGH); delay(5); digitalWrite(LCD_EN_PIN, LOW); screenIsOn = false; screenOnTime = millis(); Serial.println(">>> AMOLED POWER OFF <<<"); } void screenWake() { screenWakeRequested = true; screenHardwareOn(); screenWakeRequested = false; } void screenKeepOn() { if(screenIsOn) screenOnTime = millis(); } // ============================================================ // START ALERT // ============================================================ void triggerAlert() { gameActive = false; gameState = GAME_NONE; waitingForSecondTap = false; monitorState = ALERT; alertStart = millis(); stopSiren(); screenWake(); drawAlertScreen(); startSiren(); screenKeepOn(); Serial.println(); Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); Serial.println(" SEIZURE ALERT"); Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } // ============================================================ // RESET MONITOR // ============================================================ void resetMonitor() { stopSiren(); monitorState = MONITORING; possibleStart = 0; alertStart = 0; cooldownStart = 0; motionCount = 0; motionWindowStart = millis(); if(screenIsOn) { drawMonitoringScreen(); screenKeepOn(); } Serial.println(">>> MONITOR RESET <<<"); } // ============================================================ // PHYSICAL BUTTON (GPIO0) // ============================================================ void checkResetButton() { bool reading = digitalRead(RESET_BUTTON_PIN); unsigned long now = millis(); // ------------------------------------------------------------ // DEBOUNCE // ------------------------------------------------------------ // If the physical reading changed, start a new debounce timer. if (reading != lastButtonReading) { lastButtonChange = now; lastButtonReading = reading; } // Ignore the button until the reading has been stable. if (now - lastButtonChange < BUTTON_DEBOUNCE) return; // ------------------------------------------------------------ // ONLY ACT ON A REAL STATE CHANGE // ------------------------------------------------------------ if (reading == buttonState) return; buttonState = reading; // ------------------------------------------------------------ // BUTTON PRESSED // ------------------------------------------------------------ if (buttonState == BUTTON_PRESSED) { // If the screen is currently OFF, a single GPIO0 press // simply wakes it. We do NOT start a double-press here. if (!screenIsOn) { Serial.println("GPIO0: waking screen"); waitingForButtonSecondPress = false; screenWake(); return; } // A second press within the allowed time = DOUBLE PRESS. if (waitingForButtonSecondPress) { if (now - firstButtonPressTime <= BUTTON_DOUBLE_PRESS_WINDOW) { waitingForButtonSecondPress = false; Serial.println(">>> GPIO0 DOUBLE PRESS <<<"); // Double press toggles the display. if (screenIsOn) { Serial.println(">>> SCREEN OFF <<<"); screenHardwareOff(); } else { Serial.println(">>> SCREEN WAKE <<<"); screenWake(); } return; } // The previous press was too long ago. // Treat this as a brand-new first press. waitingForButtonSecondPress = false; } // -------------------------------------------------------- // FIRST PRESS // -------------------------------------------------------- waitingForButtonSecondPress = true; firstButtonPressTime = now; Serial.println("GPIO0: first press"); } // ------------------------------------------------------------ // SINGLE PRESS TIMEOUT // ------------------------------------------------------------ // If nobody pressed GPIO0 a second time within the window, // the first press becomes a normal single press. if (waitingForButtonSecondPress && now - firstButtonPressTime > BUTTON_DOUBLE_PRESS_WINDOW) { waitingForButtonSecondPress = false; Serial.println(">>> GPIO0 SINGLE PRESS <<<"); // If the screen somehow became OFF while waiting, // just wake it. if (!screenIsOn) { Serial.println("Screen asleep - waking only."); screenWake(); return; } // -------------------------------------------------------- // HOME / RESET // -------------------------------------------------------- Serial.println(">>> HOME / RESET <<<"); stopSiren(); monitorState = MONITORING; possibleStart = 0; alertStart = 0; cooldownStart = 0; motionCount = 0; motionWindowStart = millis(); gameActive = false; gameState = GAME_NONE; waitingForSecondTap = false; calcActive = false; currentScreen = SCREEN_HOME; drawHomeScreen(); screenKeepOn(); } } // ============================================================ // SEIZURE DETECTION // ============================================================ void updateSeizureDetection() { unsigned long now = millis(); motion_delta = fabs(total_acc - previous_acc); previous_acc = total_acc; if (now - motionWindowStart >= SEIZURE_WINDOW) { motionWindowStart = now; motionCount = 0; } if (motion_delta >= MOTION_DELTA_THRESHOLD) motionCount++; if (monitorState == MONITORING) { if (motionCount >= MOTION_COUNT_THRESHOLD) { monitorState = POSSIBLE_SEIZURE; possibleStart = now; screenWake(); drawPossibleSeizureScreen(); screenKeepOn(); Serial.println(">>> POSSIBLE SEIZURE PATTERN <<<"); return; } } else if (monitorState == POSSIBLE_SEIZURE) { if (motionCount >= MOTION_COUNT_THRESHOLD) { if (now - possibleStart >= SEIZURE_CONFIRM_TIME) { triggerAlert(); return; } } else { if (now - possibleStart > SEIZURE_CONFIRM_TIME) { monitorState = MONITORING; if(screenIsOn) { drawMonitoringScreen(); screenKeepOn(); } } } } else if (monitorState == ALERT) { if (now - alertStart >= ALERT_DURATION) { stopSiren(); monitorState = COOLDOWN; cooldownStart = now; drawCooldownScreen(); screenKeepOn(); } } else if (monitorState == COOLDOWN) { if (now - cooldownStart >= ALERT_COOLDOWN) { monitorState = MONITORING; if(screenIsOn) { drawMonitoringScreen(); screenKeepOn(); } } } } // ============================================================ // SERIAL SENSOR DATA // ============================================================ void printSensorData() { unsigned long now = millis(); if (now - lastSerial < 250) return; lastSerial = now; Serial.print("X="); Serial.print(acc.x, 3); Serial.print(" G Y="); Serial.print(acc.y, 3); Serial.print(" G Z="); Serial.print(acc.z, 3); Serial.print(" G TOTAL="); Serial.print(total_acc, 3); Serial.print(" G DELTA="); Serial.print(motion_delta, 3); Serial.print(" MOTION="); Serial.print(motionCount); Serial.print(" STATE="); switch (monitorState) { case MONITORING: Serial.println("MONITORING"); break; case POSSIBLE_SEIZURE: Serial.println("POSSIBLE_SEIZURE"); break; case ALERT: Serial.println("ALERT"); break; case COOLDOWN: Serial.println("COOLDOWN"); break; } } // ============================================================ // TOUCH READING // ============================================================ bool readTouch(int &x, int &y) { x = FT3168->IIC_Read_Device_Value( FT3168->Arduino_IIC_Touch::Value_Information::TOUCH_COORDINATE_X); y = FT3168->IIC_Read_Device_Value( FT3168->Arduino_IIC_Touch::Value_Information::TOUCH_COORDINATE_Y); if (x < 0 || y < 0) return false; if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return false; return true; } // ============================================================ // MONITORING TOUCH (DOUBLE-TAP TO START GAME) // ============================================================ void processMonitoringTouch(int x, int y) { unsigned long now = millis(); if (now - lastTouchTime < TOUCH_DEBOUNCE) return; lastTouchTime = now; if (waitingForSecondTap) { if (now - firstTapTime <= DOUBLE_TAP_WINDOW) { waitingForSecondTap = false; Serial.println(">>> DOUBLE TAP DETECTED <<<"); Serial.print("Second tap X="); Serial.print(x); Serial.print(" Y="); Serial.println(y); startGame(); return; } waitingForSecondTap = false; Serial.println("Second tap timeout - starting new first tap."); } waitingForSecondTap = true; firstTapTime = now; Serial.println("First tap detected."); Serial.println("Waiting for second tap..."); } // ============================================================ // GAME SCREEN // ============================================================ void drawGameQuestion() { if(!screenIsOn) return; gfx->fillScreen(COLOR_BG); gfx->setTextColor(COLOR_CYAN); gfx->setTextSize(3); gfx->setCursor(55, 70); gfx->println("GUESS THE"); gfx->setCursor(105, 120); gfx->println("COLOR?"); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(2); gfx->setCursor(90, 230); gfx->println("TAP TO REVEAL"); gfx->setTextSize(2); gfx->setCursor(125, 430); gfx->print("SCORE: "); gfx->print(score); } // ============================================================ // GAME REVEAL SCREEN // ============================================================ void drawGameReveal() { if(!screenIsOn) return; gfx->fillScreen(gameColors[selectedColor].color); gfx->setTextColor(COLOR_WHITE); if (selectedColor == 1 || selectedColor == 3 || selectedColor == 5) gfx->setTextColor(COLOR_BG); gfx->setTextSize(4); gfx->setCursor(70, 120); gfx->println(gameColors[selectedColor].name); gfx->setTextSize(2); gfx->setCursor(65, 230); gfx->println("WERE YOU RIGHT?"); gfx->fillRoundRect(25, 300, 110, 65, 10, COLOR_GREEN); gfx->setTextColor(COLOR_BG); gfx->setTextSize(2); gfx->setCursor(55, 323); gfx->println("YES"); gfx->fillRoundRect(150, 300, 110, 65, 10, COLOR_RED); gfx->setTextColor(COLOR_WHITE); gfx->setCursor(185, 323); gfx->println("NO"); gfx->fillRoundRect(275, 300, 110, 65, 10, COLOR_CYAN); gfx->setTextColor(COLOR_BG); gfx->setCursor(300, 323); gfx->println("EXIT"); gfx->setTextColor(COLOR_WHITE); gfx->setTextSize(2); gfx->setCursor(130, 410); gfx->print("SCORE: "); gfx->print(score); } // ============================================================ // START / NEW ROUND / EXIT GAME // ============================================================ void startGame() { gameActive = true; gameState = GAME_QUESTION; waitingForSecondTap = false; score = 0; rounds = 0; Serial.println(">>> COLOR GAME STARTED <<<"); drawGameQuestion(); screenKeepOn(); } void newGameRound() { selectedColor = random(0, GAME_COLOR_COUNT); gameState = GAME_REVEAL; drawGameReveal(); screenKeepOn(); } void exitGame() { gameActive = false; gameState = GAME_NONE; waitingForSecondTap = false; Serial.print("Color game finished. Score: "); Serial.print(score); Serial.print("/"); Serial.println(rounds); if(screenIsOn) { drawMonitoringScreen(); screenKeepOn(); } } // ============================================================ // GAME TOUCH // ============================================================ void processGameTouch(int x, int y) { unsigned long now = millis(); if (now - lastTouchTime < TOUCH_DEBOUNCE) return; lastTouchTime = now; if (gameState == GAME_QUESTION) { Serial.println("Game: question tapped"); newGameRound(); return; } if (gameState == GAME_REVEAL) { if (x >= 25 && x <= 135 && y >= 300 && y <= 365) { score++; rounds++; Serial.println("Game: YES"); drawGameQuestion(); gameState = GAME_QUESTION; screenKeepOn(); return; } if (x >= 150 && x <= 260 && y >= 300 && y <= 365) { rounds++; Serial.println("Game: NO"); drawGameQuestion(); gameState = GAME_QUESTION; screenKeepOn(); return; } if (x >= 275 && x <= 385 && y >= 300 && y <= 365) { Serial.println("Game: EXIT"); exitGame(); return; } } } // ============================================================ // TOUCH SYSTEM // ============================================================ void processTouch() { // ------------------------------------------------------------ // NO NEW TOUCH = NOTHING TO DO // ------------------------------------------------------------ // // The FT3168 interrupt sets this flag when a touch occurs. // This prevents us from continuously reading old/stale // touchscreen coordinates. // if (!FT3168->IIC_Interrupt_Flag) return; // Clear the event flag now that we are handling it. FT3168->IIC_Interrupt_Flag = false; int x, y; // ------------------------------------------------------------ // READ THE TOUCH COORDINATES // ------------------------------------------------------------ if (!readTouch(x, y)) return; Serial.print("Touch X="); Serial.print(x); Serial.print(" Y="); Serial.println(y); // ------------------------------------------------------------ // SCREEN IS OFF // ------------------------------------------------------------ // // A real touchscreen interrupt while asleep wakes the watch. // IMPORTANT: We do NOT send that same touch into an app. // if (!screenIsOn) { Serial.println(">>> TOUCH WAKE <<<"); screenWake(); return; } // ------------------------------------------------------------ // ALERT MODE // ------------------------------------------------------------ // // Don't let touchscreen taps interfere with an alert. // if (monitorState == ALERT) return; // ------------------------------------------------------------ // HOME SCREEN // ------------------------------------------------------------ if (currentScreen == SCREEN_HOME) { processHomeScreenTouch(x, y); return; } // ------------------------------------------------------------ // MONITORING SCREEN // ------------------------------------------------------------ if (currentScreen == SCREEN_MONITORING) { if (monitorState == MONITORING) { processMonitoringTouch(x, y); } return; } // ------------------------------------------------------------ // COLOR GAME // ------------------------------------------------------------ if (currentScreen == SCREEN_GAME) { processGameTouch(x, y); return; } // ------------------------------------------------------------ // CALCULATOR // ------------------------------------------------------------ if (currentScreen == SCREEN_CALC) { processCalcTouch(x, y); return; } } // ============================================================ // SETUP // ============================================================ void setup() { Serial.begin(115200); delay(3000); Serial.println(); Serial.println("========================================"); Serial.println(" PIPER FALL / SEZURE WATCH"); Serial.println(" ESP32-S3 + CO5300 + QMI8658 + RTC"); Serial.println(" V2 + AXP2101 BATTERY"); Serial.println("========================================"); // Button pinMode(RESET_BUTTON_PIN, INPUT_PULLUP); // Waveshare hardware #ifdef DEV_DEVICE_INIT DEV_DEVICE_INIT(); Serial.println("Waveshare hardware initialized."); #else Serial.println("WARNING: DEV_DEVICE_INIT not defined!"); #endif // LCD power rail pinMode(LCD_EN_PIN, OUTPUT); digitalWrite(LCD_EN_PIN, HIGH); screenIsOn = true; screenOnTime = millis(); screenWakeRequested = false; Serial.println("LCD power rail (DSI_PWR_EN) on GPIO"); Serial.println(LCD_EN_PIN); // Display Serial.println("Initializing AMOLED..."); if (!gfx->begin()) { Serial.println("AMOLED initialization failed!"); while (true) delay(1000); } gfx->fillScreen(COLOR_BG); Serial.println("AMOLED initialized."); // I2C Wire.begin(IIC_SDA, IIC_SCL); Serial.println("I2C bus initialized."); // I2C scan i2cScan(); // Power / AXP2101 if (!initPower()) { Serial.println("WARNING: AXP2101 unavailable."); Serial.println("Continuing without battery diagnostics."); } // Touch Serial.println("Initializing FT3168 touch..."); unsigned long ft3168Start = millis(); while (true) { if (FT3168->begin()) { break; } Serial.println("FT3168 init failed, retrying..."); delay(1000); if (millis() - ft3168Start > 15000) { Serial.println("FT3168 timeout - continuing without touch."); break; } } FT3168->IIC_Write_Device_State( FT3168->Arduino_IIC_Touch::Device::TOUCH_POWER_MODE, FT3168->Arduino_IIC_Touch::Device_Mode::TOUCH_POWER_MONITOR); Serial.println("FT3168 touch ready."); Serial.print("TP_INT pin: "); Serial.println(TP_INT); pinMode(TP_INT, INPUT_PULLUP); Serial.print("TP_INT state: "); Serial.println(digitalRead(TP_INT)); // RTC Serial.println("Initializing PCF85063 RTC..."); if (!rtc.begin(Wire, IIC_SDA, IIC_SCL)) { Serial.println("RTC initialization failed!"); gfx->fillScreen(COLOR_BG); gfx->setTextColor(COLOR_RED); gfx->setTextSize(3); gfx->setCursor(35, 100); gfx->println("RTC ERROR"); while (true) delay(1000); } Serial.println("PCF85063 detected."); // Set RTC from compile time uint8_t buildMonth = 1; uint8_t buildDay; uint16_t buildYear; uint8_t buildHour; uint8_t buildMinute; uint8_t buildSecond; char monthString[4]; int day, year; sscanf(__DATE__, "%3s %d %d", monthString, &day, &year); if (strcmp(monthString, "Jan") == 0) buildMonth = 1; else if (strcmp(monthString, "Feb") == 0) buildMonth = 2; else if (strcmp(monthString, "Mar") == 0) buildMonth = 3; else if (strcmp(monthString, "Apr") == 0) buildMonth = 4; else if (strcmp(monthString, "May") == 0) buildMonth = 5; else if (strcmp(monthString, "Jun") == 0) buildMonth = 6; else if (strcmp(monthString, "Jul") == 0) buildMonth = 7; else if (strcmp(monthString, "Aug") == 0) buildMonth = 8; else if (strcmp(monthString, "Sep") == 0) buildMonth = 9; else if (strcmp(monthString, "Oct") == 0) buildMonth = 10; else if (strcmp(monthString, "Nov") == 0) buildMonth = 11; else buildMonth = 12; buildYear = year; buildDay = day; sscanf(__TIME__, "%hhu:%hhu:%hhu", &buildHour, &buildMinute, &buildSecond); buildMinute += 3; if (buildMinute >= 60) { buildMinute -= 60; buildHour++; if (buildHour >= 24) { buildHour = 0; buildDay++; } } rtc.setDateTime(buildYear, buildMonth, buildDay, buildHour, buildMinute, buildSecond); RTC_DateTime rtcNow = rtc.getDateTime(); Serial.print("RTC: "); Serial.print(rtcNow.getHour()); Serial.print(":"); Serial.print(rtcNow.getMinute()); Serial.println(); // QMI8658 Serial.println("Initializing QMI8658..."); if (!qmi.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) { Serial.println("QMI8658 initialization failed."); gfx->fillScreen(COLOR_BG); gfx->setTextColor(COLOR_RED); gfx->setTextSize(3); gfx->setCursor(40, 100); gfx->println("QMI8658"); gfx->setCursor(40, 150); gfx->println("ERROR"); while (true) delay(1000); } Serial.println("QMI8658 detected."); if (!qmi.configAccelerometer( SensorQMI8658::ACC_RANGE_4G, SensorQMI8658::ACC_ODR_1000Hz, SensorQMI8658::LPF_MODE_0)) { Serial.println("Accelerometer configuration failed."); } else { Serial.println("Accelerometer configured."); } if (!qmi.enableAccelerometer()) { Serial.println("Accelerometer enable failed."); } else { Serial.println("Accelerometer enabled."); } // Audio initAudio(); // Siren task if (audioReady) { xTaskCreatePinnedToCore( sirenTask, "SirenTask", 4096, NULL, 1, &sirenTaskHandle, 0); } // Random randomSeed(micros()); // Initial values motionWindowStart = millis(); previous_acc = 1.0f; sensorSampleCount = 0; // Initial battery if (powerReady) { lastBatteryUpdate = millis() - BATTERY_UPDATE_INTERVAL; updateBatteryData(); } // Initial screen drawHomeScreen(); // Heartbeat lastHeartbeat = millis(); Serial.println(); Serial.println("========================================"); Serial.println(" SYSTEM READY"); Serial.println("========================================"); Serial.println("GPIO0 = Single press Home / Reset"); Serial.println("GPIO0 = Double press Screen ON/OFF"); Serial.print("GPIO0 double-press window = "); Serial.print(BUTTON_DOUBLE_PRESS_WINDOW); Serial.println(" ms"); Serial.println("Tap icons to launch apps"); Serial.println("Touch controller = FT3168"); Serial.println("Power management = AXP2101"); Serial.print("Motion delta threshold = "); Serial.print(MOTION_DELTA_THRESHOLD, 3); Serial.println(" G"); Serial.print("Double-tap window = "); Serial.print(DOUBLE_TAP_WINDOW); Serial.println(" ms"); Serial.print("Touch debounce = "); Serial.print(TOUCH_DEBOUNCE); Serial.println(" ms"); Serial.print("Screen auto-off = "); Serial.print(HOME_SCREEN_TIMEOUT); Serial.println(" ms"); Serial.println("10-second system heartbeat enabled."); Serial.println(); } // ============================================================ // SCREEN AUTO OFF // ============================================================ void screenAutoOffCheck() { if(!screenIsOn) return; unsigned long timeout; switch(currentScreen) { case SCREEN_HOME: timeout = HOME_SCREEN_TIMEOUT; break; case SCREEN_MONITORING: timeout = IM_SCREEN_TIMEOUT; break; case SCREEN_GAME: timeout = GAME_SCREEN_TIMEOUT; break; case SCREEN_CALC: timeout = CALC_SCREEN_TIMEOUT; break; default: timeout = HOME_SCREEN_TIMEOUT; break; } if(millis() - screenOnTime >= timeout) { Serial.println(">>> SCREEN TIMEOUT <<<"); screenHardwareOff(); } } // ============================================================ // LOOP // ============================================================ void loop() { // Physical button checkResetButton(); // Touch processTouch(); // Clock unsigned long now = millis(); if (now - lastClockDisplay >= 1000) { lastClockDisplay = now; // Only redraw the clock when the panel is actually on. if (screenIsOn && monitorState == MONITORING && currentScreen == SCREEN_MONITORING) updateClockDisplay(); } // Screen auto-off screenAutoOffCheck(); // Battery updateBatteryData(); // Accelerometer if (qmi.getDataReady()) { if (qmi.getAccelerometer(acc.x, acc.y, acc.z)) { sensorSampleCount++; total_acc = sqrt((acc.x * acc.x) + (acc.y * acc.y) + (acc.z * acc.z)); printSensorData(); updateSeizureDetection(); now = millis(); // Only redraw monitoring when the panel is actually on. if (screenIsOn && monitorState == MONITORING && currentScreen == SCREEN_MONITORING) { if (now - lastDisplay >= 100) { lastDisplay = now; updateMonitoringValues(); } } } } // Heartbeat systemHeartbeat(); // Yield yield(); }