// 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 <Arduino.h>
#include <Wire.h>
#include <math.h>
#include <memory>
#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<Arduino_IIC_DriveBus> IIC_Bus =
std::make_shared<Arduino_HWIIC>(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<Arduino_IIC> 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();
}