#include #include #include #include #include #include #include #include #include #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 #include #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 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(&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(&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(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 &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(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(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(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(); } }