Add Calendar Mode for automated e-ink calendar display

Features:
- CalendarActivity: Connects to WiFi, fetches BMP from URL, displays as sleep screen
- Timer wake: Device wakes every N hours (configurable) to refresh
- Settings: calendarModeEnabled, calendarRefreshHours, calendarServerUrl
- Fallback: Uses cached image if fetch fails
- Power button still works for normal use (GPIO wake preserved)
This commit is contained in:
Chris Korhonen 2026-01-17 06:35:11 -05:00
parent 21277e03eb
commit 6600681082
5 changed files with 324 additions and 2 deletions

View File

@ -12,9 +12,9 @@
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 2; // Incremented for calendar mode
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 18; constexpr uint8_t SETTINGS_COUNT = 21; // Added 3 calendar settings
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -48,6 +48,10 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, textAntiAliasing); serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, longPressChapterSkip);
// Calendar mode settings
serialization::writePod(outputFile, calendarModeEnabled);
serialization::writePod(outputFile, calendarRefreshHours);
serialization::writeString(outputFile, std::string(calendarServerUrl));
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -116,6 +120,18 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip); serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
// Calendar mode settings
serialization::readPod(inputFile, calendarModeEnabled);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, calendarRefreshHours);
if (++settingsRead >= fileSettingsCount) break;
{
std::string urlStr;
serialization::readString(inputFile, urlStr);
strncpy(calendarServerUrl, urlStr.c_str(), sizeof(calendarServerUrl) - 1);
calendarServerUrl[sizeof(calendarServerUrl) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -93,6 +93,11 @@ class CrossPointSettings {
// Long-press chapter skip on side buttons // Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1; uint8_t longPressChapterSkip = 1;
// Calendar mode settings
uint8_t calendarModeEnabled = 0; // 0 = disabled, 1 = enabled
uint8_t calendarRefreshHours = 4; // Refresh interval in hours (1-24)
char calendarServerUrl[256] = ""; // URL to fetch BMP image from
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance

View File

@ -0,0 +1,232 @@
#include "CalendarActivity.h"
#include <GfxRenderer.h>
#include <HTTPClient.h>
#include <SDCardManager.h>
#include <WiFi.h>
#include <esp_sleep.h>
#include "../../CrossPointSettings.h"
#include "../../WifiCredentialStore.h"
#include "../../fontIds.h"
#include "../boot_sleep/SleepActivity.h"
// External functions from main.cpp
extern void exitActivity();
extern void enterNewActivity(Activity* activity);
extern void enterDeepSleep();
void CalendarActivity::onEnter() {
Activity::onEnter();
state = CalendarState::INIT;
stateStartTime = millis();
Serial.printf("[%lu] [CAL] Calendar mode starting\n", millis());
// Begin WiFi connection
startWifiConnection();
}
void CalendarActivity::startWifiConnection() {
Serial.printf("[%lu] [CAL] Loading WiFi credentials\n", millis());
// Load saved credentials
WIFI_STORE.loadFromFile();
const auto& credentials = WIFI_STORE.getCredentials();
if (credentials.empty()) {
handleError("No saved WiFi");
return;
}
// Use first saved network
const auto& cred = credentials[0];
Serial.printf("[%lu] [CAL] Connecting to: %s\n", millis(), cred.ssid.c_str());
WiFi.mode(WIFI_STA);
WiFi.begin(cred.ssid.c_str(), cred.password.c_str());
state = CalendarState::CONNECTING_WIFI;
stateStartTime = millis();
}
bool CalendarActivity::checkWifiConnection() {
return WiFi.status() == WL_CONNECTED;
}
bool CalendarActivity::fetchAndSaveImage() {
Serial.printf("[%lu] [CAL] Fetching image from: %s\n", millis(), SETTINGS.calendarServerUrl);
HTTPClient http;
http.begin(SETTINGS.calendarServerUrl);
http.setTimeout(30000);
http.setConnectTimeout(10000);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("[%lu] [CAL] HTTP error: %d\n", millis(), httpCode);
http.end();
return false;
}
int contentLength = http.getSize();
Serial.printf("[%lu] [CAL] Content length: %d bytes\n", millis(), contentLength);
// Open file for writing using SdMan
FsFile file;
if (!SdMan.openFileForWrite("CAL", "/sleep.bmp", file)) {
Serial.printf("[%lu] [CAL] Failed to open /sleep.bmp for writing\n", millis());
http.end();
return false;
}
// Stream response to file
WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[512];
int totalWritten = 0;
while (http.connected() && (contentLength > 0 || contentLength == -1)) {
size_t available = stream->available();
if (available) {
size_t toRead = min(available, sizeof(buffer));
size_t bytesRead = stream->readBytes(buffer, toRead);
if (bytesRead > 0) {
file.write(buffer, bytesRead);
totalWritten += bytesRead;
if (contentLength > 0) {
contentLength -= bytesRead;
}
}
} else {
delay(10);
}
// Check for timeout
if (millis() - stateStartTime > HTTP_TIMEOUT_MS) {
Serial.printf("[%lu] [CAL] HTTP timeout during download\n", millis());
file.close();
http.end();
return false;
}
// Break if we've received everything
if (contentLength == 0) {
break;
}
}
file.close();
http.end();
Serial.printf("[%lu] [CAL] Saved %d bytes to /sleep.bmp\n", millis(), totalWritten);
return totalWritten > 0;
}
void CalendarActivity::renderSleepScreen() {
Serial.printf("[%lu] [CAL] Rendering sleep screen\n", millis());
// Force sleep screen mode to CUSTOM to use our downloaded image
SETTINGS.sleepScreen = CrossPointSettings::CUSTOM;
// Create and enter SleepActivity to render the image
exitActivity();
enterNewActivity(new SleepActivity(renderer, mappedInput));
}
void CalendarActivity::scheduleWakeAndSleep() {
Serial.printf("[%lu] [CAL] Scheduling wake in %d hours\n", millis(), SETTINGS.calendarRefreshHours);
// Disconnect WiFi to save power
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
// Calculate sleep duration in microseconds
uint64_t sleepDurationUs = (uint64_t)SETTINGS.calendarRefreshHours * 60ULL * 60ULL * 1000000ULL;
Serial.printf("[%lu] [CAL] Sleep duration: %llu us (%d hours)\n", millis(), sleepDurationUs,
SETTINGS.calendarRefreshHours);
// Enable timer wakeup
esp_sleep_enable_timer_wakeup(sleepDurationUs);
// Also keep GPIO wakeup for power button (allows manual wake for normal use)
esp_deep_sleep_enable_gpio_wakeup(1ULL << 3, ESP_GPIO_WAKEUP_GPIO_LOW); // Power button pin
Serial.printf("[%lu] [CAL] Entering deep sleep\n", millis());
// Enter deep sleep
esp_deep_sleep_start();
}
void CalendarActivity::handleError(const char* message) {
Serial.printf("[%lu] [CAL] Error: %s\n", millis(), message);
errorMessage = message;
state = CalendarState::ERROR;
stateStartTime = millis();
// For now, just log - we'll use default sleep screen on error
}
void CalendarActivity::renderStatus(const char* status) {
// Minimal status rendering - just log for now
Serial.printf("[%lu] [CAL] Status: %s\n", millis(), status);
}
void CalendarActivity::loop() {
switch (state) {
case CalendarState::INIT:
// Should not reach here - onEnter handles init
break;
case CalendarState::CONNECTING_WIFI:
if (checkWifiConnection()) {
Serial.printf("[%lu] [CAL] WiFi connected, IP: %s\n", millis(), WiFi.localIP().toString().c_str());
state = CalendarState::FETCHING_IMAGE;
stateStartTime = millis();
} else if (millis() - stateStartTime > WIFI_TIMEOUT_MS) {
handleError("WiFi timeout");
}
break;
case CalendarState::FETCHING_IMAGE:
if (fetchAndSaveImage()) {
Serial.printf("[%lu] [CAL] Image saved successfully\n", millis());
state = CalendarState::RENDERING;
} else {
// Check if we have a cached image
if (SdMan.exists("/sleep.bmp")) {
Serial.printf("[%lu] [CAL] Fetch failed, using cached image\n", millis());
state = CalendarState::RENDERING;
} else {
handleError("Fetch failed");
}
}
break;
case CalendarState::RENDERING:
renderSleepScreen();
// After SleepActivity renders, we need to schedule sleep
state = CalendarState::SCHEDULING_SLEEP;
break;
case CalendarState::SCHEDULING_SLEEP:
scheduleWakeAndSleep();
// Should never reach here - device sleeps
break;
case CalendarState::ERROR:
// Wait 3 seconds showing error, then sleep anyway (use cached image if available)
if (millis() - stateStartTime > 3000) {
if (SdMan.exists("/sleep.bmp")) {
state = CalendarState::RENDERING;
} else {
// No cached image - just sleep with default screen and try again later
scheduleWakeAndSleep();
}
}
break;
}
}

View File

@ -0,0 +1,57 @@
#pragma once
#include "../Activity.h"
/**
* CalendarActivity - Automated calendar image fetch and display
*
* This activity is triggered on timer wake (not power button wake).
* It connects to WiFi, fetches a BMP image from a configured URL,
* saves it as the sleep screen, and returns to deep sleep.
*
* Flow:
* 1. Load saved WiFi credentials
* 2. Connect to WiFi (timeout: 30s)
* 3. HTTP GET image from configured URL (timeout: 60s)
* 4. Save image to /sleep.bmp on SD card
* 5. Render sleep screen
* 6. Schedule timer wake for next refresh
* 7. Enter deep sleep
*/
enum class CalendarState {
INIT,
CONNECTING_WIFI,
FETCHING_IMAGE,
SAVING_IMAGE,
RENDERING,
SCHEDULING_SLEEP,
ERROR
};
class CalendarActivity final : public Activity {
public:
explicit CalendarActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("Calendar", renderer, mappedInput) {}
void onEnter() override;
void loop() override;
bool preventAutoSleep() override { return true; }
bool skipLoopDelay() override { return true; }
private:
CalendarState state = CalendarState::INIT;
unsigned long stateStartTime = 0;
String errorMessage;
void startWifiConnection();
bool checkWifiConnection();
bool fetchAndSaveImage();
void renderSleepScreen();
void scheduleWakeAndSleep();
void handleError(const char* message);
void renderStatus(const char* status);
static constexpr unsigned long WIFI_TIMEOUT_MS = 30000;
static constexpr unsigned long HTTP_TIMEOUT_MS = 60000;
};

View File

@ -6,6 +6,7 @@
#include <SDCardManager.h> #include <SDCardManager.h>
#include <SPI.h> #include <SPI.h>
#include <builtinFonts/all.h> #include <builtinFonts/all.h>
#include <esp_sleep.h>
#include <cstring> #include <cstring>
@ -21,6 +22,7 @@
#include "activities/reader/ReaderActivity.h" #include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h" #include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h" #include "activities/util/FullScreenMessageActivity.h"
#include "activities/calendar/CalendarActivity.h"
#include "fontIds.h" #include "fontIds.h"
#define SPI_FQ 40000000 #define SPI_FQ 40000000
@ -290,6 +292,16 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
// Check if this is a timer wake for calendar mode
esp_sleep_wakeup_cause_t wakeupCause = esp_sleep_get_wakeup_cause();
if (wakeupCause == ESP_SLEEP_WAKEUP_TIMER && SETTINGS.calendarModeEnabled) {
Serial.printf("[%lu] [ ] Timer wake detected - entering calendar mode\n", millis());
setupDisplayAndFonts();
exitActivity();
enterNewActivity(new CalendarActivity(renderer, mappedInputManager));
return; // Skip normal boot flow
}
// verify power button press duration after we've read settings. // verify power button press duration after we've read settings.
verifyWakeupLongPress(); verifyWakeupLongPress();