Neale Pickett
·
2024-06-09
wallart.ino
1#include <FastLED.h>
2#include <ArduinoHttpClient.h>
3#include <WiFiClientSecure.h>
4#include <WiFiUdp.h>
5#include <TimeLib.h>
6#include "durations.h"
7#include "picker.h"
8#include "network.h"
9#include "settings.h"
10
11#define VERSION 2
12#define GRIDLEN 64
13
14#define HTTPS_TIMEOUT (2 * SECOND)
15#define IMAGE_PULL_MIN_INTERVAL (5 * MINUTE)
16
17CRGB grid[GRIDLEN];
18CRGB actual[GRIDLEN];
19
20// Rotation, in degrees: [0, 90, 180, 270]
21int rotation = 0;
22
23void show() {
24 for (int y = 0; y < 8; y++) {
25 for (int x = 0; x < 8; x++) {
26 int pos;
27 switch (rotation) {
28 case 90:
29 pos = (x)*8 + (7-y);
30 break;
31 case 180:
32 pos = (7-y)*8 + (7-x);
33 break;
34 case 270:
35 pos = (7-x)*8 + y;
36 break;
37 default:
38 pos = (y)*8 + x;
39 break;
40 }
41 actual[pos] = grid[y*8 + x];
42 }
43 }
44 FastLED.show();
45}
46
47void clear() {
48 fill_solid(grid, GRIDLEN, CRGB::Black);
49}
50
51// I am so ashamed of this.
52// But C++ is a real pain for me at this point.
53#include "clock.h"
54
55void fade(int cycles = 2) {
56 int reps = (cycles*GRIDLEN) + random(GRIDLEN);
57 int hue = random(256);
58 for (int i = 0; i < reps; i++) {
59 for (int pos = 0; pos < 8; pos++) {
60 uint8_t p = cm5xlat(8, (i+pos) % GRIDLEN);
61 grid[p] = CHSV(hue, 255, pos * 32);
62 }
63 show();
64 pause(80);
65 }
66}
67
68void singleCursor(int count = 80) {
69 for (int i = 0; i < count; i++) {
70 grid[20] = CHSV(0, 210, 127 * (i%2));
71 show();
72 pause(120);
73 }
74}
75
76#define NUM_SPARKS 3
77void sparkle(int cycles=50) {
78 int pos[NUM_SPARKS] = {0};
79
80 for (int i = 0; i < cycles; i++) {
81 for (int j = 0; j < NUM_SPARKS; j++) {
82 grid[pos[j]] = CRGB::Black;
83 pos[j] = random(GRIDLEN);
84 grid[pos[j]] = CRGB::Gray;
85 }
86 show();
87 pause(40);
88 }
89}
90
91#define NUM_GLITCH 4
92#define GLITCH_FRAMES 64
93void glitchPulse(int cycles=1000) {
94 int steps[NUM_GLITCH] = {0};
95 int pos[NUM_GLITCH] = {0};
96 CRGB color[NUM_GLITCH];
97
98 for (int i = 0; i < NUM_GLITCH; i++) {
99 steps[i] = GLITCH_FRAMES / NUM_GLITCH * i;
100 color[i] = CRGB::Brown;
101 }
102
103 for (int frame = 0; frame < cycles; frame++) {
104 for (int i = 0; i < NUM_GLITCH; i++) {
105 if (steps[i] == 0) {
106 steps[i] = GLITCH_FRAMES;
107 pos[i] = random(GRIDLEN);
108 color[i] = CHSV(random(256), 64 + random(64), 255);
109 }
110 CRGB c = color[i];
111 int bmask = (0xff * steps[i] / 32) & 0xff;
112 if (steps[i] == GLITCH_FRAMES/2) {
113 bmask = 0xff - bmask;
114 }
115 c.red &= bmask;
116 c.green &= bmask;
117 c.blue &= bmask;
118 grid[pos[i]] = c;
119 steps[i]--;
120 }
121 show();
122 pause(100);
123 }
124}
125
126void conwayish(int cycles=5000) {
127 uint8_t total[GRIDLEN];
128 uint8_t left[GRIDLEN] = {0};
129 uint8_t hue = random(0, 64);
130
131 for (int i = 0; i < GRIDLEN; i++) {
132 total[i] = random(64, 256);
133 left[i] = total[i];
134 }
135
136 for (int frame = 0; frame < cycles; frame++) {
137 for (int i = 0; i < GRIDLEN; i++) {
138 if (left[i] == 0) {
139 left[i] = total[i];
140 if (grid[i].getLuma() == 0) {
141 grid[i].setHSV(hue, 180, 192);
142 } else {
143 grid[i] = CRGB::Black;
144 }
145 } else {
146 left[i]--;
147 }
148 }
149 show();
150 pause(20);
151 }
152}
153
154uint8_t cm5xlat(uint8_t width, uint8_t pos) {
155 if (width == 0) {
156 return pos;
157 }
158
159 uint8_t x = pos % width;
160 uint8_t y = pos / width;
161 uint8_t odd = y % 2;
162
163 return (y*width) + ((width-x-1)*odd) + (x*(1-odd));
164}
165
166void cm5(uint8_t width=0, int cycles=200) {
167 for (int frame = 0; frame < cycles; frame++) {
168 int val = 127 * random(2);
169 for (uint8_t pos = 0; pos < GRIDLEN; pos++) {
170 uint8_t xpos = cm5xlat(width, pos);
171 if (pos < GRIDLEN-1) {
172 uint8_t x2pos = cm5xlat(width, pos+1);
173 grid[xpos] = grid[x2pos];
174 } else {
175 grid[xpos] = CHSV(0, 255, val);
176 }
177 }
178 show();
179 pause(500);
180 }
181}
182
183// Display MAC address at startup.
184void displayMacAddress(int cycles=40) {
185 uint64_t addr = ESP.getEfuseMac();
186
187 Serial.println("mac=" + String(ESP.getEfuseMac(), HEX));
188
189 // Set some custom things per device.
190 // It would have been nice if doing this in the Access Point UI were easier than switching the MAC address.
191 switch (addr) {
192 case 0x18fc1d519140:
193 rotation = 270;
194 break;
195 }
196
197 for (; cycles > 0; cycles -= 1) {
198 // Top: version
199 for (int i = 0; i < 8; i++) {
200 bool bit = (VERSION>>i) & 1;
201 grid[7-i] = bit ? CRGB::Black : CRGB::Aqua;
202 }
203
204 // Middle: MAC address
205 for (int i = 0; i < 48; i++) {
206 int pos = i + 8;
207 grid[pos] = CHSV(HUE_YELLOW, 255, ((addr>>(47-i)) & 1)?255:64);
208 }
209
210 // Bottom: connected status
211 fill_solid(grid+56, 8, connected() ? CRGB::Aqua : CRGB::Red);
212
213 show();
214 pause(250*MILLISECOND);
215 }
216}
217
218// Art from the network
219int NetArtFrames = 0;
220CRGB NetArt[8][GRIDLEN];
221
222void netart(int count=40) {
223 if (NetArtFrames < 1) {
224 return;
225 }
226
227 for (int i = 0; i < count; i++) {
228 memcpy(grid, NetArt[i%NetArtFrames], GRIDLEN*3);
229 show();
230 pause(500);
231 }
232}
233
234uint8_t netgetStatus(uint8_t hue) {
235 static int positions[4] = {0};
236 for (int j = 0; j < 4; j++) {
237 grid[positions[j]] = CHSV(0, 0, 0);
238 positions[j] = random(GRIDLEN);
239 grid[positions[j]] = CHSV(hue, 255, 180);
240 }
241 show();
242 pause(500);
243 return hue;
244}
245
246void netget(int count=60) {
247 uint8_t hue = netgetStatus(HUE_BLUE);
248 static unsigned long nextPull = 0; // when to pull next
249
250#if defined(ART_HOSTNAME) && defined(ART_PORT) && defined(ART_PATH)
251 if (millis() < nextPull) {
252 // Let's not bombard the server
253 hue = HUE_ORANGE;
254 } else if (connected()) {
255 WiFiClientSecure scli;
256
257 nextPull = millis() + IMAGE_PULL_MIN_INTERVAL;
258
259 hue = netgetStatus(HUE_AQUA);
260 scli.setInsecure();
261
262 HttpClient https(scli, ART_HOSTNAME, ART_PORT);
263 do {
264 String path = String(ART_PATH) + "?mac=" + String(ESP.getEfuseMac(), HEX);
265 Serial.println(path);
266 if (https.get(path) != 0) break;
267 hue = netgetStatus(HUE_GREEN);
268
269 if (https.skipResponseHeaders() != HTTP_SUCCESS) break;
270 hue = netgetStatus(HUE_YELLOW);
271
272 size_t readBytes = 0;
273 for (int i = 0; i < 12; i++) {
274 size_t artBytesLeft = sizeof(NetArt) - readBytes;
275
276 if (https.endOfBodyReached() || (artBytesLeft == 0)) {
277 hue = netgetStatus(HUE_ORANGE);
278 NetArtFrames = (readBytes / 3) / GRIDLEN;
279 break;
280 }
281 int l = https.read((uint8_t *)NetArt + readBytes, artBytesLeft);
282 if (-1 == l) {
283 break;
284 }
285 readBytes += l;
286 }
287 } while(false);
288 https.stop();
289 }
290#endif
291
292 for (int i = 0; i < count; i++) {
293 netgetStatus(hue);
294 }
295}
296
297const int spinner_pos[4] = {27, 28, 36, 35};
298void spinner(int count=32) {
299 for (int i = 0; i < count; i++) {
300 int pos = spinner_pos[i % 4];
301 grid[pos] = CRGB::OliveDrab;
302 show();
303 pause(125);
304 grid[pos] = CRGB::Black;
305 }
306}
307
308void displayTimeDozenal(unsigned long duration = 20*SECOND) {
309 if (!clock_is_set()) return;
310 unsigned long end = millis() + duration;
311
312 clear();
313
314 while (millis() < end) {
315 struct tm info;
316 getLocalTime(&info);
317
318 int hh = info.tm_hour;
319 int mmss = (info.tm_min * 60) + info.tm_sec;
320 uint8_t hue = HUE_YELLOW;
321
322 // Top: Hours
323 if (hh >= 12) {
324 hue = HUE_ORANGE;
325 hh -= 12;
326 }
327
328 // Middle: 5m (300s)
329 uint8_t mm = (mmss/300) % 12;
330
331 // Bottom: 25s
332 uint8_t ss = (mmss/25) % 12;
333
334 // Outer: 5s
335 uint8_t s = (mmss/5) % 5;
336 grid[64 - 7 - 1] = CHSV(HUE_GREEN, 128, (s==1)?96:0);
337 grid[64 - 15 - 1] = CHSV(HUE_GREEN, 128, (s==2)?96:0);
338 grid[64 - 8 - 1] = CHSV(HUE_GREEN, 128, (s==3)?96:0);
339 grid[64 - 0 - 1] = CHSV(HUE_GREEN, 128, (s==4)?96:0);
340
341 for (int i = 0; i < 12; i++) {
342 // Omit first and last position on a row
343 int pos = i + 1;
344 if (pos > 6) {
345 pos += 2;
346 }
347
348 grid[pos + 0] = CHSV(hue, 255, (i<hh)?128:48);
349 grid[pos + 24] = CHSV(HUE_RED, 255, (i<mm)?128:48);
350 grid[pos + 48] = CHSV(HUE_PINK, 128, (i<ss)?96:48);
351 }
352 show();
353
354 pause(250 * MILLISECOND);
355 }
356}
357
358void setup() {
359 pinMode(RESET_PIN, INPUT_PULLUP);
360 pinMode(LED_BUILTIN, OUTPUT);
361 Serial.begin(19200);
362 FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(actual, GRIDLEN);
363 // Maybe it's the plexiglass, but for my build, I need to dial back the red
364 //FastLED.setCorrection(0xd0ffff);
365 network_setup(WFM_PASSWORD);
366
367 // Show our mac address, for debugging?
368 FastLED.setBrightness(DAY_BRIGHTNESS);
369 displayMacAddress();
370 sparkle();
371}
372
373void loop() {
374 Picker p;
375 bool day = true;
376
377 if (clock_is_set()) {
378 struct tm info;
379 getLocalTime(&info);
380 day = ((info.tm_hour >= DAY_BEGIN) && (info.tm_hour < DAY_END));
381 }
382 FastLED.setBrightness(day?DAY_BRIGHTNESS:NIGHT_BRIGHTNESS);
383
384 // At night, always display the clock
385 if (!day && clock_is_set()) {
386 displayTimeDigits(day);
387 return;
388 }
389
390 if (p.Pick(4) && clock_is_set()) {
391 displayTimeDigits(day, 2 * MINUTE);
392 } else if (p.Pick(4) && clock_is_set()) {
393 displayTimeDozenal();
394 } else if (p.Pick(4)) {
395 netget();
396 } else if (day && p.Pick(4)) {
397 // These can be hella bright
398 netart();
399 } else if (p.Pick(1)) {
400 fade();
401 singleCursor(20);
402 } else if (p.Pick(1)) {
403 sparkle();
404 } else if (p.Pick(4)) {
405 singleCursor();
406 } else if (p.Pick(8)) {
407 conwayish();
408 } else if (p.Pick(8)) {
409 glitchPulse();
410 } else if (p.Pick(2)) {
411 cm5(0);
412 } else if (p.Pick(2)) {
413 cm5(8);
414 } else if (p.Pick(2)) {
415 cm5(16);
416 }
417}