Created
January 26, 2026 16:47
-
-
Save MarsTechHAN/ae1c02a4d60a63a84bb2ee4ac4fa2f1b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <M5Unified.h> | |
| #include <WiFi.h> | |
| #include <esp_sleep.h> | |
| #include <esp_pm.h> | |
| #include <driver/rtc_io.h> | |
| #include <sys/time.h> | |
| #include "soc/esp32s3/rtc.h" | |
| // Import M5PM1 from lib folder | |
| #include "M5PM1.h" | |
| // StickS3 Button GPIO definitions | |
| #define BTN_A_GPIO GPIO_NUM_11 | |
| #define BTN_B_GPIO GPIO_NUM_12 | |
| // Create M5PM1 instance | |
| M5PM1 pm1; | |
| // Constants | |
| const uint32_t WORK_CYCLE_DURATION = 15 * 60 * 1000; // 15 minutes | |
| const uint32_t ALERT_BEFORE_END = 5 * 1000; // 5 seconds before end | |
| const uint32_t SCREEN_ON_DURATION = 15 * 1000; // 15 seconds screen on | |
| const uint32_t BLOCK_DURATION = 30 * 60 * 1000; // 30 minutes per block | |
| const uint32_t REST_BLOCK_DURATION = 30 * 60 * 1000; // 30 minutes per rest triangle | |
| // Colors - Professional and eye-friendly | |
| const uint32_t COLOR_WORK_BG = 0x1B3A4B; // Deep blue for work | |
| const uint32_t COLOR_WORK_TEXT = 0xE8F4F8; // Light cyan text | |
| const uint32_t COLOR_WORK_ACCENT = 0x4FC3F7; // Bright blue accent | |
| const uint32_t COLOR_REST_BG = 0x2E4D3D; // Deep green for rest | |
| const uint32_t COLOR_REST_TEXT = 0xE8F5E9; // Light green text | |
| const uint32_t COLOR_REST_ACCENT = 0x81C784; // Soft green accent | |
| const uint32_t COLOR_BLOCK_EMPTY = 0x37474F; // Dark gray for empty blocks | |
| const uint32_t COLOR_BLOCK_FILL = 0xFFB74D; // Warm orange for filled blocks | |
| const uint32_t COLOR_TRIANGLE_EMPTY = 0x3D5A6B; // Dark teal for empty triangles | |
| const uint32_t COLOR_TRIANGLE_FILL = 0x90CAF9; // Light blue for filled triangles | |
| // State variables using RTC time | |
| RTC_DATA_ATTR bool isWorkMode = true; | |
| RTC_DATA_ATTR uint32_t totalWorkTime = 0; // Total work time in milliseconds | |
| RTC_DATA_ATTR uint32_t totalRestTime = 0; // Total rest time in milliseconds | |
| RTC_DATA_ATTR uint32_t savedCycleElapsed = 0; // Saved elapsed time before sleep | |
| RTC_DATA_ATTR bool isCountdownPhase = false; | |
| RTC_DATA_ATTR bool isTimeUp = false; | |
| RTC_DATA_ATTR bool screenWasOn = false; | |
| RTC_DATA_ATTR uint64_t rtcSleepStartUs = 0; | |
| uint32_t wakeupTimeMs = 0; // Millis when device woke up | |
| uint32_t cycleStartMillis = 0; // Millis when cycle started (relative to wakeup) | |
| uint32_t screenOnMillis = 0; // Millis when screen turned on | |
| bool needConfirmation = false; | |
| bool buttonPressed = false; | |
| bool awake = false; | |
| uint32_t timeUpStartMs = 0; | |
| uint32_t lastInvertToggleMs = 0; | |
| bool invertDisplayOn = false; | |
| // Sprite for double buffering | |
| LGFX_Sprite canvas(&M5.Display); | |
| // Battery monitoring | |
| float batteryVoltage = 0.0; | |
| int batteryPercent = 0; | |
| uint32_t lastBatteryUpdate = 0; | |
| const uint32_t BATTERY_UPDATE_INTERVAL = 5000; // Update every 5 seconds | |
| // Get current elapsed time for cycle (accounts for sleep) | |
| uint32_t getCurrentCycleElapsed() { | |
| if (isTimeUp) { | |
| return WORK_CYCLE_DURATION; | |
| } | |
| return savedCycleElapsed + (millis() - cycleStartMillis); | |
| } | |
| // Update battery status | |
| void updateBatteryStatus() { | |
| uint16_t voltage_mv = 0; | |
| m5pm1_err_t result = pm1.readVbat(&voltage_mv); | |
| if (result == M5PM1_OK && voltage_mv > 0) { | |
| batteryVoltage = voltage_mv / 1000.0; // Convert to volts | |
| // Calculate battery percentage based on typical Li-ion voltage curve | |
| // 4.2V = 100%, 3.7V = 50%, 3.0V = 0% | |
| if (batteryVoltage >= 4.2) { | |
| batteryPercent = 100; | |
| } else if (batteryVoltage >= 3.7) { | |
| batteryPercent = 50 + (int)((batteryVoltage - 3.7) / 0.5 * 50); | |
| } else if (batteryVoltage >= 3.0) { | |
| batteryPercent = (int)((batteryVoltage - 3.0) / 0.7 * 50); | |
| } else { | |
| batteryPercent = 0; | |
| } | |
| // Clamp to 0-100 range | |
| if (batteryPercent > 100) batteryPercent = 100; | |
| if (batteryPercent < 0) batteryPercent = 0; | |
| } else { | |
| // Failed to read, try to use M5.Power if available | |
| batteryVoltage = M5.Power.getBatteryVoltage() / 1000.0; | |
| if (batteryVoltage > 0) { | |
| // Calculate percentage based on voltage | |
| if (batteryVoltage >= 4.2) { | |
| batteryPercent = 100; | |
| } else if (batteryVoltage >= 3.7) { | |
| batteryPercent = 50 + (int)((batteryVoltage - 3.7) / 0.5 * 50); | |
| } else if (batteryVoltage >= 3.0) { | |
| batteryPercent = (int)((batteryVoltage - 3.0) / 0.7 * 50); | |
| } else { | |
| batteryPercent = 0; | |
| } | |
| if (batteryPercent > 100) batteryPercent = 100; | |
| if (batteryPercent < 0) batteryPercent = 0; | |
| } | |
| } | |
| } | |
| // Draw battery icon in bottom right corner | |
| void drawBatteryIcon(LGFX_Sprite* spr) { | |
| const int iconWidth = 16; | |
| const int iconHeight = 8; | |
| const int tipWidth = 2; | |
| const int tipHeight = 4; | |
| const int margin = 2; | |
| // Calculate position for bottom right (text first, then icon) | |
| spr->setFont(&fonts::Font0); | |
| spr->setTextSize(1); | |
| char batteryText[16]; | |
| sprintf(batteryText, "%.2fV", batteryVoltage); | |
| int textWidth = spr->textWidth(batteryText); | |
| int totalWidth = iconWidth + tipWidth + 3 + textWidth; // icon + gap + text | |
| int iconX = spr->width() - totalWidth - margin; | |
| int iconY = spr->height() - iconHeight - margin; | |
| // Battery body outline | |
| spr->drawRect(iconX, iconY, iconWidth, iconHeight, COLOR_WORK_TEXT); | |
| // Battery tip | |
| spr->fillRect(iconX + iconWidth, iconY + (iconHeight - tipHeight) / 2, tipWidth, tipHeight, COLOR_WORK_TEXT); | |
| // Battery fill based on percentage | |
| uint32_t fillColor; | |
| if (batteryPercent > 50) { | |
| fillColor = 0x4CAF50; // Green | |
| } else if (batteryPercent > 20) { | |
| fillColor = 0xFFC107; // Amber | |
| } else { | |
| fillColor = 0xF44336; // Red | |
| } | |
| int fillWidth = (iconWidth - 4) * batteryPercent / 100; | |
| if (fillWidth > 0) { | |
| spr->fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4, fillColor); | |
| } | |
| // Draw voltage text to the right of icon | |
| spr->setTextDatum(bottom_right); | |
| spr->setTextColor(COLOR_WORK_TEXT); | |
| spr->drawString(batteryText, spr->width() - margin, spr->height() - margin); | |
| } | |
| void disablePeripherals() { | |
| // Disable WiFi and Bluetooth | |
| WiFi.mode(WIFI_OFF); | |
| btStop(); | |
| // Disable microphone and speaker | |
| M5.Mic.end(); | |
| M5.Speaker.end(); | |
| // Set low power mode | |
| setCpuFrequencyMhz(80); // Lower CPU frequency | |
| // Configure M5PM1 power management for ultra low power | |
| // Based on PMIC pinout: | |
| // - PYG3_SPK_Pulse (pin 13) -> G3 (GPIO3) PWM function | |
| // - PY_STATUS_LED (pin 5) -> LED_EN_PP | |
| // Turn off speaker by setting GPIO3 to LOW (SPK_Pulse) | |
| // GPIO3 corresponds to M5PM1_GPIO_NUM_3 | |
| pm1.gpioSetMode(M5PM1_GPIO_NUM_3, M5PM1_GPIO_MODE_OUTPUT); | |
| pm1.gpioSetOutput(M5PM1_GPIO_NUM_3, LOW); | |
| // Turn off status LED by setting LED_EN to LOW | |
| pm1.setLedEnLevel(false); | |
| // Keep LDO 3.3V enabled (required for system) | |
| pm1.setLdoEnable(true); | |
| // Keep DCDC 5V enabled (required for display and system) | |
| pm1.setDcdcEnable(true); | |
| // Disable BOOST/GROVE port if not needed | |
| pm1.setBoostEnable(false); | |
| // Optional: Configure charge current for battery safety | |
| // pm1.setChargeEnable(true); // Keep charging enabled | |
| // Configure RTC to use high-frequency clock for better accuracy | |
| // ESP32-S3 uses internal RC oscillator by default, which is calibrated | |
| // The RTC will continue running during deep sleep | |
| } | |
| void drawProgressBlocks(LGFX_Sprite* spr, uint32_t totalTime) { | |
| const int screenWidth = spr->width(); | |
| const int blockSize = 12; // Square blocks | |
| const int blockSpacing = 2; | |
| const int blockY = 3; | |
| const int maxBlocks = (screenWidth - 10) / (blockSize + blockSpacing); | |
| uint32_t completedBlocks = totalTime / BLOCK_DURATION; | |
| uint32_t currentBlockProgress = (totalTime % BLOCK_DURATION) * 100 / BLOCK_DURATION; | |
| int startX = (screenWidth - (maxBlocks * (blockSize + blockSpacing) - blockSpacing)) / 2; | |
| int x = startX; | |
| for (int i = 0; i < maxBlocks; i++) { | |
| if (i < completedBlocks) { | |
| // Fully filled block | |
| spr->fillRoundRect(x, blockY, blockSize, blockSize, 2, COLOR_BLOCK_FILL); | |
| } else if (i == completedBlocks) { | |
| // Partially filled block | |
| spr->drawRoundRect(x, blockY, blockSize, blockSize, 2, COLOR_BLOCK_EMPTY); | |
| int fillWidth = (blockSize - 2) * currentBlockProgress / 100; | |
| if (fillWidth > 0) { | |
| spr->fillRoundRect(x + 1, blockY + 1, fillWidth, blockSize - 2, 1, COLOR_BLOCK_FILL); | |
| } | |
| } else { | |
| // Empty block | |
| spr->drawRoundRect(x, blockY, blockSize, blockSize, 2, COLOR_BLOCK_EMPTY); | |
| } | |
| x += blockSize + blockSpacing; | |
| } | |
| } | |
| void drawRestTriangles(LGFX_Sprite* spr, uint32_t totalRestTime) { | |
| const int screenWidth = spr->width(); | |
| const int triangleWidth = 12; | |
| const int triangleHeight = 10; | |
| const int triangleSpacing = 2; | |
| const int triangleY = 18; // Below the blocks | |
| const int maxTriangles = (screenWidth - 10) / (triangleWidth + triangleSpacing); | |
| uint32_t completedTriangles = totalRestTime / REST_BLOCK_DURATION; | |
| uint32_t currentTriangleProgress = (totalRestTime % REST_BLOCK_DURATION) * 100 / REST_BLOCK_DURATION; | |
| int startX = (screenWidth - (maxTriangles * (triangleWidth + triangleSpacing) - triangleSpacing)) / 2; | |
| int x = startX; | |
| for (int i = 0; i < maxTriangles; i++) { | |
| int x0 = x + triangleWidth / 2; | |
| int y0 = triangleY; | |
| int x1 = x; | |
| int y1 = triangleY + triangleHeight; | |
| int x2 = x + triangleWidth; | |
| int y2 = triangleY + triangleHeight; | |
| if (i < completedTriangles) { | |
| // Fully filled triangle | |
| spr->fillTriangle(x0, y0, x1, y1, x2, y2, COLOR_TRIANGLE_FILL); | |
| } else if (i == completedTriangles && currentTriangleProgress > 0) { | |
| // Partially filled triangle | |
| spr->drawTriangle(x0, y0, x1, y1, x2, y2, COLOR_TRIANGLE_EMPTY); | |
| // Fill from bottom up | |
| int fillHeight = triangleHeight * currentTriangleProgress / 100; | |
| if (fillHeight > 0) { | |
| int cutY = y1 - fillHeight; | |
| // Calculate the width at the cut line | |
| float widthRatio = (float)fillHeight / triangleHeight; | |
| int cutWidth = triangleWidth * widthRatio; | |
| int cutX1 = x0 - cutWidth / 2; | |
| int cutX2 = x0 + cutWidth / 2; | |
| spr->fillTriangle(cutX1, cutY, cutX2, cutY, x0, y1, COLOR_TRIANGLE_FILL); | |
| spr->fillTriangle(x1, y1, cutX1, cutY, x0, y1, COLOR_TRIANGLE_FILL); | |
| spr->fillTriangle(x2, y2, cutX2, cutY, x0, y1, COLOR_TRIANGLE_FILL); | |
| } | |
| } else { | |
| // Empty triangle | |
| spr->drawTriangle(x0, y0, x1, y1, x2, y2, COLOR_TRIANGLE_EMPTY); | |
| } | |
| x += triangleWidth + triangleSpacing; | |
| } | |
| } | |
| void drawTime(LGFX_Sprite* spr, uint32_t remainingMs) { | |
| int minutes = remainingMs / 60000; | |
| int seconds = (remainingMs % 60000) / 1000; | |
| spr->setTextDatum(middle_center); | |
| spr->setTextSize(1); | |
| // Create time string | |
| char timeStr[10]; | |
| sprintf(timeStr, "%02d:%02d", minutes, seconds); | |
| // Use large font for better visibility | |
| spr->setFont(&fonts::FreeSansBold24pt7b); | |
| spr->setTextColor(isWorkMode ? COLOR_WORK_TEXT : COLOR_REST_TEXT); | |
| int centerX = spr->width() / 2; | |
| int centerY = spr->height() / 2 + 5; | |
| spr->drawString(timeStr, centerX, centerY); | |
| // Draw mode indicator | |
| spr->setFont(&fonts::FreeSans9pt7b); | |
| const char* modeText = isWorkMode ? "WORK" : "REST"; | |
| uint32_t modeColor = isWorkMode ? COLOR_WORK_ACCENT : COLOR_REST_ACCENT; | |
| spr->setTextColor(modeColor); | |
| spr->drawString(modeText, centerX, centerY + 32); | |
| } | |
| void drawCountdownPrompt(LGFX_Sprite* spr) { | |
| spr->setFont(&fonts::FreeSans9pt7b); | |
| spr->setTextColor(isWorkMode ? COLOR_WORK_TEXT : COLOR_REST_TEXT); | |
| spr->setTextDatum(bottom_center); | |
| int centerX = spr->width() / 2; | |
| int bottomY = spr->height() - 6; | |
| spr->drawString("Press Btn A", centerX, bottomY); | |
| } | |
| void drawTimeUpMessage(LGFX_Sprite* spr) { | |
| spr->setTextColor(isWorkMode ? COLOR_WORK_TEXT : COLOR_REST_TEXT); | |
| spr->setFont(&fonts::FreeSansBold18pt7b); | |
| spr->setTextDatum(top_center); | |
| spr->drawString("Time's Up!", spr->width() / 2, 6); | |
| spr->setFont(&fonts::FreeSans9pt7b); | |
| spr->setTextDatum(bottom_center); | |
| spr->drawString("Press Btn A", spr->width() / 2, spr->height() - 6); | |
| } | |
| void updateDisplay() { | |
| uint32_t bgColor = isWorkMode ? COLOR_WORK_BG : COLOR_REST_BG; | |
| // Draw to sprite buffer | |
| canvas.fillScreen(bgColor); | |
| // Calculate current cycle elapsed time | |
| uint32_t currentCycleElapsed = getCurrentCycleElapsed(); | |
| if (isWorkMode && currentCycleElapsed > WORK_CYCLE_DURATION) { | |
| currentCycleElapsed = WORK_CYCLE_DURATION; | |
| } | |
| if (isWorkMode) { | |
| drawProgressBlocks(&canvas, totalWorkTime + currentCycleElapsed); | |
| drawRestTriangles(&canvas, totalRestTime); | |
| if (isTimeUp) { | |
| drawTime(&canvas, 0); | |
| drawTimeUpMessage(&canvas); | |
| } else if (isCountdownPhase) { | |
| uint32_t remaining = (currentCycleElapsed >= WORK_CYCLE_DURATION) | |
| ? 0 | |
| : (WORK_CYCLE_DURATION - currentCycleElapsed); | |
| drawTime(&canvas, remaining); | |
| drawCountdownPrompt(&canvas); | |
| } else { | |
| uint32_t remaining = (currentCycleElapsed >= WORK_CYCLE_DURATION) | |
| ? 0 | |
| : (WORK_CYCLE_DURATION - currentCycleElapsed); | |
| drawTime(&canvas, remaining); | |
| } | |
| } else { | |
| // Rest mode - show work progress and rest triangles | |
| drawProgressBlocks(&canvas, totalWorkTime); | |
| drawRestTriangles(&canvas, totalRestTime + currentCycleElapsed); | |
| canvas.setTextDatum(middle_center); | |
| canvas.setFont(&fonts::FreeSansBold18pt7b); | |
| canvas.setTextColor(COLOR_REST_TEXT); | |
| canvas.drawString("Rest Mode", canvas.width() / 2, canvas.height() / 2); | |
| canvas.setFont(&fonts::FreeSans9pt7b); | |
| int minutes = currentCycleElapsed / 60000; | |
| int seconds = (currentCycleElapsed % 60000) / 1000; | |
| char timeStr[20]; | |
| sprintf(timeStr, "%02d:%02d", minutes, seconds); | |
| canvas.drawString(timeStr, canvas.width() / 2, canvas.height() / 2 + 25); | |
| } | |
| // Draw battery icon on top of everything | |
| drawBatteryIcon(&canvas); | |
| // Push sprite to display - no flicker | |
| canvas.pushSprite(0, 0); | |
| } | |
| void enterDeepSleep() { | |
| // Save current elapsed time before sleep | |
| if (!isTimeUp && !isCountdownPhase) { | |
| savedCycleElapsed = getCurrentCycleElapsed(); | |
| } | |
| rtcSleepStartUs = esp_rtc_get_time_us(); | |
| M5.Display.setBrightness(0); | |
| M5.Display.sleep(); | |
| // Configure wake up sources for StickS3 | |
| esp_sleep_enable_ext1_wakeup((1ULL << BTN_A_GPIO) | (1ULL << BTN_B_GPIO), ESP_EXT1_WAKEUP_ANY_LOW); | |
| // Set timer wakeup for work mode (only if not in countdown/time up) | |
| if (isWorkMode && !isTimeUp && !isCountdownPhase) { | |
| uint32_t alertAtMs = (WORK_CYCLE_DURATION > ALERT_BEFORE_END) | |
| ? (WORK_CYCLE_DURATION - ALERT_BEFORE_END) | |
| : 0; | |
| if (savedCycleElapsed < alertAtMs && alertAtMs > 1000) { | |
| uint32_t wakeAfterMs = alertAtMs - savedCycleElapsed; | |
| // Ensure timer is reasonable (between 1s and 1 hour) | |
| if (wakeAfterMs >= 1000 && wakeAfterMs <= 3600000) { | |
| esp_sleep_enable_timer_wakeup((uint64_t)wakeAfterMs * 1000ULL); | |
| } | |
| } | |
| } | |
| esp_deep_sleep_start(); | |
| } | |
| void startNewCycle() { | |
| savedCycleElapsed = 0; | |
| cycleStartMillis = millis(); | |
| isCountdownPhase = false; | |
| isTimeUp = false; | |
| needConfirmation = false; | |
| timeUpStartMs = 0; | |
| lastInvertToggleMs = 0; | |
| invertDisplayOn = false; | |
| } | |
| void setup() { | |
| auto cfg = M5.config(); | |
| cfg.internal_mic = false; | |
| cfg.internal_spk = false; | |
| M5.begin(cfg); | |
| // Initialize M5PM1 power management chip with M5Unified's I2C | |
| // PM1 uses I2C address 0x6E by default | |
| if (pm1.begin(&Wire, M5PM1_DEFAULT_ADDR) != M5PM1_OK) { | |
| // PM1 initialization failed, continue anyway | |
| } | |
| // Wait for PM1 to be ready | |
| delay(100); | |
| M5.Display.setRotation(1); | |
| M5.Display.setBrightness(100); | |
| M5.Display.wakeup(); | |
| // Create sprite buffer for flicker-free drawing | |
| canvas.createSprite(M5.Display.width(), M5.Display.height()); | |
| disablePeripherals(); | |
| // Check if this is first boot | |
| esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); | |
| if (wakeup_reason == ESP_SLEEP_WAKEUP_UNDEFINED) { | |
| // First boot - initialize everything and clear RTC residual data | |
| isWorkMode = true; | |
| totalWorkTime = 0; | |
| totalRestTime = 0; | |
| savedCycleElapsed = 0; | |
| isCountdownPhase = false; | |
| isTimeUp = false; | |
| screenWasOn = true; | |
| awake = true; | |
| rtcSleepStartUs = 0; | |
| timeUpStartMs = 0; | |
| lastInvertToggleMs = 0; | |
| invertDisplayOn = false; | |
| } else { | |
| // Waking from sleep | |
| uint64_t rtcNowUs = esp_rtc_get_time_us(); | |
| // Validate RTC time to prevent overflow | |
| if (rtcSleepStartUs > 0 && rtcNowUs > rtcSleepStartUs && (rtcNowUs - rtcSleepStartUs) < 3600000000ULL) { | |
| uint32_t sleepDeltaMs = (uint32_t)((rtcNowUs - rtcSleepStartUs) / 1000); | |
| savedCycleElapsed += sleepDeltaMs; | |
| // Clamp to cycle duration | |
| if (isWorkMode && savedCycleElapsed > WORK_CYCLE_DURATION) { | |
| savedCycleElapsed = WORK_CYCLE_DURATION; | |
| } | |
| } | |
| uint32_t alertStartElapsed = (WORK_CYCLE_DURATION > ALERT_BEFORE_END) | |
| ? (WORK_CYCLE_DURATION - ALERT_BEFORE_END) | |
| : 0; | |
| if (isWorkMode && savedCycleElapsed >= WORK_CYCLE_DURATION) { | |
| savedCycleElapsed = WORK_CYCLE_DURATION; | |
| isTimeUp = true; | |
| isCountdownPhase = false; | |
| } else if (isWorkMode && savedCycleElapsed >= alertStartElapsed) { | |
| isCountdownPhase = true; | |
| isTimeUp = false; | |
| } else { | |
| isCountdownPhase = false; | |
| isTimeUp = false; | |
| } | |
| if (isCountdownPhase || isTimeUp) { | |
| needConfirmation = true; | |
| awake = false; | |
| M5.Display.setBrightness(255); | |
| M5.Display.wakeup(); | |
| } | |
| awake = false; | |
| screenWasOn = true; | |
| } | |
| // Reset millis-based timers | |
| wakeupTimeMs = millis(); | |
| cycleStartMillis = millis(); | |
| screenOnMillis = millis(); | |
| // Initialize battery status | |
| updateBatteryStatus(); | |
| lastBatteryUpdate = millis(); | |
| updateDisplay(); | |
| } | |
| void loop() { | |
| M5.update(); | |
| uint32_t currentCycleElapsed = getCurrentCycleElapsed(); | |
| bool cycleResetThisLoop = false; | |
| // Handle button presses | |
| if (M5.BtnA.wasPressed()) { | |
| if (isWorkMode && isTimeUp) { | |
| totalWorkTime += WORK_CYCLE_DURATION; | |
| startNewCycle(); | |
| awake = true; | |
| screenWasOn = true; | |
| screenOnMillis = millis(); | |
| updateDisplay(); | |
| cycleResetThisLoop = true; | |
| } else if (!awake) { | |
| // First press after wake - just acknowledge | |
| awake = true; | |
| screenWasOn = true; // Enable screen updates | |
| screenOnMillis = millis(); | |
| updateDisplay(); | |
| } else { | |
| screenOnMillis = millis(); | |
| } | |
| } | |
| if (M5.BtnB.wasPressed()) { | |
| if (!awake) { | |
| awake = true; | |
| screenWasOn = true; // Enable screen updates | |
| screenOnMillis = millis(); | |
| updateDisplay(); | |
| } else { | |
| // Toggle work/rest mode | |
| if (isWorkMode) { | |
| // Switching to rest - save current work progress | |
| totalWorkTime += currentCycleElapsed; | |
| isWorkMode = false; | |
| savedCycleElapsed = 0; | |
| cycleStartMillis = millis(); | |
| isCountdownPhase = false; | |
| isTimeUp = false; | |
| } else { | |
| // Switching back to work - save rest time | |
| totalRestTime += currentCycleElapsed; | |
| isWorkMode = true; | |
| startNewCycle(); | |
| cycleResetThisLoop = true; | |
| } | |
| updateDisplay(); | |
| screenOnMillis = millis(); | |
| } | |
| } | |
| // Check for alert phase in work mode | |
| if (cycleResetThisLoop) { | |
| currentCycleElapsed = getCurrentCycleElapsed(); | |
| } | |
| if (isWorkMode && !cycleResetThisLoop) { | |
| uint32_t alertStartElapsed = (WORK_CYCLE_DURATION > ALERT_BEFORE_END) | |
| ? (WORK_CYCLE_DURATION - ALERT_BEFORE_END) | |
| : 0; | |
| if (!isTimeUp && currentCycleElapsed >= WORK_CYCLE_DURATION) { | |
| isTimeUp = true; | |
| isCountdownPhase = false; | |
| needConfirmation = true; | |
| awake = false; | |
| screenWasOn = true; | |
| M5.Display.wakeup(); | |
| M5.Display.setBrightness(255); | |
| updateDisplay(); | |
| screenOnMillis = millis(); | |
| timeUpStartMs = millis(); | |
| } else if (!isCountdownPhase && !isTimeUp && currentCycleElapsed >= alertStartElapsed) { | |
| isCountdownPhase = true; | |
| needConfirmation = true; | |
| awake = false; | |
| screenWasOn = true; | |
| M5.Display.wakeup(); | |
| M5.Display.setBrightness(255); | |
| updateDisplay(); | |
| screenOnMillis = millis(); | |
| } | |
| // Update display every second during work | |
| static uint32_t lastUpdate = 0; | |
| if (millis() - lastUpdate >= 1000) { | |
| lastUpdate = millis(); | |
| if (screenWasOn || isCountdownPhase || isTimeUp) { | |
| updateDisplay(); | |
| } | |
| } | |
| } else { | |
| // Rest mode - update display every second if screen is on | |
| static uint32_t lastRestUpdate = 0; | |
| if (millis() - lastRestUpdate >= 1000) { | |
| lastRestUpdate = millis(); | |
| if (screenWasOn) { | |
| updateDisplay(); | |
| } | |
| } | |
| } | |
| // Update battery status periodically | |
| if (millis() - lastBatteryUpdate >= BATTERY_UPDATE_INTERVAL) { | |
| updateBatteryStatus(); | |
| lastBatteryUpdate = millis(); | |
| if (screenWasOn || isCountdownPhase || isTimeUp) { | |
| updateDisplay(); | |
| } | |
| } | |
| // Handle screen timeout | |
| if (!isCountdownPhase && !isTimeUp && millis() - screenOnMillis >= SCREEN_ON_DURATION) { | |
| screenWasOn = false; // Will be set to true on next wake | |
| enterDeepSleep(); | |
| } | |
| // Flash invert after timeout in time-up phase | |
| if (isTimeUp) { | |
| if (timeUpStartMs == 0) { | |
| timeUpStartMs = millis(); | |
| } | |
| if (millis() - timeUpStartMs >= SCREEN_ON_DURATION) { | |
| if (millis() - lastInvertToggleMs >= 500) { | |
| invertDisplayOn = !invertDisplayOn; | |
| M5.Display.invertDisplay(invertDisplayOn); | |
| lastInvertToggleMs = millis(); | |
| } | |
| } | |
| } else { | |
| if (invertDisplayOn) { | |
| M5.Display.invertDisplay(false); | |
| invertDisplayOn = false; | |
| } | |
| timeUpStartMs = 0; | |
| lastInvertToggleMs = 0; | |
| } | |
| delay(50); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment