New numeric clock

This commit is contained in:
Neale Pickett 2024-06-09 20:53:19 -07:00
parent 584dc77319
commit 7f1756eccc
9 changed files with 243 additions and 64 deletions

10
CHANGELOG.md Normal file
View File

@ -0,0 +1,10 @@
## v2 - 2024-06-09
### Added
* New Clock mode with arabic numerals
* Firmware version now displayed at boot time
* Rotation can be hardcoded based on MAC address.
### Changed
* Tweaked gamma adjustment for better yellows
* Makefile closer to something useful
* Updated library versions

View File

@ -1,8 +1,8 @@
BOARD = --board adafruit:samd:adafruit_trinket_m0 BOARD = --board esp32:esp32:featheresp32
verify: main.ino verify: wallart.ino
arduino --verify $(BOARD) $< arduino --verify $(BOARD) $<
install: main.ino install: wallart.ino
arduino --upload $(BOARD) $< arduino --upload $(BOARD) $<

View File

@ -96,6 +96,16 @@ it displays something like a clock.
* Each pixel in the bottom row is 25 seconds * Each pixel in the bottom row is 25 seconds
* There are four pixels around the bottom that move every 5 seconds * There are four pixels around the bottom that move every 5 seconds
Build Dependencies
------------
You'll need the following:
* esp32 boards: Arduino ESP32 Feather
* FastLED library
* WifiManagerTZ library (and its dependencies)
* ArduinoHttpClient library
Updating Firmware Updating Firmware
----------------- -----------------
@ -123,3 +133,13 @@ but if you want to make NeoPixel art,
think hard about what the end result should look like. think hard about what the end result should look like.
It's not enough to make a cool light show; It's not enough to make a cool light show;
it has to make people wonder "what is that for?" it has to make people wonder "what is that for?"
Apology
----------
I am no longer a C++ programmer.
The structure of this code is awful.
I'm sorry.
I didn't feel like a 2-day refresher in a language I never use,
for code nobody else is likely to ever compile.

85
clock.h Normal file
View File

@ -0,0 +1,85 @@
const uint8_t digits4x4[5][4] = {
{
0b00100110,
0b00101010,
0b00101010,
0b00101100,
},
{
0b11001110,
0b01100010,
0b00101000,
0b11001110,
},
{
0b11101010,
0b10001010,
0b00101110,
0b11100010,
},
{
0b11101000,
0b00101100,
0b00101010,
0b00100110,
},
{
0b11001110,
0b10101010,
0b01101110,
0b00101110,
}
};
void digitDraw(int xoffset, int yoffset, int val, CRGB color) {
for (int y=0; y<4; y++) {
uint8_t row = digits4x4[val/2][y] >> (4*(val%2));
for (int x=0; x<4; x++) {
bool bit = (row>>(3-x)) & 1;
int pos = (yoffset+y)*8 + (xoffset+x);
if (bit) {
grid[pos] = color;
}
}
}
}
void displayTimeDigits(bool day, unsigned long duration = 20*SECOND) {
unsigned long end = millis() + duration;
bool flash = false;
while (millis() < end) {
struct tm info;
getLocalTime(&info);
int h0 = (info.tm_hour / 1) % 10;
int h1 = (info.tm_hour / 10) % 10;
int m0 = (info.tm_min / 1) % 10;
int m1 = (info.tm_min / 10) % 10;
uint8_t hhue = day?HUE_AQUA:HUE_ORANGE;
uint8_t mhue = day?HUE_ORANGE:HUE_RED;
// Draw background
if (day) {
fill_solid(grid, 32, CHSV(hhue, 120, 32));
fill_solid(grid+32, 32, CHSV(mhue, 120, 32));
} else {
clear();
}
// Draw foreground
CRGB hcolor = CHSV(hhue, 240, 120);
CRGB mcolor = CHSV(mhue, 120, 120);
digitDraw(0, 0, h1, hcolor);
digitDraw(4, 0, h0, hcolor);
digitDraw(0, 4, m1, mcolor);
digitDraw(4, 4, m0, mcolor);
show();
pause(SECOND);
flash = !flash;
}
}

View File

@ -31,7 +31,7 @@ void network_setup(char *password) {
String hostname = "WallArt"; String hostname = "WallArt";
WiFiManagerNS::NTP::onTimeAvailable(&on_time_available); WiFiManagerNS::NTP::onTimeAvailable(&on_time_available);
WiFiManagerNS::init(&wfm); WiFiManagerNS::init(&wfm, nullptr);
std::vector<const char *> menu = {"wifi", "info", "custom", "param", "sep", "update", "restart", "exit"}; std::vector<const char *> menu = {"wifi", "info", "custom", "param", "sep", "update", "restart", "exit"};
wfm.setMenu(menu); wfm.setMenu(menu);

View File

@ -3,14 +3,14 @@
#include "picker.h" #include "picker.h"
Picker::Picker() { Picker::Picker() {
val = random(1, 256); this->val = random(1, 256);
} }
bool Picker::Pick(uint8_t likelihood) { bool Picker::Pick(uint8_t likelihood) {
bool picked = false;
if ((val > 0) && (val <= likelihood)) { if ((val > 0) && (val <= likelihood)) {
val = 0; picked = true;
return true;
} }
val -= likelihood; val -= likelihood;
return false; return picked;
} }

View File

@ -7,5 +7,5 @@ public:
Picker(); Picker();
bool Pick(uint8_t); bool Pick(uint8_t);
private: private:
uint8_t val; int val;
}; };

29
settings.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
/*
* The hours when the day begins and ends.
* At night, all you get is a dim clock.
*/
#define DAY_BEGIN 6
#define DAY_END 20
#define DAY_BRIGHTNESS 0x80
#define NIGHT_BRIGHTNESS 0x10
/*
* Define these to fetch from a wallart-server
*
* https://git.woozle.org/neale/wallart-server
*/
#define ART_HOSTNAME "www.woozle.org"
#define ART_PORT 443
#define ART_PATH "/wallart/wallart.bin"
/*
* The password used when running as an access point.
*/
#define WFM_PASSWORD "artsy fartsy"
/*
* The output pin your neopixel array is connected to.
*/
#define NEOPIXEL_PIN 32

View File

@ -6,34 +6,51 @@
#include "durations.h" #include "durations.h"
#include "picker.h" #include "picker.h"
#include "network.h" #include "network.h"
#include "settings.h"
#define NEOPIXEL_PIN 32 #define VERSION 2
#define GRIDLEN 64 #define GRIDLEN 64
#define WFM_PASSWORD "artsy fartsy"
/*
* The hours when the day begins and ends.
* At night, all you get is a dim clock.
*/
#define DAY_BEGIN 6
#define DAY_END 20
#define DAY_BRIGHTNESS 0x80
#define NIGHT_BRIGHTNESS 0x10
/*
* Define these to fetch from a wallart-server
*
* https://git.woozle.org/neale/wallart-server
*/
#define ART_HOSTNAME "www.woozle.org"
#define ART_PORT 443
#define ART_PATH "/wallart/wallart.bin"
#define HTTPS_TIMEOUT (2 * SECOND) #define HTTPS_TIMEOUT (2 * SECOND)
#define IMAGE_PULL_MIN_INTERVAL (5 * MINUTE) #define IMAGE_PULL_MIN_INTERVAL (5 * MINUTE)
CRGB grid[GRIDLEN]; CRGB grid[GRIDLEN];
CRGB actual[GRIDLEN];
// Rotation, in degrees: [0, 90, 180, 270]
int rotation = 0;
void show() {
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
int pos;
switch (rotation) {
case 90:
pos = (x)*8 + (7-y);
break;
case 180:
pos = (7-y)*8 + (7-x);
break;
case 270:
pos = (7-x)*8 + y;
break;
default:
pos = (y)*8 + x;
break;
}
actual[pos] = grid[y*8 + x];
}
}
FastLED.show();
}
void clear() {
fill_solid(grid, GRIDLEN, CRGB::Black);
}
// I am so ashamed of this.
// But C++ is a real pain for me at this point.
#include "clock.h"
void fade(int cycles = 2) { void fade(int cycles = 2) {
int reps = (cycles*GRIDLEN) + random(GRIDLEN); int reps = (cycles*GRIDLEN) + random(GRIDLEN);
@ -43,7 +60,7 @@ void fade(int cycles = 2) {
uint8_t p = cm5xlat(8, (i+pos) % GRIDLEN); uint8_t p = cm5xlat(8, (i+pos) % GRIDLEN);
grid[p] = CHSV(hue, 255, pos * 32); grid[p] = CHSV(hue, 255, pos * 32);
} }
FastLED.show(); show();
pause(80); pause(80);
} }
} }
@ -51,7 +68,7 @@ void fade(int cycles = 2) {
void singleCursor(int count = 80) { void singleCursor(int count = 80) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
grid[20] = CHSV(0, 210, 127 * (i%2)); grid[20] = CHSV(0, 210, 127 * (i%2));
FastLED.show(); show();
pause(120); pause(120);
} }
} }
@ -66,7 +83,7 @@ void sparkle(int cycles=50) {
pos[j] = random(GRIDLEN); pos[j] = random(GRIDLEN);
grid[pos[j]] = CRGB::Gray; grid[pos[j]] = CRGB::Gray;
} }
FastLED.show(); show();
pause(40); pause(40);
} }
} }
@ -101,7 +118,7 @@ void glitchPulse(int cycles=1000) {
grid[pos[i]] = c; grid[pos[i]] = c;
steps[i]--; steps[i]--;
} }
FastLED.show(); show();
pause(100); pause(100);
} }
} }
@ -129,7 +146,7 @@ void conwayish(int cycles=5000) {
left[i]--; left[i]--;
} }
} }
FastLED.show(); show();
pause(20); pause(20);
} }
} }
@ -158,27 +175,45 @@ void cm5(uint8_t width=0, int cycles=200) {
grid[xpos] = CHSV(0, 255, val); grid[xpos] = CHSV(0, 255, val);
} }
} }
FastLED.show(); show();
pause(500); pause(500);
} }
} }
// Display MAC address at startup.
void displayMacAddress(int cycles=40) { void displayMacAddress(int cycles=40) {
uint64_t addr = ESP.getEfuseMac(); uint64_t addr = ESP.getEfuseMac();
for (; cycles > 0; cycles -= 1) { Serial.println("mac=" + String(ESP.getEfuseMac(), HEX));
bool conn = connected();
fill_solid(grid, GRIDLEN, CHSV(conn?HUE_AQUA:HUE_RED, 128, 64)); // Set some custom things per device.
for (int i = 0; i < 48; i++) { // It would have been nice if doing this in the Access Point UI were easier than switching the MAC address.
int pos = i + 8; switch (addr) {
grid[pos] = CHSV(HUE_YELLOW, 255, ((addr>>(47-i)) & 1)?255:64); case 0x18fc1d519140:
rotation = 270;
break;
}
for (; cycles > 0; cycles -= 1) {
// Top: version
for (int i = 0; i < 8; i++) {
bool bit = (VERSION>>i) & 1;
grid[7-i] = bit ? CRGB::Black : CRGB::Aqua;
} }
grid[0] = CRGB::Black;
if (!conn && (cycles % 2)) { // Middle: MAC address
grid[1] = CRGB::Black; for (int octet = 0; octet < 6; octet++) {
for (int i = 0; i < 8; i++) {
int pos = 8 + (octet*8) + (7-i);
bool bit = (addr>>(octet*8 + i)) & 1;
grid[pos] = bit ? CRGB::Yellow: CRGB::Black;
}
} }
FastLED.show();
// Bottom: connected status
fill_solid(grid+56, 8, connected() ? CRGB::Aqua : CRGB::Red);
show();
pause(250*MILLISECOND); pause(250*MILLISECOND);
} }
} }
@ -194,7 +229,7 @@ void netart(int count=40) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
memcpy(grid, NetArt[i%NetArtFrames], GRIDLEN*3); memcpy(grid, NetArt[i%NetArtFrames], GRIDLEN*3);
FastLED.show(); show();
pause(500); pause(500);
} }
} }
@ -206,7 +241,7 @@ uint8_t netgetStatus(uint8_t hue) {
positions[j] = random(GRIDLEN); positions[j] = random(GRIDLEN);
grid[positions[j]] = CHSV(hue, 255, 180); grid[positions[j]] = CHSV(hue, 255, 180);
} }
FastLED.show(); show();
pause(500); pause(500);
return hue; return hue;
} }
@ -267,17 +302,17 @@ void spinner(int count=32) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int pos = spinner_pos[i % 4]; int pos = spinner_pos[i % 4];
grid[pos] = CRGB::OliveDrab; grid[pos] = CRGB::OliveDrab;
FastLED.show(); show();
pause(125); pause(125);
grid[pos] = CRGB::Black; grid[pos] = CRGB::Black;
} }
} }
void displayTime(unsigned long duration = 20*SECOND) { void displayTimeDozenal(unsigned long duration = 20*SECOND) {
if (!clock_is_set()) return; if (!clock_is_set()) return;
unsigned long end = millis() + duration; unsigned long end = millis() + duration;
FastLED.clear(); clear();
while (millis() < end) { while (millis() < end) {
struct tm info; struct tm info;
@ -317,7 +352,7 @@ void displayTime(unsigned long duration = 20*SECOND) {
grid[pos + 24] = CHSV(HUE_RED, 255, (i<mm)?128:48); grid[pos + 24] = CHSV(HUE_RED, 255, (i<mm)?128:48);
grid[pos + 48] = CHSV(HUE_PINK, 128, (i<ss)?96:48); grid[pos + 48] = CHSV(HUE_PINK, 128, (i<ss)?96:48);
} }
FastLED.show(); show();
pause(250 * MILLISECOND); pause(250 * MILLISECOND);
} }
@ -327,20 +362,19 @@ void setup() {
pinMode(RESET_PIN, INPUT_PULLUP); pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT); pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(19200); Serial.begin(19200);
FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(grid, GRIDLEN); FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(actual, GRIDLEN);
// Maybe it's the plexiglass, but for my build, I need to dial back the red // Maybe it's the plexiglass, but for my build, I need to dial back the red
FastLED.setCorrection(0xd0ffff); //FastLED.setCorrection(0xd0ffff);
network_setup(WFM_PASSWORD); network_setup(WFM_PASSWORD);
// Show our mac address, for debugging? // Show our mac address, for debugging?
FastLED.setBrightness(DAY_BRIGHTNESS);
displayMacAddress(); displayMacAddress();
sparkle(); sparkle();
} }
void loop() { void loop() {
Picker p; Picker p;
uint8_t getprob = 4;
bool conn = connected();
bool day = true; bool day = true;
if (clock_is_set()) { if (clock_is_set()) {
@ -350,16 +384,17 @@ void loop() {
} }
FastLED.setBrightness(day?DAY_BRIGHTNESS:NIGHT_BRIGHTNESS); FastLED.setBrightness(day?DAY_BRIGHTNESS:NIGHT_BRIGHTNESS);
// If we don't yet have net art, try a little harder to get it. // At night, always display the clock
if ((NetArtFrames == 0) || !conn) { if (!day && clock_is_set()) {
getprob = 16; displayTimeDigits(day);
return;
} }
if (!day && clock_is_set()) { if (p.Pick(4) && clock_is_set()) {
displayTime(); displayTimeDigits(day, 2 * MINUTE);
} else if (p.Pick(4) && clock_is_set()) { } else if (p.Pick(4) && clock_is_set()) {
displayTime(2 * MINUTE); displayTimeDozenal();
} else if (p.Pick(getprob)) { } else if (p.Pick(4)) {
netget(); netget();
} else if (day && p.Pick(4)) { } else if (day && p.Pick(4)) {
// These can be hella bright // These can be hella bright