-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgraytimer.ino
More file actions
273 lines (238 loc) · 8.95 KB
/
graytimer.ino
File metadata and controls
273 lines (238 loc) · 8.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#include "myutils.h"
// ==================== CONFIGURATION ====================
const bool ENABLE_TIME_SETUP = false; // Enable serial time input on boot
const bool ENABLE_PARTIAL_REFRESH = true; // Enable partial refresh for faster updates
const uint8_t FULL_REFRESH_INTERVAL = 10; // Full refresh when minute % N == 0 (e.g., :00, :10, :20, etc.)
const bool ENABLE_WATCHFACE_CYCLING = true; // Auto-cycle through watchfaces every 10 mins
const bool ENABLE_SERIAL_DEBUG = false; // Disable Serial to save ~1-2mA power (set false for deployment)
// Smart polling strategy for battery optimization
const uint16_t POLL_INTERVAL_MS = 200; // Poll every 200ms during active window
const uint8_t WAKE_BEFORE_MINUTE_SEC = 57; // Wake at second 57 (3-second polling window)
// Range: 50-57 recommended (10s-3s window)
// Lower = more battery, higher = safer
// Changed from 55 to 57 for better battery (2s more sleep)
// Serial debug macros (only print if enabled)
#define DEBUG_PRINT(x) if(ENABLE_SERIAL_DEBUG) Serial.print(x)
#define DEBUG_PRINTLN(x) if(ENABLE_SERIAL_DEBUG) Serial.println(x)
#define DEBUG_FLUSH() if(ENABLE_SERIAL_DEBUG) Serial.flush()
// Global RTC manager
RTCManager rtcManager;
// Track last displayed minute to detect changes
uint8_t lastDisplayedMinute = 255; // Force first update
bool displayInitialized = false; // Track if display has been initialized
bool firstUpdate = true; // Track first update to force full refresh
// Watchface cycling - array of all available watchfaces
WatchFace* allWatchFaces[] = {
new WatchFace_atat(),
new WatchFace_atdp(),
new WatchFace_b1(),
new WatchFace_ben10(),
new WatchFace_bird(),
new WatchFace_bird2(),
new WatchFace_bugs(),
new WatchFace_claw(),
new WatchFace_claw2(),
new WatchFace_claw3(),
new WatchFace_crow(),
new WatchFace_dog(),
new WatchFace_giraffe1(),
new WatchFace_guitar1(),
new WatchFace_guitar2(),
new WatchFace_h(),
new WatchFace_harley(),
new WatchFace_harry934(),
new WatchFace_herbert(),
new WatchFace_hogwarts(),
new WatchFace_hogwarts2(),
new WatchFace_hogwarts3(),
new WatchFace_hogwarts4(),
new WatchFace_jitsu1(),
new WatchFace_jitsu2(),
new WatchFace_jitsu3(),
new WatchFace_jitsu4(),
new WatchFace_jitsu5(),
new WatchFace_krishna(),
new WatchFace_macaw(),
new WatchFace_mikew(),
new WatchFace_mountain1(),
new WatchFace_mountain2(),
new WatchFace_peacock(),
new WatchFace_peacock3(),
new WatchFace_pegasus(),
new WatchFace_penguin_beatles(),
new WatchFace_penguins(),
new WatchFace_planets(),
new WatchFace_ps(),
new WatchFace_saturn(),
new WatchFace_sensei(),
new WatchFace_sortinghat(),
new WatchFace_square(),
new WatchFace_square_invert(),
new WatchFace_stormtrooper2(),
new WatchFace_stormtrooper3floyd(),
new WatchFace_sullivan(),
new WatchFace_thiruman(),
new WatchFace_tom(),
new WatchFace_tree(),
new WatchFace_walker(),
new WatchFace_xwing(),
new WatchFace_zebra()
};
const uint8_t NUM_WATCHFACES = sizeof(allWatchFaces) / sizeof(allWatchFaces[0]);
uint8_t currentWatchFaceIndex = 0;
WatchFace* currentWatchFace = allWatchFaces[currentWatchFaceIndex];
/**
* Setup - runs once on power-on
*/
void setup() {
if (ENABLE_SERIAL_DEBUG) {
Serial.begin(115200);
while (!Serial) delay(10);
}
DEBUG_PRINTLN("\n=== E-Paper Watch ===");
// Initialize RTC
if (!rtcManager.begin()) {
DEBUG_PRINTLN("ERROR: RTC not found!");
while (1) delay(1000);
}
// Seed random number generator using RTC time with improved entropy
// NOTE: We seed once and then use the continuous LCG sequence for best distribution
DateTime now = rtcManager.rtc.now();
// Create a well-distributed seed from timestamp components
// Using prime number multiplication for better mixing
unsigned long seed = now.unixtime();
seed = seed * 2654435761UL; // Knuth's multiplicative hash constant
seed ^= (now.second() * 16777619UL); // FNV prime
seed ^= (now.minute() << 11);
seed ^= (now.hour() << 19);
randomSeed(seed);
// Pick random initial watchface
// The continuous LCG sequence provides excellent distribution
currentWatchFaceIndex = random(NUM_WATCHFACES);
currentWatchFace = allWatchFaces[currentWatchFaceIndex];
DEBUG_PRINT("Initial random watchface #");
DEBUG_PRINTLN(currentWatchFaceIndex);
// Optional: Set time via Serial
if (ENABLE_TIME_SETUP) {
rtcManager.setupTimeViaSerial();
}
// Initial display update
DEBUG_PRINTLN("Initializing display...");
updateDisplay();
lastDisplayedMinute = rtcManager.getCurrentMinute();
DEBUG_PRINTLN("Watch ready!");
DEBUG_PRINT("Smart polling: Sleep until :");
DEBUG_PRINT(WAKE_BEFORE_MINUTE_SEC);
DEBUG_PRINT("s, then poll every ");
DEBUG_PRINT(POLL_INTERVAL_MS);
DEBUG_PRINTLN("ms");
}
/**
* Smart polling loop - optimized for battery life with NO DRIFT
*
* Strategy (drift-proof):
* 1. After display update, check current second
* 2. Calculate safe sleep time (to wake at WAKE_BEFORE_MINUTE_SEC)
* 3. Sleep until configured wake second
* 4. Poll every POLL_INTERVAL_MS until minute changes
* 5. Repeat
*
* This ensures we ALWAYS catch the minute change
* Power consumption: ~200-500µA average
*/
void loop() {
uint8_t currentMinute = rtcManager.getCurrentMinute();
// Check if minute changed
if (currentMinute != lastDisplayedMinute) {
DEBUG_PRINT("Minute ");
DEBUG_PRINT(lastDisplayedMinute);
DEBUG_PRINT(" → ");
DEBUG_PRINTLN(currentMinute);
updateDisplay();
lastDisplayedMinute = currentMinute;
// Get current second to calculate sleep duration
DateTime now = rtcManager.rtc.now();
uint8_t currentSecond = now.second();
// Calculate how long to sleep to wake up at WAKE_BEFORE_MINUTE_SEC
// We want to wake a few seconds before the minute changes to start polling
uint16_t sleepSeconds;
if (currentSecond < WAKE_BEFORE_MINUTE_SEC) {
// If we're before wake time, sleep until wake second
sleepSeconds = WAKE_BEFORE_MINUTE_SEC - currentSecond;
} else {
// If we're after wake time, sleep until next minute's wake second
sleepSeconds = (60 - currentSecond) + WAKE_BEFORE_MINUTE_SEC;
}
// Sleep for calculated duration (minus 1 second for safety)
if (sleepSeconds > 1) {
sleepSeconds -= 1; // Wake up 1 second early to be safe
DEBUG_PRINT("Sleeping for ");
DEBUG_PRINT(sleepSeconds);
DEBUG_PRINTLN(" seconds...");
DEBUG_FLUSH();
delay(sleepSeconds * 1000);
}
// Now we're at approximately WAKE_BEFORE_MINUTE_SEC
// Poll every POLL_INTERVAL_MS until minute changes
DEBUG_PRINT("Active polling (");
DEBUG_PRINT(POLL_INTERVAL_MS);
DEBUG_PRINTLN("ms) - waiting for minute change");
}
// Active polling mode - check every POLL_INTERVAL_MS
// This catches the minute change quickly
delay(POLL_INTERVAL_MS);
}
/**
* Update e-paper display with current time from RTC
*/
void updateDisplay() {
// Initialize display ONLY ONCE on first call
if (!displayInitialized) {
display.init(115200, true, 2, false);
display.setRotation(0);
displayInitialized = true;
}
// Get formatted time and date from RTC
String timeStr = rtcManager.getFormattedTime(!currentWatchFace->noAMPM);
String dateStr = rtcManager.getFormattedDate();
uint8_t currentMinute = rtcManager.getCurrentMinute();
// Check if it's time for full refresh (every 10 mins: :00, :10, :20, etc.)
bool isFullRefreshTime = (currentMinute % FULL_REFRESH_INTERVAL == 0);
// Cycle to random watchface if enabled and it's full refresh time
if (ENABLE_WATCHFACE_CYCLING && isFullRefreshTime && !firstUpdate) {
// Pick a random watchface different from the current one
// Using continuous LCG sequence (no re-seeding) for optimal distribution
uint8_t newIndex;
do {
newIndex = random(NUM_WATCHFACES);
} while (newIndex == currentWatchFaceIndex && NUM_WATCHFACES > 1);
currentWatchFaceIndex = newIndex;
currentWatchFace = allWatchFaces[currentWatchFaceIndex];
DEBUG_PRINT("Random watchface #");
DEBUG_PRINT(currentWatchFaceIndex);
DEBUG_PRINT(" (");
DEBUG_PRINT(NUM_WATCHFACES);
DEBUG_PRINTLN(" total)");
}
// Determine refresh mode
// FULL refresh: First update OR every 10 minutes (for ghosting prevention)
// PARTIAL refresh: All other times (faster, no flicker)
bool usePartialRefresh = ENABLE_PARTIAL_REFRESH &&
!firstUpdate &&
!isFullRefreshTime;
if (firstUpdate) {
firstUpdate = false;
}
unsigned long startTime = millis();
// Draw watchface
drawWatchFace(currentWatchFace, timeStr, dateStr, usePartialRefresh);
unsigned long elapsed = millis() - startTime;
DEBUG_PRINT(timeStr);
DEBUG_PRINT(" (");
DEBUG_PRINT(usePartialRefresh ? "partial" : "full");
DEBUG_PRINT(", ");
DEBUG_PRINT(elapsed);
DEBUG_PRINTLN("ms)");
// Put display in low-power mode
display.hibernate();
}