wallart

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

commit
7f1756e
parent
584dc77
author
Neale Pickett
date
2024-06-09 21:53:19 -0600 MDT
New numeric clock
9 files changed,  +243, -64
A CHANGELOG.md
+10, -0
 1@@ -0,0 +1,10 @@
 2+## v2 - 2024-06-09
 3+### Added
 4+* New Clock mode with arabic numerals
 5+* Firmware version now displayed at boot time
 6+* Rotation can be hardcoded based on MAC address.
 7+
 8+### Changed
 9+* Tweaked gamma adjustment for better yellows
10+* Makefile closer to something useful
11+* Updated library versions
M Makefile
+3, -3
 1@@ -1,8 +1,8 @@
 2-BOARD = --board adafruit:samd:adafruit_trinket_m0
 3+BOARD = --board esp32:esp32:featheresp32
 4 
 5-verify: main.ino
 6+verify: wallart.ino
 7 	arduino --verify $(BOARD) $<
 8 	
 9-install: main.ino
10+install: wallart.ino
11 	arduino --upload $(BOARD) $<
12 
M README.md
+20, -0
 1@@ -96,6 +96,16 @@ it displays something like a clock.
 2 * Each pixel in the bottom row is 25 seconds
 3 * There are four pixels around the bottom that move every 5 seconds
 4 
 5+Build Dependencies
 6+------------
 7+
 8+You'll need the following:
 9+
10+* esp32 boards: Arduino ESP32 Feather
11+* FastLED library
12+* WifiManagerTZ library (and its dependencies)
13+* ArduinoHttpClient library 
14+
15 
16 Updating Firmware
17 -----------------
18@@ -123,3 +133,13 @@ but if you want to make NeoPixel art,
19 think hard about what the end result should look like.
20 It's not enough to make a cool light show;
21 it has to make people wonder "what is that for?"
22+
23+
24+Apology
25+----------
26+
27+I am no longer a C++ programmer.
28+The structure of this code is awful.
29+I'm sorry.
30+I didn't feel like a 2-day refresher in a language I never use,
31+for code nobody else is likely to ever compile.
A clock.h
+85, -0
 1@@ -0,0 +1,85 @@
 2+const uint8_t digits4x4[5][4] = {
 3+    {
 4+        0b00100110,
 5+        0b00101010,
 6+        0b00101010,
 7+        0b00101100,
 8+    },
 9+    {
10+        0b11001110,
11+        0b01100010,
12+        0b00101000,
13+        0b11001110,
14+    },
15+    {
16+        0b11101010,
17+        0b10001010,
18+        0b00101110,
19+        0b11100010,
20+    },
21+    {
22+        0b11101000,
23+        0b00101100,
24+        0b00101010,
25+        0b00100110,
26+    },
27+    {
28+        0b11001110,
29+        0b10101010,
30+        0b01101110,
31+        0b00101110,
32+    }
33+};
34+
35+void digitDraw(int xoffset, int yoffset, int val, CRGB color) {
36+    for (int y=0; y<4; y++) {
37+        uint8_t row = digits4x4[val/2][y] >> (4*(val%2));
38+        for (int x=0; x<4; x++) {
39+            bool bit = (row>>(3-x)) & 1;
40+            int pos = (yoffset+y)*8 + (xoffset+x);
41+            if (bit) {
42+                grid[pos] = color;
43+            }
44+        }
45+    }
46+}
47+
48+void displayTimeDigits(bool day, unsigned long duration = 20*SECOND) {
49+    unsigned long end = millis() + duration;
50+
51+    bool flash = false;
52+
53+    while (millis() < end) {
54+        struct tm info;
55+        getLocalTime(&info);
56+
57+        int h0 = (info.tm_hour /  1) % 10;
58+        int h1 = (info.tm_hour / 10) % 10;
59+        int m0 = (info.tm_min /  1) % 10;
60+        int m1 = (info.tm_min / 10) % 10;
61+
62+        uint8_t hhue = day?HUE_AQUA:HUE_ORANGE;
63+        uint8_t mhue = day?HUE_ORANGE:HUE_RED;
64+
65+        // Draw background
66+        if (day) {
67+            fill_solid(grid, 32, CHSV(hhue, 120, 32));
68+            fill_solid(grid+32, 32, CHSV(mhue, 120, 32));
69+        } else {
70+            clear();
71+        }
72+        
73+        // Draw foreground
74+        CRGB hcolor = CHSV(hhue, 240, 120);
75+        CRGB mcolor = CHSV(mhue, 120, 120);
76+        digitDraw(0, 0, h1, hcolor);
77+        digitDraw(4, 0, h0, hcolor);
78+        digitDraw(0, 4, m1, mcolor);
79+        digitDraw(4, 4, m0, mcolor);
80+
81+        show();
82+        pause(SECOND);
83+
84+        flash = !flash;
85+    }
86+}
M network.cpp
+1, -1
1@@ -31,7 +31,7 @@ void network_setup(char *password) {
2   String hostname = "WallArt";
3 
4   WiFiManagerNS::NTP::onTimeAvailable(&on_time_available);
5-  WiFiManagerNS::init(&wfm);
6+  WiFiManagerNS::init(&wfm, nullptr);
7 
8   std::vector<const char *> menu = {"wifi", "info", "custom", "param", "sep", "update", "restart", "exit"};
9   wfm.setMenu(menu);
M picker.cpp
+4, -4
 1@@ -3,14 +3,14 @@
 2 #include "picker.h"
 3 
 4 Picker::Picker() {
 5-    val = random(1, 256);
 6+    this->val = random(1, 256);
 7 }
 8 
 9 bool Picker::Pick(uint8_t likelihood) {
10+    bool picked = false;
11     if ((val > 0) && (val <= likelihood)) {
12-        val = 0;
13-        return true;
14+        picked = true;
15     }
16     val -= likelihood;
17-    return false;
18+    return picked;
19 }
M picker.h
+1, -1
1@@ -7,5 +7,5 @@ public:
2     Picker();
3     bool Pick(uint8_t);
4 private:
5-    uint8_t val;
6+    int val;
7 };
A settings.h
+29, -0
 1@@ -0,0 +1,29 @@
 2+#pragma once
 3+
 4+/* 
 5+ * The hours when the day begins and ends.
 6+ * At night, all you get is a dim clock.
 7+ */
 8+#define DAY_BEGIN 6
 9+#define DAY_END 20
10+#define DAY_BRIGHTNESS 0x80
11+#define NIGHT_BRIGHTNESS 0x10
12+
13+/*
14+ * Define these to fetch from a wallart-server
15+ *
16+ * https://git.woozle.org/neale/wallart-server
17+ */
18+#define ART_HOSTNAME "www.woozle.org"
19+#define ART_PORT 443
20+#define ART_PATH "/wallart/wallart.bin"
21+
22+/*
23+ * The password used when running as an access point.
24+ */
25+#define WFM_PASSWORD "artsy fartsy"
26+
27+/*
28+ * The output pin your neopixel array is connected to.
29+ */
30+#define NEOPIXEL_PIN 32
M wallart.ino
+90, -55
  1@@ -6,34 +6,51 @@
  2 #include "durations.h"
  3 #include "picker.h"
  4 #include "network.h"
  5+#include "settings.h"
  6 
  7-#define NEOPIXEL_PIN 32
  8+#define VERSION 2
  9 #define GRIDLEN 64
 10-#define WFM_PASSWORD "artsy fartsy"
 11-
 12-/* 
 13- * The hours when the day begins and ends.
 14- * At night, all you get is a dim clock.
 15- */
 16-#define DAY_BEGIN 6
 17-#define DAY_END 20
 18-#define DAY_BRIGHTNESS 0x80
 19-#define NIGHT_BRIGHTNESS 0x10
 20-
 21-/*
 22- * Define these to fetch from a wallart-server
 23- *
 24- * https://git.woozle.org/neale/wallart-server
 25- */
 26-#define ART_HOSTNAME "www.woozle.org"
 27-#define ART_PORT 443
 28-#define ART_PATH "/wallart/wallart.bin"
 29 
 30 #define HTTPS_TIMEOUT (2 * SECOND)
 31 #define IMAGE_PULL_MIN_INTERVAL (5 * MINUTE)
 32 
 33-
 34 CRGB grid[GRIDLEN];
 35+CRGB actual[GRIDLEN];
 36+
 37+// Rotation, in degrees: [0, 90, 180, 270]
 38+int rotation = 0;
 39+
 40+void show() {
 41+  for (int y = 0; y < 8; y++) {
 42+    for (int x = 0; x < 8; x++) {
 43+      int pos;
 44+      switch (rotation) {
 45+        case 90:
 46+          pos = (x)*8 + (7-y);
 47+          break;
 48+        case 180:
 49+          pos = (7-y)*8 + (7-x);
 50+          break;
 51+        case 270:
 52+          pos = (7-x)*8 + y;
 53+          break;
 54+        default:
 55+          pos = (y)*8 + x;
 56+          break;
 57+      }
 58+      actual[pos] = grid[y*8 + x];
 59+    }
 60+  }
 61+  FastLED.show();
 62+}
 63+
 64+void clear() {
 65+  fill_solid(grid, GRIDLEN, CRGB::Black);
 66+}
 67+
 68+// I am so ashamed of this.
 69+// But C++ is a real pain for me at this point.
 70+#include "clock.h"
 71 
 72 void fade(int cycles = 2) {
 73   int reps = (cycles*GRIDLEN) + random(GRIDLEN);
 74@@ -43,7 +60,7 @@ void fade(int cycles = 2) {
 75       uint8_t p = cm5xlat(8, (i+pos) % GRIDLEN);
 76       grid[p] = CHSV(hue, 255, pos * 32);
 77     }
 78-    FastLED.show();
 79+    show();
 80     pause(80);
 81   }
 82 }
 83@@ -51,7 +68,7 @@ void fade(int cycles = 2) {
 84 void singleCursor(int count = 80) {
 85   for (int i = 0; i < count; i++) {
 86     grid[20] = CHSV(0, 210, 127 * (i%2));
 87-    FastLED.show();
 88+    show();
 89     pause(120);
 90   }
 91 }
 92@@ -66,7 +83,7 @@ void sparkle(int cycles=50) {
 93 			pos[j] = random(GRIDLEN);
 94 			grid[pos[j]] = CRGB::Gray;
 95 		}
 96-		FastLED.show();
 97+		show();
 98 		pause(40);
 99 	}
100 }
101@@ -101,7 +118,7 @@ void glitchPulse(int cycles=1000) {
102       grid[pos[i]] = c;
103       steps[i]--;
104     }
105-    FastLED.show();
106+    show();
107     pause(100);
108   }
109 }
110@@ -129,7 +146,7 @@ void conwayish(int cycles=5000) {
111         left[i]--;
112       }
113     }
114-    FastLED.show();
115+    show();
116     pause(20);
117   }
118 }
119@@ -158,27 +175,45 @@ void cm5(uint8_t width=0, int cycles=200) {
120         grid[xpos] = CHSV(0, 255, val);
121       }
122     }
123-    FastLED.show();
124+    show();
125     pause(500);
126   }
127 }
128 
129+// Display MAC address at startup.
130 void displayMacAddress(int cycles=40) {
131   uint64_t addr = ESP.getEfuseMac();
132 
133-  for (; cycles > 0; cycles -= 1) {
134-    bool conn = connected();
135+  Serial.println("mac=" + String(ESP.getEfuseMac(), HEX));
136+
137+  // Set some custom things per device.
138+  // It would have been nice if doing this in the Access Point UI were easier than switching the MAC address.
139+  switch (addr) {
140+    case 0x18fc1d519140:
141+      rotation = 270;
142+      break;
143+  }
144 
145-    fill_solid(grid, GRIDLEN, CHSV(conn?HUE_AQUA:HUE_RED, 128, 64));
146-    for (int i = 0; i < 48; i++) {
147-      int pos = i + 8;
148-      grid[pos] = CHSV(HUE_YELLOW, 255, ((addr>>(47-i)) & 1)?255:64);
149+  for (; cycles > 0; cycles -= 1) {
150+    // Top: version
151+    for (int i = 0; i < 8; i++) {
152+      bool bit = (VERSION>>i) & 1;
153+      grid[7-i] = bit ? CRGB::Black : CRGB::Aqua;
154     }
155-    grid[0] = CRGB::Black;
156-    if (!conn && (cycles % 2)) {
157-      grid[1] = CRGB::Black;
158+
159+    // Middle: MAC address
160+    for (int octet = 0; octet < 6; octet++) {
161+      for (int i = 0; i < 8; i++) {
162+        int pos = 8 + (octet*8) + (7-i);
163+        bool bit = (addr>>(octet*8 + i)) & 1;
164+        grid[pos] = bit ? CRGB::Yellow: CRGB::Black;
165+      }
166     }
167-    FastLED.show();
168+
169+    // Bottom: connected status
170+    fill_solid(grid+56, 8, connected() ? CRGB::Aqua : CRGB::Red);
171+
172+    show();
173     pause(250*MILLISECOND);
174   }
175 }
176@@ -194,7 +229,7 @@ void netart(int count=40) {
177 
178 	for (int i = 0; i < count; i++) {
179 		memcpy(grid, NetArt[i%NetArtFrames], GRIDLEN*3);
180-		FastLED.show();
181+		show();
182 		pause(500);
183 	}
184 }
185@@ -206,7 +241,7 @@ uint8_t netgetStatus(uint8_t hue) {
186 		positions[j] = random(GRIDLEN);
187 		grid[positions[j]] = CHSV(hue, 255, 180);
188 	}
189-	FastLED.show();
190+	show();
191 	pause(500);
192 	return hue;
193 }
194@@ -267,17 +302,17 @@ void spinner(int count=32) {
195 	for (int i = 0; i < count; i++) {
196 		int pos = spinner_pos[i % 4];
197 		grid[pos] = CRGB::OliveDrab;
198-		FastLED.show();
199+		show();
200 		pause(125);
201 		grid[pos] = CRGB::Black;
202 	}
203 }
204 
205-void displayTime(unsigned long duration = 20*SECOND) {
206+void displayTimeDozenal(unsigned long duration = 20*SECOND) {
207   if (!clock_is_set()) return;
208   unsigned long end = millis() + duration;
209 
210-  FastLED.clear();
211+  clear();
212 
213   while (millis() < end) {
214     struct tm info;
215@@ -317,7 +352,7 @@ void displayTime(unsigned long duration = 20*SECOND) {
216       grid[pos + 24] = CHSV(HUE_RED, 255, (i<mm)?128:48);
217       grid[pos + 48] = CHSV(HUE_PINK, 128, (i<ss)?96:48);
218     }
219-    FastLED.show();
220+    show();
221 
222     pause(250 * MILLISECOND);
223   }
224@@ -327,20 +362,19 @@ void setup() {
225   pinMode(RESET_PIN, INPUT_PULLUP);
226   pinMode(LED_BUILTIN, OUTPUT);
227   Serial.begin(19200);
228-  FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(grid, GRIDLEN);
229+  FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(actual, GRIDLEN);
230   // Maybe it's the plexiglass, but for my build, I need to dial back the red
231-  FastLED.setCorrection(0xd0ffff);
232+  //FastLED.setCorrection(0xd0ffff);
233   network_setup(WFM_PASSWORD);
234 
235   // Show our mac address, for debugging?
236+  FastLED.setBrightness(DAY_BRIGHTNESS);
237   displayMacAddress();
238   sparkle();
239 }
240 
241 void loop() {
242 	Picker p;
243-  uint8_t getprob = 4;
244-  bool conn = connected();
245   bool day = true;
246 
247   if (clock_is_set()) {
248@@ -350,16 +384,17 @@ void loop() {
249   }
250   FastLED.setBrightness(day?DAY_BRIGHTNESS:NIGHT_BRIGHTNESS);
251 
252-  // If we don't yet have net art, try a little harder to get it.
253-  if ((NetArtFrames == 0) || !conn) {
254-    getprob = 16;
255-  }
256-
257+  // At night, always display the clock
258   if (!day && clock_is_set()) {
259-    displayTime();
260+    displayTimeDigits(day);
261+    return;
262+  }
263+  
264+  if (p.Pick(4) && clock_is_set()) {
265+    displayTimeDigits(day, 2 * MINUTE);
266   } else if (p.Pick(4) && clock_is_set()) {
267-    displayTime(2 * MINUTE);
268-  } else if (p.Pick(getprob)) {
269+    displayTimeDozenal();
270+  } else if (p.Pick(4)) {
271     netget();
272   } else if (day && p.Pick(4)) {
273     // These can be hella bright