Upgraded firmware for Simone Giertz's Every Day Calendar that links an ATProto-powered ESP32, for sync with goals.garden 🌱
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Wifi connected animation

+96 -11
+96 -11
firmware/sketches/EverydayCalendar/EverydayCalendar.ino
··· 15 15 void honeyDrip(); 16 16 void clearAnimation(); 17 17 void doBurst(Point p); 18 + void doRipple(int8_t scaledX, int8_t scaledY); 19 + void doConnectedRipple(); 18 20 19 21 EverydayCalendar_touch cal_touch; 20 22 EverydayCalendar_lights cal_lights; ··· 69 71 70 72 uint16_t ANIM_MAX_TICK = 0; 71 73 74 + // Ripple animation - expanding circle like a raindrop on water 75 + // Dynamically calculated: lights LEDs within 1 unit of the circle's perimeter 76 + // Uses scaled coordinates (x2) to allow 0.5 LED radius increments and fractional center 77 + typedef struct { 78 + int8_t x; // Center X in scaled coords (actual * 2) 79 + int8_t y; // Center Y in scaled coords (actual * 2) 80 + unsigned long startTime = 0; // 0 = inactive 81 + } Ripple; 82 + const size_t RIPPLE_COUNT = 1; // Single ripple from center 83 + Ripple ripples[RIPPLE_COUNT]; 84 + 85 + const uint16_t RIPPLE_DURATION_MS = 2000; // 2 seconds total 86 + const uint8_t RIPPLE_MAX_RADIUS = 40; // In half-LED units (0.5 increments), so 20 LEDs max 87 + const uint16_t RIPPLE_MS_PER_RADIUS = RIPPLE_DURATION_MS / RIPPLE_MAX_RADIUS; // 50ms 88 + 72 89 73 90 void doBurst(Point p) { 74 91 for (size_t i = 0; i < BURST_COUNT; i++) ··· 82 99 } 83 100 } 84 101 102 + void doRipple(int8_t scaledX, int8_t scaledY) { 103 + for (size_t i = 0; i < RIPPLE_COUNT; i++) { 104 + Ripple* ripple = &ripples[i]; 105 + if (ripple->startTime == 0) { 106 + ripple->x = scaledX; 107 + ripple->y = scaledY; 108 + ripple->startTime = millis(); 109 + if (ripple->startTime == 0) ripple->startTime = 1; // Avoid 0 (inactive marker) 110 + return; 111 + } 112 + } 113 + } 114 + 115 + void doConnectedRipple() { 116 + // Start ripple from halfway between June and July, on the 16th 117 + // Scaled coordinates (x2): (5.5, 15) becomes (11, 30) 118 + doRipple(11, 30); 119 + Serial.println(F("Connected ripple animation started")); 120 + } 121 + 85 122 void setup() { 86 123 Serial.begin(9600); 87 124 Serial.println("Sketch setup started"); 88 125 89 - // Init data 90 - for (size_t a = 0; a < ANIM_COUNT; a++) 91 - { 126 + // Init burst animation max tick 127 + for (size_t a = 0; a < ANIM_COUNT; a++) { 92 128 if (ANIM[a].tickEnd > ANIM_MAX_TICK) { 93 129 ANIM_MAX_TICK = ANIM[a].tickEnd; 94 130 } ··· 140 176 void loop() { 141 177 // Process incoming sync commands from ESP32 142 178 cal_sync.update(); 179 + 180 + // Track online status changes - trigger ripple when connected 181 + static bool previousOnlineStatus = false; 182 + bool currentOnlineStatus = cal_sync.isOnline(); 183 + if (currentOnlineStatus && !previousOnlineStatus) { 184 + doConnectedRipple(); 185 + } 186 + previousOnlineStatus = currentOnlineStatus; 143 187 144 188 static Point previouslyHeldButton = {(char)0xFF, (char)0xFF}; // 0xFF and 0xFF if no button is held 145 189 static uint16_t touchCount = 1; ··· 211 255 previouslyHeldButton.x = cal_touch.x; 212 256 previouslyHeldButton.y = cal_touch.y; 213 257 214 - // Bursts 258 + // Animations (bursts and ripples) 215 259 bool needsClearing = true; 216 - for (size_t i = 0; i < BURST_COUNT; i++) 217 - { 218 - // increment tick for active bursts 260 + 261 + // Process bursts 262 + for (size_t i = 0; i < BURST_COUNT; i++) { 219 263 Burst* burst = &bursts[i]; 220 264 if (burst->tick == UINT16_MAX) { 221 265 continue; 222 266 } 223 267 burst->tick++; 224 - 225 - // clear previous overrides if necessary 268 + 226 269 if (needsClearing) { 227 270 cal_lights.clearOverrideLEDs(); 228 271 needsClearing = false; 229 272 } 230 273 231 - // end burst when at max tick 232 274 if (burst->tick >= ANIM_MAX_TICK) { 233 275 burst->tick = UINT16_MAX; 234 276 continue; 235 277 } 236 278 237 - // enable the overrides for this tick 238 279 for (size_t a = 0; a < ANIM_COUNT; a++) { 239 280 const AnimPattern* anim = &ANIM[a]; 240 281 if (burst->tick < anim->tickStart || burst->tick >= anim->tickEnd) { 241 282 continue; 242 283 } 243 284 cal_lights.setOverrideLED(burst->p.x + anim->offset.x, burst->p.y + anim->offset.y, true); 285 + } 286 + } 287 + 288 + // Process ripples - dynamically calculate circle perimeter 289 + for (size_t i = 0; i < RIPPLE_COUNT; i++) { 290 + Ripple* ripple = &ripples[i]; 291 + if (ripple->startTime == 0) { 292 + continue; 293 + } 294 + 295 + unsigned long elapsed = millis() - ripple->startTime; 296 + 297 + // End ripple when animation complete 298 + if (elapsed >= RIPPLE_DURATION_MS) { 299 + ripple->startTime = 0; 300 + continue; 301 + } 302 + 303 + if (needsClearing) { 304 + cal_lights.clearOverrideLEDs(); 305 + needsClearing = false; 306 + } 307 + 308 + // Calculate current radius in scaled units (0.5 LED increments) 309 + uint8_t radius = elapsed / RIPPLE_MS_PER_RADIUS; 310 + 311 + // Calculate squared distance bounds for the ring 312 + // Ring width is ±1 LED = ±2 in scaled units 313 + int16_t innerRadius = (radius > 2) ? (radius - 2) : 0; 314 + int16_t outerRadius = radius + 2; 315 + int16_t innerRadiusSq = innerRadius * innerRadius; 316 + int16_t outerRadiusSq = outerRadius * outerRadius; 317 + 318 + // Check all LEDs using scaled coordinates (LED pos * 2) 319 + for (int8_t month = 0; month < 12; month++) { 320 + for (int8_t day = 0; day < 31; day++) { 321 + int16_t dx = (month * 2) - ripple->x; 322 + int16_t dy = (day * 2) - ripple->y; 323 + int16_t distSq = dx * dx + dy * dy; 324 + 325 + if (distSq >= innerRadiusSq && distSq <= outerRadiusSq) { 326 + cal_lights.setOverrideLED(month, day, true); 327 + } 328 + } 244 329 } 245 330 } 246 331 }