514 lines
12 KiB
C++
514 lines
12 KiB
C++
#include <array>
|
|
#include <memory>
|
|
|
|
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <WiFiMulti.h>
|
|
#include <SPIFFS.h>
|
|
|
|
#include <ETH.h>
|
|
|
|
#include <FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
|
|
#include "WebServer.h"
|
|
#include "Fader.h"
|
|
#include "UDPProto.h"
|
|
#include "UpdateServer.h"
|
|
#include "Font.h"
|
|
#include "Config.h"
|
|
|
|
#include "Animation/AllAnimations.h"
|
|
#include "Animation/AnimationController.h"
|
|
|
|
#include <esp32_digital_led_lib.h>
|
|
#include <esp32_digital_led_funcs.h>
|
|
|
|
#include "coreids.h"
|
|
|
|
const uint32_t FRAME_INTERVAL_US = 16666;
|
|
const uint32_t NUM_STRIPS = 2;
|
|
const uint32_t NUM_LEDS = 150;
|
|
const uint32_t FLIP_STRIPS_MASK = 0x00000002;
|
|
|
|
std::array<strand_t, 1> STRANDS { // Avoid using any of the strapping pins on the ESP32, anything >=32, 16, 17... not much left.
|
|
strand_t {.rmtChannel = 0, .gpioNum = 4, .ledType = LED_SK6812W_V1, .brightLimit = 32, .numPixels = NUM_LEDS * NUM_STRIPS},
|
|
};
|
|
|
|
bool led_on = false;
|
|
|
|
size_t led_idx;
|
|
WiFiMulti wiFiMulti;
|
|
|
|
// this mutex is locked while LEDs are written and must be also locked
|
|
// by any task accessing the flash, as (probably) the interrupts
|
|
// from the flash cause the LED update to be unreliable.
|
|
SemaphoreHandle_t ledLockoutMutex;
|
|
|
|
Fader ledFader(NUM_STRIPS, NUM_LEDS, 1, FLIP_STRIPS_MASK);
|
|
UDPProto udpProto(&ledFader);
|
|
UpdateServer *updateServer;
|
|
|
|
AnimationController animController(&ledFader);
|
|
|
|
static bool eth_connected = false;
|
|
|
|
void WiFiEvent(WiFiEvent_t event)
|
|
{
|
|
switch (event) {
|
|
case SYSTEM_EVENT_ETH_START:
|
|
Serial.println("ETH Started");
|
|
//set eth hostname here
|
|
ETH.setHostname("zamusiclight");
|
|
break;
|
|
case SYSTEM_EVENT_ETH_CONNECTED:
|
|
Serial.println("ETH Connected");
|
|
break;
|
|
case SYSTEM_EVENT_ETH_GOT_IP:
|
|
Serial.print("ETH MAC: ");
|
|
Serial.print(ETH.macAddress());
|
|
Serial.print(", IPv4: ");
|
|
Serial.print(ETH.localIP());
|
|
if (ETH.fullDuplex()) {
|
|
Serial.print(", FULL_DUPLEX");
|
|
}
|
|
Serial.print(", ");
|
|
Serial.print(ETH.linkSpeed());
|
|
Serial.println("Mbps");
|
|
eth_connected = true;
|
|
break;
|
|
case SYSTEM_EVENT_ETH_DISCONNECTED:
|
|
Serial.println("ETH Disconnected");
|
|
eth_connected = false;
|
|
break;
|
|
case SYSTEM_EVENT_ETH_STOP:
|
|
Serial.println("ETH Stopped");
|
|
eth_connected = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool initLEDs()
|
|
{
|
|
/****************************************************************************
|
|
If you have multiple strands connected, but not all are in use, the
|
|
GPIO power-on defaults for the unused strand data lines will typically be
|
|
high-impedance. Unless you are pulling the data lines high or low via a
|
|
resistor, this will lead to noise on those unused but connected channels
|
|
and unwanted driving of those unallocated strands.
|
|
This optional gpioSetup() code helps avoid that problem programmatically.
|
|
****************************************************************************/
|
|
|
|
digitalLeds_initDriver();
|
|
|
|
for (int i = 0; i < STRANDS.size(); i++) {
|
|
gpioSetup(STRANDS[i].gpioNum, OUTPUT, LOW);
|
|
}
|
|
|
|
strand_t *strands[STRANDS.size()];
|
|
for (int i = 0; i < STRANDS.size(); i++)
|
|
{
|
|
strands[i] = &STRANDS[i];
|
|
}
|
|
int rc = digitalLeds_addStrands(strands, STRANDS.size());
|
|
if (rc)
|
|
{
|
|
Serial.print("Init rc = ");
|
|
Serial.println(rc);
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < STRANDS.size(); i++)
|
|
{
|
|
strand_t *pStrand = strands[i];
|
|
Serial.print("Strand ");
|
|
Serial.print(i);
|
|
Serial.print(" = ");
|
|
Serial.print((uint32_t)(pStrand->pixels), HEX);
|
|
Serial.println();
|
|
#if DEBUG_ESP32_DIGITAL_LED_LIB
|
|
dumpDebugBuffer(-2, digitalLeds_debugBuffer);
|
|
#endif
|
|
}
|
|
|
|
led_idx = 0;
|
|
return true;
|
|
}
|
|
|
|
enum LEDState {
|
|
STARTUP,
|
|
UDP,
|
|
SHOWIP,
|
|
ANIMATION,
|
|
TRANSITION,
|
|
TRANSITION_FADE2BLACK
|
|
};
|
|
|
|
static AnimationController::DefaultAnimation loadSavedAnimation(void)
|
|
{
|
|
size_t ret;
|
|
|
|
File f = SPIFFS.open("/dyn/last_anim");
|
|
if(!f) {
|
|
// error, but return something useful
|
|
goto readErr;
|
|
}
|
|
|
|
AnimationController::DefaultAnimation anim;
|
|
|
|
ret = f.readBytes(reinterpret_cast<char*>(&anim), sizeof(anim));
|
|
|
|
if(ret != sizeof(anim)) {
|
|
goto readErr;
|
|
}
|
|
|
|
f.close();
|
|
|
|
return anim;
|
|
|
|
readErr:
|
|
return AnimationController::FIRE_COLD;
|
|
}
|
|
|
|
static void updateSavedAnimation(AnimationController::DefaultAnimation newSavedAnim)
|
|
{
|
|
AnimationController::DefaultAnimation oldSavedAnim = loadSavedAnimation();
|
|
|
|
if(oldSavedAnim == newSavedAnim) {
|
|
return;
|
|
}
|
|
|
|
SPIFFS.mkdir("/dyn/");
|
|
File f = SPIFFS.open("/dyn/last_anim", "w");
|
|
|
|
if(!f) {
|
|
return;
|
|
}
|
|
|
|
size_t ret = f.write(reinterpret_cast<const uint8_t*>(&newSavedAnim), sizeof(newSavedAnim));
|
|
|
|
if(ret != sizeof(newSavedAnim)) {
|
|
// failed
|
|
}
|
|
|
|
f.close();
|
|
}
|
|
|
|
static void ledFSM(void)
|
|
{
|
|
static bool stateEntered = true;
|
|
static LEDState ledState = STARTUP;
|
|
static LEDState nextState;
|
|
|
|
static unsigned long lastUDPUpdate = 0;
|
|
|
|
static unsigned long stateEnteredTime = 0;
|
|
|
|
LEDState lastState = ledState;
|
|
|
|
switch(ledState) {
|
|
case STARTUP:
|
|
animController.loop();
|
|
|
|
if(animController.isIdle()) {
|
|
Bitmap bmp(16, 16);
|
|
String infoStr = "IP: " + WiFi.localIP().toString(); // + " - IPv6: " + WiFi.localIPv6().toString();
|
|
Font::textToBitmap(infoStr.c_str(), &bmp, Fader::Color{0, 0, 0, 32});
|
|
|
|
animController.changeAnimation(std::unique_ptr<Animation>(new ImageScrollerAnimation(&ledFader, &bmp, 5)), false);
|
|
ledState = SHOWIP;
|
|
}
|
|
break;
|
|
|
|
case SHOWIP:
|
|
if(stateEntered) {
|
|
animController.restart();
|
|
}
|
|
|
|
animController.loop();
|
|
|
|
if(((WiFi.status() == WL_CONNECTED) || (WiFi.getMode() == WIFI_MODE_AP)) &&
|
|
udpProto.check()) {
|
|
// UDP packet received -> transition to UDP state
|
|
nextState = UDP;
|
|
ledState = TRANSITION;
|
|
}
|
|
|
|
// change to last used animation after some time
|
|
if((millis() - stateEnteredTime) > 60000) {
|
|
// FIXME: does not work with transition because of restart() call in ANIMATION state
|
|
animController.changeAnimation(loadSavedAnimation(), false);
|
|
ledState = ANIMATION;
|
|
}
|
|
break;
|
|
|
|
case ANIMATION:
|
|
if(stateEntered) {
|
|
animController.restart();
|
|
}
|
|
|
|
animController.loop();
|
|
|
|
if(((WiFi.status() == WL_CONNECTED) || (WiFi.getMode() == WIFI_MODE_AP)) &&
|
|
udpProto.check()) {
|
|
// UDP packet received -> transition to UDP state
|
|
nextState = UDP;
|
|
ledState = TRANSITION;
|
|
}
|
|
|
|
if((animController.animationInitiator() == AnimationController::USER) &&
|
|
(animController.currentFrame() == 60*60)) { // FIXME: #define FPS and use everywhere
|
|
updateSavedAnimation(animController.lastDefaultAnimation());
|
|
}
|
|
break;
|
|
|
|
case UDP:
|
|
if((WiFi.status() != WL_CONNECTED) && (WiFi.getMode() != WIFI_MODE_AP)) {
|
|
// WiFi disconnected -> switch to animation immediately to show reconnect animation
|
|
ledState = ANIMATION;
|
|
break;
|
|
}
|
|
|
|
if(udpProto.loop()) {
|
|
lastUDPUpdate = millis();
|
|
} else if(millis() - lastUDPUpdate > 3000) {
|
|
// seems like no more UDP data arrives -> transition to ANIMATION state
|
|
nextState = ANIMATION;
|
|
ledState = TRANSITION;
|
|
}
|
|
|
|
ledFader.update();
|
|
break;
|
|
|
|
case TRANSITION:
|
|
if(stateEntered) {
|
|
animController.stop();
|
|
}
|
|
|
|
if(animController.isIdle()) {
|
|
ledState = TRANSITION_FADE2BLACK;
|
|
} else {
|
|
animController.loop();
|
|
}
|
|
break;
|
|
|
|
case TRANSITION_FADE2BLACK:
|
|
if(stateEntered) {
|
|
ledFader.set_fadestep(2);
|
|
ledFader.fade_color(Fader::Color{0, 0, 0, 0});
|
|
}
|
|
|
|
ledFader.update();
|
|
if(!ledFader.something_changed()) {
|
|
// transition finished
|
|
ledState = nextState;
|
|
lastUDPUpdate = millis();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if(lastState != ledState) {
|
|
stateEntered = true;
|
|
stateEnteredTime = millis();
|
|
} else {
|
|
stateEntered = false;
|
|
}
|
|
}
|
|
|
|
static void fineDelay(uint32_t us)
|
|
{
|
|
uint32_t ms = us / 1000;
|
|
us -= ms * 1000;
|
|
|
|
if(ms) {
|
|
delay(ms);
|
|
}
|
|
|
|
delayMicroseconds(us);
|
|
}
|
|
|
|
static void ledTask( void * parameter )
|
|
{
|
|
uint64_t frame = 0;
|
|
|
|
/* loop forever */
|
|
for(;;){
|
|
|
|
uint32_t start_time = micros();
|
|
|
|
ledFSM();
|
|
|
|
led_idx++;
|
|
if(led_idx >= STRANDS[0].numPixels) {
|
|
led_idx = 0;
|
|
}
|
|
|
|
strand_t *strands[STRANDS.size()];
|
|
for (int i = 0; i < STRANDS.size(); i++)
|
|
{
|
|
strands[i] = &STRANDS[i];
|
|
}
|
|
|
|
if(ledFader.something_changed()) {
|
|
const std::vector<Fader::Color> &colors = ledFader.get_color_values();
|
|
|
|
for (size_t i = 0; i < STRANDS[0].numPixels; i++) {
|
|
const Fader::Color &c = colors[i];
|
|
STRANDS[0].pixels[i] = pixelFromRGBW(c.r, c.g, c.b, c.w);
|
|
}
|
|
|
|
xSemaphoreTake(ledLockoutMutex, portMAX_DELAY);
|
|
digitalLeds_drawPixels(strands, 1);
|
|
xSemaphoreGive(ledLockoutMutex);
|
|
}
|
|
|
|
frame++;
|
|
|
|
uint32_t now = micros();
|
|
uint32_t duration = now - start_time;
|
|
|
|
if(frame % 60 == 0) {
|
|
uint32_t load = 100 * duration / FRAME_INTERVAL_US;
|
|
|
|
Serial.print("Duration: ");
|
|
Serial.print(duration);
|
|
Serial.print("μs → CPU Load: ");
|
|
Serial.print(load);
|
|
Serial.println(" %");
|
|
}
|
|
|
|
if(duration < FRAME_INTERVAL_US) {
|
|
fineDelay(FRAME_INTERVAL_US - duration);
|
|
}
|
|
}
|
|
/* delete a task when finish,
|
|
this will never happen because this is infinity loop */
|
|
vTaskDelete( NULL );
|
|
}
|
|
|
|
void wifi_setup(void)
|
|
{
|
|
Serial.println("Trying to connect...");
|
|
|
|
WiFi.setHostname("mlmini");
|
|
|
|
for(size_t tries = 0; tries < 10; tries++)
|
|
{
|
|
if(wiFiMulti.run() == WL_CONNECTED) {
|
|
animController.changeAnimation(std::unique_ptr<Animation>(new ConnectionEstablishedAnimation(&ledFader, true)));
|
|
|
|
Serial.println("");
|
|
Serial.println("WiFi connected");
|
|
Serial.println("IP address: ");
|
|
Serial.println(WiFi.localIP());
|
|
break;
|
|
}
|
|
|
|
led_on = !led_on;
|
|
digitalWrite(2, led_on);
|
|
delay(100);
|
|
|
|
Serial.print(".");
|
|
}
|
|
|
|
if(WiFi.status() != WL_CONNECTED) {
|
|
Serial.println("Connection failed, setting up access point...");
|
|
|
|
IPAddress apIP(192, 168, 42, 1);
|
|
|
|
WiFi.mode(WIFI_AP);
|
|
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
|
|
WiFi.softAP("🕯️💡☀️", "Licht234");
|
|
WiFi.enableAP(true);
|
|
|
|
animController.changeAnimation(std::unique_ptr<Animation>(new ConnectionEstablishedAnimation(&ledFader, false)));
|
|
}
|
|
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
pinMode(2, OUTPUT); // On-Board LED
|
|
|
|
Serial.begin(115200);
|
|
delay(10);
|
|
|
|
// initalize the semaphores before anything else
|
|
ledLockoutMutex = xSemaphoreCreateMutex();
|
|
|
|
if(!initLEDs()) {
|
|
Serial.println("LED setup failed!");
|
|
while(true) {
|
|
delay(100);
|
|
}
|
|
}
|
|
|
|
if(!SPIFFS.begin()) {
|
|
Serial.println("SPIFFS setup failed!");
|
|
while(true) {
|
|
delay(100);
|
|
}
|
|
}
|
|
|
|
// load configuration (especially crypto data)
|
|
Config::instance().load();
|
|
|
|
digitalWrite(2, HIGH);
|
|
|
|
Serial.println();
|
|
Serial.println();
|
|
|
|
animController.changeAnimation(std::unique_ptr<Animation>(new ConnectingAnimation(&ledFader)), false);
|
|
|
|
xTaskCreatePinnedToCore(
|
|
ledTask, /* Task function. */
|
|
"LED Task", /* name of task. */
|
|
10000, /* Stack size of task */
|
|
NULL, /* parameter of the task */
|
|
3, /* priority of the task */
|
|
NULL, /* Task handle to keep track of created task */
|
|
CORE_ID_LED);
|
|
|
|
WiFi.onEvent(WiFiEvent);
|
|
|
|
// Connect the WiFi network (or start an AP if that doesn't work)
|
|
for (auto &net : Config::instance().getWLANList())
|
|
{
|
|
Serial.print("Adding network ");
|
|
Serial.println(net.ssid.c_str());
|
|
wiFiMulti.addAP(net.ssid.c_str(), net.password.c_str());
|
|
}
|
|
|
|
wifi_setup();
|
|
|
|
ETH.begin();
|
|
|
|
// start the UDP server
|
|
udpProto.start(2703);
|
|
|
|
// start the web server
|
|
WebServer::instance().setFader(&ledFader);
|
|
WebServer::instance().setLedLockoutMutex(&ledLockoutMutex);
|
|
WebServer::instance().setAnimationController(&animController);
|
|
WebServer::instance().start();
|
|
|
|
// start the update server
|
|
updateServer = new UpdateServer(Config::instance().getCRPassword(), &ledLockoutMutex);
|
|
updateServer->start();
|
|
}
|
|
|
|
void loop() {
|
|
led_on = !led_on;
|
|
digitalWrite(2, led_on);
|
|
delay(500);
|
|
|
|
// reconnect WiFi when connection is lost
|
|
if(WiFi.status() == WL_CONNECTION_LOST) {
|
|
Serial.println("WLAN disconnected. Restarting setup.");
|
|
wifi_setup();
|
|
}
|
|
}
|