Compare commits

..

No commits in common. "master" and "1.3" have entirely different histories.
master ... 1.3

11 changed files with 109 additions and 394 deletions

View File

@ -1,15 +0,0 @@
## 2.1 - 2024-06-09
### Fixed
* MAC address on boot now displayed correctly.
## 2.0 - 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 +0,0 @@
BOARD = --board esp32:esp32:featheresp32
verify: wallart.ino
arduino --verify $(BOARD) $<
install: wallart.ino
arduino --upload $(BOARD) $<

View File

@ -21,41 +21,6 @@ form different ideas about what it's displaying.
That's cool.
Setup
-----
When you first plug it in,
you will see a yellow pattern with blue or red bars around it.
The pattern is your mac address.
If the bars are red and a pixel is flashing,
that means you need to set up WiFi.
You can also look at the back for a red LED.
If it's lit, you need to set up WiFi.
Get your phone or computer to connect to an access point
called "WallArt".
The password is "artsy fartsy", unless you changed it in the source code.
Once connected,
you should get a browser window that lets you connect.
If not, try going to http://neverssl.com/.
Please configure the clock before the WiFi.
This will set up your time zone,
so it doesn't blind you in the middle of the night.
You can clear the wifi information with a reset.
Reset
------
Plug the device in,
and connect GND to pin A0 (right next to GND).
The red LED on the Feather board should come on immediately,
indicating it needs the network set up again.
Network Server
--------------
@ -90,29 +55,15 @@ Clock
At night,
and sometimes during the day,
it displays something like a clock.
You will need to tell it your time zone.
It doesn't do daylight saving time, sorry.
I suggest you set it to standard time and pretend it's in sync with the sun.
* Each pixel in the top row is 1 hour (3600 seconds)
* Each pixel in the middle row is 5 minutes (300 seconds)
* Each pixel in the bottom row is 25 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
-----------------
python3 esptool.py --chip esp32 --port "/dev/ttyUSB0" --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x1000 wallart.ino.bootloader.bin 0x8000 wallart.ino.partitions.bin 0xe000 boot_app0.bin 0x10000 wallart.ino.bin
Philosophy
----------
@ -133,13 +84,3 @@ but if you want to make NeoPixel art,
think hard about what the end result should look like.
It's not enough to make a cool light show;
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
View File

@ -1,85 +0,0 @@
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

@ -1,60 +1,28 @@
#include <FastLED.h>
#include <WiFiManager.h>
#include <WiFiManagerTz.h>
#include <esp_wifi.h>
#include <esp_sntp.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include "network.h"
WiFiManager wfm;
void network_reset() {
Serial.println("Resetting network");
wfm.resetSettings();
}
bool time_was_accurate_once = false;
bool clock_is_set() {
return time_was_accurate_once;
}
void on_time_available(struct timeval *t) {
struct tm timeInfo;
getLocalTime(&timeInfo, 1000);
Serial.println(&timeInfo, "%A, %B %d %Y %H:%M:%S %Z %z ");
time_was_accurate_once = true;
}
void network_setup(char *password) {
String hostname = "WallArt";
String hostid = String(ESP.getEfuseMac(), HEX);
String hostname = "Wall Art " + hostid;
WiFiManagerNS::NTP::onTimeAvailable(&on_time_available);
WiFiManagerNS::init(&wfm, nullptr);
std::vector<const char *> menu = {"wifi", "info", "custom", "param", "sep", "update", "restart", "exit"};
wfm.setMenu(menu);
wfm.setConfigPortalBlocking(false);
wfm.setHostname(hostname);
wfm.autoConnect(hostname.c_str(), password);
pinMode(LED_BUILTIN, OUTPUT);
}
bool connected() {
return WiFi.status() == WL_CONNECTED;
}
bool timeConfigured = false;
void pause(uint32_t dwMs) {
if (connected() && !timeConfigured) {
WiFiManagerNS::configTime();
timeConfigured = true;
}
if (!digitalRead(RESET_PIN)) {
network_reset();
}
for (uint32_t t = 0; t < dwMs; t += 10) {
wfm.process();
digitalWrite(LED_BUILTIN, !connected());

View File

@ -1,11 +1,6 @@
#pragma once
// Short this to ground to reset the network
#define RESET_PIN 26
void network_reset();
void network_setup(char *password);
bool connected();
void pause(uint32_t dwMs);
void netget(int count);
bool clock_is_set();

View File

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

View File

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

View File

@ -1,29 +0,0 @@
#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

17
timezones.h Normal file
View File

@ -0,0 +1,17 @@
#include <Timezone.h>
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240};
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300};
Timezone TZ_US_Eastern(usEDT, usEST);
TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300};
TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360};
Timezone TZ_US_Central(usCDT, usCST);
TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360};
TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420};
Timezone TZ_US_Mountain(usMDT, usMST);
TimeChangeRule usPDT = {"EDT", Second, Sun, Mar, 2, -420};
TimeChangeRule usPST = {"EST", First, Sun, Nov, 2, -480};
Timezone TZ_US_Pacific(usPDT, usPST);

View File

@ -2,56 +2,60 @@
#include <ArduinoHttpClient.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <NTPClient.h>
#include <Time.h>
#include "durations.h"
#include "timezones.h"
#include "picker.h"
#include "network.h"
#include "settings.h"
#define VERSION 2
#define NEOPIXEL_PIN 32
#define GRIDLEN 64
#define WFM_PASSWORD "artsy fartsy"
#define TIMEZONE TZ_US_Mountain
/*
* 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 IMAGE_PULL_MIN_INTERVAL (5 * MINUTE)
CRGB grid[GRIDLEN];
CRGB actual[GRIDLEN];
// Rotation, in degrees: [0, 90, 180, 270]
int rotation = 0;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
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];
}
void setup() {
FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(grid, GRIDLEN);
// Maybe it's the plexiglass but for my build I really need to dial back the red
FastLED.setCorrection(0xc0ffff);
network_setup(WFM_PASSWORD);
}
bool updateTime() {
if (timeClient.update()) {
time_t now = timeClient.getEpochTime();
time_t local = TIMEZONE.toLocal(now);
setTime(local);
return true;
}
FastLED.show();
return false;
}
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) {
int reps = (cycles*GRIDLEN) + random(GRIDLEN);
int hue = random(256);
@ -60,7 +64,7 @@ void fade(int cycles = 2) {
uint8_t p = cm5xlat(8, (i+pos) % GRIDLEN);
grid[p] = CHSV(hue, 255, pos * 32);
}
show();
FastLED.show();
pause(80);
}
}
@ -68,7 +72,7 @@ void fade(int cycles = 2) {
void singleCursor(int count = 80) {
for (int i = 0; i < count; i++) {
grid[20] = CHSV(0, 210, 127 * (i%2));
show();
FastLED.show();
pause(120);
}
}
@ -83,7 +87,7 @@ void sparkle(int cycles=50) {
pos[j] = random(GRIDLEN);
grid[pos[j]] = CRGB::Gray;
}
show();
FastLED.show();
pause(40);
}
}
@ -118,7 +122,7 @@ void glitchPulse(int cycles=1000) {
grid[pos[i]] = c;
steps[i]--;
}
show();
FastLED.show();
pause(100);
}
}
@ -146,7 +150,7 @@ void conwayish(int cycles=5000) {
left[i]--;
}
}
show();
FastLED.show();
pause(20);
}
}
@ -175,46 +179,11 @@ void cm5(uint8_t width=0, int cycles=200) {
grid[xpos] = CHSV(0, 255, val);
}
}
show();
FastLED.show();
pause(500);
}
}
// Display MAC address at startup.
void displayMacAddress(int cycles=40) {
uint64_t addr = ESP.getEfuseMac();
Serial.println("mac=" + String(ESP.getEfuseMac(), HEX));
// Set some custom things per device.
// It would have been nice if doing this in the Access Point UI were easier than switching the MAC address.
switch (addr) {
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;
}
// Middle: MAC address
for (int i = 0; i < 48; i++) {
int pos = i + 8;
grid[pos] = CHSV(HUE_YELLOW, 255, ((addr>>(47-i)) & 1)?255:64);
}
// Bottom: connected status
fill_solid(grid+56, 8, connected() ? CRGB::Aqua : CRGB::Red);
show();
pause(250*MILLISECOND);
}
}
// Art from the network
int NetArtFrames = 0;
CRGB NetArt[8][GRIDLEN];
@ -226,7 +195,7 @@ void netart(int count=40) {
for (int i = 0; i < count; i++) {
memcpy(grid, NetArt[i%NetArtFrames], GRIDLEN*3);
show();
FastLED.show();
pause(500);
}
}
@ -238,52 +207,32 @@ uint8_t netgetStatus(uint8_t hue) {
positions[j] = random(GRIDLEN);
grid[positions[j]] = CHSV(hue, 255, 180);
}
show();
FastLED.show();
pause(500);
return hue;
}
void netget(int count=60) {
uint8_t hue = netgetStatus(HUE_BLUE);
static unsigned long nextPull = 0; // when to pull next
#if defined(ART_HOSTNAME) && defined(ART_PORT) && defined(ART_PATH)
if (millis() < nextPull) {
// Let's not bombard the server
hue = HUE_ORANGE;
} else if (connected()) {
if (connected()) {
WiFiClientSecure scli;
nextPull = millis() + IMAGE_PULL_MIN_INTERVAL;
hue = netgetStatus(HUE_AQUA);
scli.setInsecure();
HttpClient https(scli, ART_HOSTNAME, ART_PORT);
do {
String path = String(ART_PATH) + "?mac=" + String(ESP.getEfuseMac(), HEX);
Serial.println(path);
if (https.get(path) != 0) break;
if (https.get(ART_PATH) != 0) break;
hue = netgetStatus(HUE_GREEN);
if (https.skipResponseHeaders() != HTTP_SUCCESS) break;
hue = netgetStatus(HUE_YELLOW);
size_t readBytes = 0;
for (int i = 0; i < 12; i++) {
size_t artBytesLeft = sizeof(NetArt) - readBytes;
if (https.endOfBodyReached() || (artBytesLeft == 0)) {
hue = netgetStatus(HUE_ORANGE);
NetArtFrames = (readBytes / 3) / GRIDLEN;
break;
}
int l = https.read((uint8_t *)NetArt + readBytes, artBytesLeft);
if (-1 == l) {
break;
}
readBytes += l;
}
int artlen = https.read((uint8_t *)NetArt, sizeof(NetArt));
hue = netgetStatus(HUE_ORANGE);
NetArtFrames = (artlen / 3) / GRIDLEN;
} while(false);
https.stop();
}
@ -299,28 +248,25 @@ void spinner(int count=32) {
for (int i = 0; i < count; i++) {
int pos = spinner_pos[i % 4];
grid[pos] = CRGB::OliveDrab;
show();
FastLED.show();
pause(125);
grid[pos] = CRGB::Black;
}
}
void displayTimeDozenal(unsigned long duration = 20*SECOND) {
if (!clock_is_set()) return;
void displayTime(unsigned long duration = 20 * SECOND) {
if (timeStatus() != timeSet) return;
unsigned long end = millis() + duration;
clear();
FastLED.clear();
while (millis() < end) {
struct tm info;
getLocalTime(&info);
int hh = info.tm_hour;
int mmss = (info.tm_min * 60) + info.tm_sec;
updateTime();
int hh = hour();
int mmss = now() % 3600;
uint8_t hue = HUE_YELLOW;
// Top: Hours
if (hh >= 12) {
if (isPM()) {
hue = HUE_ORANGE;
hh -= 12;
}
@ -333,10 +279,10 @@ void displayTimeDozenal(unsigned long duration = 20*SECOND) {
// Outer: 5s
uint8_t s = (mmss/5) % 5;
grid[64 - 7 - 1] = CHSV(HUE_GREEN, 128, (s==1)?96:0);
grid[64 - 15 - 1] = CHSV(HUE_GREEN, 128, (s==2)?96:0);
grid[64 - 8 - 1] = CHSV(HUE_GREEN, 128, (s==3)?96:0);
grid[64 - 0 - 1] = CHSV(HUE_GREEN, 128, (s==4)?96:0);
grid[64 - 7 - 1] = CHSV(HUE_PURPLE, 128, (s==1)?96:0);
grid[64 - 15 - 1] = CHSV(HUE_PURPLE, 128, (s==2)?96:0);
grid[64 - 8 - 1] = CHSV(HUE_PURPLE, 128, (s==3)?96:0);
grid[64 - 0 - 1] = CHSV(HUE_PURPLE, 128, (s==4)?96:0);
for (int i = 0; i < 12; i++) {
// Omit first and last position on a row
@ -349,49 +295,34 @@ void displayTimeDozenal(unsigned long duration = 20*SECOND) {
grid[pos + 24] = CHSV(HUE_RED, 255, (i<mm)?128:48);
grid[pos + 48] = CHSV(HUE_PINK, 128, (i<ss)?96:48);
}
show();
FastLED.show();
pause(250 * MILLISECOND);
}
}
void setup() {
pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(19200);
FastLED.addLeds<WS2812, NEOPIXEL_PIN, GRB>(actual, GRIDLEN);
// Maybe it's the plexiglass, but for my build, I need to dial back the red
//FastLED.setCorrection(0xd0ffff);
network_setup(WFM_PASSWORD);
// Show our mac address, for debugging?
FastLED.setBrightness(DAY_BRIGHTNESS);
displayMacAddress();
sparkle();
}
void loop() {
Picker p;
uint8_t getprob = 4;
bool conn = connected();
bool day = true;
if (clock_is_set()) {
struct tm info;
getLocalTime(&info);
day = ((info.tm_hour >= DAY_BEGIN) && (info.tm_hour < DAY_END));
updateTime();
if (timeStatus() == timeSet) {
int hh = hour();
day = ((hh >= DAY_BEGIN) && (hh < DAY_END));
}
FastLED.setBrightness(day?DAY_BRIGHTNESS:NIGHT_BRIGHTNESS);
// At night, always display the clock
if (!day && clock_is_set()) {
displayTimeDigits(day);
return;
// If we don't yet have net art, try a little harder to get it.
if ((NetArtFrames == 0) || !conn) {
getprob = 16;
}
if (p.Pick(4) && clock_is_set()) {
displayTimeDigits(day, 2 * MINUTE);
} else if (p.Pick(4) && clock_is_set()) {
displayTimeDozenal();
} else if (p.Pick(4)) {
if (!day || p.Pick(4)) {
// At night, only ever show the clock
displayTime(2 * MINUTE);
} else if (p.Pick(getprob)) {
netget();
} else if (day && p.Pick(4)) {
// These can be hella bright