wallart

8x8 pixel display firmware
git clone https://git.woozle.org/neale/wallart.git

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}