#include #include #include #include #include #include #include #include "animation.h" #include "driver/temperature_sensor.h" // #define PHILLIPS_HUE_URL "https://192.168.0.20/clip/v2/resource/light/5557eceb-fd62-4859-b587-28f67e948969" #define PHILLIPS_HUE_URL "https://192.168.0.20/clip/v2/resource/light/" #define PHILLIPS_HUE_WZ_MITTE "5557eceb-fd62-4859-b587-28f67e948969" #define PHILLIPS_HUE_APPLICATION_KEY_NAME "hue-application-key" #define PHILLIPS_HUE_APPLICATION_KEY_VALUE "Xs4bbYSXvlPlNR7nKmpLpsLnLCbz42C4SrvO93Fz" #define PIN_NEO_PIXEL 2 // The ESP32 pin GPIO16 connected to NeoPixel #define NUM_PIXELS 400 // The number of LEDs (pixels) on NeoPixel LED strip #define COLLUMMS_PANEL 20 #define ROWS_PANEL 20 #define TARGET_UPDATE_RATE 1.0/30.0*1000.0 //Updaterate in ms #define ANIMATION_CYCLE_SPEED_MS 2000 //Switch animation every 20s #define FRAME_SIZE (COLLUMMS_PANEL * ROWS_PANEL * 3) // 1200 bytes per frame #define SD_CS 7 #define SD_MOSI 6 #define SD_MISO 5 #define SD_SCK 4 #define BUTTON 3 #define WebServerOn enum EntryType { COUNT_FOLDERS, COUNT_FILES }; enum displayType { DISPLAY_ANIMATION, DISPLAY_VIDEO, DISPLAY_PICTURE }; Adafruit_NeoPixel NeoPixel(NUM_PIXELS, PIN_NEO_PIXEL, NEO_GRB + NEO_KHZ800); String ServerResponseBody; String ServerResponseBody2; HTTPClient http; StaticJsonDocument<20000> doc; SPIClass spiSD(FSPI); // Use FSPI on ESP32 File myFile; temperature_sensor_handle_t temp_sensor = NULL; #ifdef WebServerOn WebServer server(80); #endif // const char* ssid = "FRITZ!Box 6490 Cable"; // const char* password = "29644657346255273822"; int serverFrameOverride = 0; const char* ssid = "Ununu-Schanze"; const char* password = "68838192089895491452"; void setup() { Serial.begin(115200); NeoPixel.begin(); // initialize NeoPixel strip object (REQUIRED) WiFi.begin(ssid, password); connectWifi(); //Connect to Wifi and show a progress bar on the matrix. Also returns IP-Address over Serial startSDCard(); //Tries to look for SD-Card and start SPI communication with it #ifdef WebServerOn //Activate WebServer and bind actions for requests (GET POST) server.on("/", HTTP_GET, handleRoot); server.on("/set", HTTP_POST, handleSet); server.on("/normal", HTTP_POST, handleNormal); server.begin(); #endif NeoPixel.fill(NeoPixel.Color(0,0,0),0,99); NeoPixel.setBrightness(50); NeoPixel.show(); pinMode(BUTTON, INPUT); // while(digitalRead(BUTTON)){ // displayAnimationFrame(millis(), 1, 0); // } // while(true){ // NeoPixel.fill(PhillipsHueColor(PHILLIPS_HUE_WZ_MITTE)); // NeoPixel.show(); // delay(200); // } // while(true){ // showManuRoom(); // } } // void loop() { // float bri = PhillipsHueBrightness(PHILLIPS_HUE_URL); // int on = PhillipsHueOn(PHILLIPS_HUE_URL); static int previousMillis = millis(); static displayType Type = DISPLAY_ANIMATION; server.handleClient();; if(!serverFrameOverride){ switch(Type){ case DISPLAY_ANIMATION: if(displayAnimationFrame(millis(), 1, 0) == 3){Type = DISPLAY_PICTURE;} break; case DISPLAY_VIDEO: if(updateVideoPlayback() == 3){Type = DISPLAY_ANIMATION;} break; case DISPLAY_PICTURE: if(updatePicturePlayback() == 3){Type = DISPLAY_VIDEO;} break; default: break; } }else{ parseJsonAndSetPixel(); } } int updatePicturePlayback() { static unsigned long previousMillis = -20000; static int maxFolders = -1; static int folderIndex = 0; static int frameIndex = 0; static bool playing = false; unsigned long currentMillis = millis(); // Respect update rate without blocking if (currentMillis - previousMillis < TARGET_UPDATE_RATE*30*5) { //every 20sec return 0; } previousMillis = currentMillis; // Initialize folder count once if (maxFolders == -1) { maxFolders = countTopLevelEntries(SD, "/pictures", COUNT_FOLDERS); folderIndex = 0; frameIndex = 0; playing = true; } if (folderIndex >= maxFolders) { // Restart from first folder (or stop if preferred) folderIndex = 0; return 3; } String pathToRead = getEntryByIndex(SD, "/pictures", folderIndex, COUNT_FOLDERS); // Try to read ONE frame only (non-blocking) bool frameExists = readEntryByPathSetPixelFrameStream( SD, pathToRead + "/frame.bin", frameIndex ); if (frameExists) { NeoPixel.show(); frameIndex++; } else { // Move to next folder when frames are done frameIndex = 0; folderIndex++; } // Button handling (non-blocking edge detect) static bool lastButtonState = HIGH; static int lastButtonStateTime = millis(); bool currentButtonState = digitalRead(BUTTON); if (lastButtonState == HIGH && currentButtonState == LOW) { // Button pressed delay(20); folderIndex++; frameIndex = 0; } lastButtonState = currentButtonState; lastButtonStateTime = millis(); } int updateVideoPlayback() { static unsigned long previousMillis = 0; static int maxFolders = -1; static int folderIndex = 0; static int frameIndex = 0; static bool playing = false; unsigned long currentMillis = millis(); // Respect update rate without blocking if (currentMillis - previousMillis < TARGET_UPDATE_RATE) { return 0; } previousMillis = currentMillis; // Initialize folder count once if (maxFolders == -1) { maxFolders = countTopLevelEntries(SD, "/videos", COUNT_FOLDERS); folderIndex = 0; frameIndex = 0; playing = true; } if (folderIndex >= maxFolders) { // Restart from first folder (or stop if preferred) folderIndex = 0; return 3; } String pathToRead = getEntryByIndex(SD, "/videos", folderIndex, COUNT_FOLDERS); // Try to read ONE frame only (non-blocking) bool frameExists = readEntryByPathSetPixelFrameStream( SD, pathToRead + "/frame.bin", frameIndex ); if (frameExists) { NeoPixel.show(); frameIndex++; } else { // Move to next folder when frames are done frameIndex = 0; folderIndex++; } // Button handling (non-blocking edge detect) static bool lastButtonState = HIGH; static int lastButtonStateTime = millis(); bool currentButtonState = digitalRead(BUTTON); if (lastButtonState == HIGH && currentButtonState == LOW) { // Button pressed delay(20); folderIndex++; frameIndex = 0; } lastButtonState = currentButtonState; lastButtonStateTime = millis(); } int displayAnimationFrame(int timeMs, int CycleAnimationOn, int AnimationIndex = 0){ uint32_t color; static int animation = AnimationIndex; static int prevtimeanimation = timeMs; if(!CycleAnimationOn){ animation = AnimationIndex; } for (int y = 0; y < ROWS_PANEL; y++) { for (int x = 0; x < COLLUMMS_PANEL; x++) { switch(animation){ case 0: animation++; break; case 1: color = bubbles(x, y, timeMs); break; case 2: color = galaxy(x, y, timeMs); break; case 3: animation++; break; case 4: color = flyingWireCubeHard(x, y, timeMs); break; case 5: color = pacman(x, y, timeMs); break; case 6: color = coolEffect(x, y, timeMs); break; case 7: color = creeper(x, y, timeMs); break; case 8: color = flappyBirdPixel(x, y, timeMs); break; case 9: color = xwingDeathStarPixel(x, y, timeMs); break; case 10: color = nyanCatPixel(x, y, timeMs); break; case 11: color = bonfireMoonPixel(x, y, timeMs); break; case 12: color = jumpingJackPixel(x, y, timeMs); break; case 13: color = spinningDavidStarPixel(x, y, timeMs); break; case 14: color = starrySky(x, y, timeMs); break; case 15: color = purpleRain(x, y, timeMs); break; case 16: color = amongUsPixel(x, y, timeMs); break; case 17: color = minecraftPumpkin(x, y, timeMs); break; default: animation = 0; return 3; break; } if(timeMs - prevtimeanimation >= ANIMATION_CYCLE_SPEED_MS){ animation++; prevtimeanimation = timeMs; } NeoPixel.setPixelColor(getPixelByCoordinate(x,y), color); } } NeoPixel.show(); return 0; } void showManuRoom(void){ for(int y = 0; y < ROWS_PANEL; y++){ for(int x = 0; x < COLLUMMS_PANEL; x++){ NeoPixel.setPixelColor(getPixelByCoordinate(x, y), NeoPixel.Color(imageManuBude[y][x] ? 255: 0, imageManuBude[y][x] ? 255: 0, imageManuBude[y][x] ? 255: 0)); } } NeoPixel.setPixelColor(getPixelByCoordinate(13,16), PhillipsHueColor("5557eceb-fd62-4859-b587-28f67e948969")); NeoPixel.setPixelColor(getPixelByCoordinate(13,17), PhillipsHueColor("5ccff572-baaa-4fa0-8998-d43e595e28c5")); NeoPixel.setPixelColor(getPixelByCoordinate(13,18), PhillipsHueColor("29cc7114-9f1f-44ba-baab-00718cd4d1f3")); NeoPixel.setPixelColor(getPixelByCoordinate(0,17), PhillipsHueColor("ccc6bced-f859-4a3d-8af2-7885f79d166c")); NeoPixel.setPixelColor(getPixelByCoordinate(2,17), PhillipsHueColor("76b2d794-657a-43ed-8f0e-f15c56e396e1")); NeoPixel.setPixelColor(getPixelByCoordinate(4,17), PhillipsHueColor("ccc6bced-f859-4a3d-8af2-7885f79d166c")); NeoPixel.setPixelColor(getPixelByCoordinate(11,7), PhillipsHueColor("e3c9d639-72df-415f-a579-2311dedbd2e7")); NeoPixel.setPixelColor(getPixelByCoordinate(11,8), PhillipsHueColor("acec0a2e-0755-4e79-8c77-a4e972d11149")); NeoPixel.setPixelColor(getPixelByCoordinate(10,4), PhillipsHueColorOnlyBrightness("563584d7-7252-45a0-9109-332f5714781d")); NeoPixel.setPixelColor(getPixelByCoordinate(10,3), PhillipsHueColorOnlyBrightness("72f09c6e-0a5b-4836-8bda-7249248c1e01")); NeoPixel.setPixelColor(getPixelByCoordinate(0,11), PhillipsHueColor("5a13afda-216a-4b41-bec2-96beaee94012")); NeoPixel.setPixelColor(getPixelByCoordinate(0,12), PhillipsHueColor("ee8b00b0-4969-4274-8e5e-df291b06c9f7")); NeoPixel.setPixelColor(getPixelByCoordinate(4,11), PhillipsHueColorOnlyBrightness("d95fa043-d067-48f5-8b3a-59ea650fe5c0")); NeoPixel.setPixelColor(getPixelByCoordinate(4,12), PhillipsHueColor("ee63ef6a-72dd-4a8e-a2d3-7f446a241699")); NeoPixel.setPixelColor(getPixelByCoordinate(4,13), PhillipsHueColorOnlyBrightness("eb7f0210-b132-46b1-a061-853efcc42cfb")); NeoPixel.show(); } int readEntryByPathSetPixelFrameStream(fs::FS &fs, const String &filePath, int frameIndex) { static uint8_t frameBuffer[FRAME_SIZE]; // preallocated buffer File file = fs.open(filePath); if(!file) { Serial.println("Could not open animation file on SD!"); return 0; } // Seek to the frame start if(!file.seek(frameIndex * FRAME_SIZE)) { Serial.println("Seek failed!"); file.close(); return 0; } // Read the frame into RAM size_t bytesRead = file.read(frameBuffer, FRAME_SIZE); file.close(); if(bytesRead != FRAME_SIZE) { Serial.println("Frame read incomplete!"); return 0; } // Set pixels int idx = 0; for(int y = 0; y < ROWS_PANEL; y++) { for(int x = 0; x < COLLUMMS_PANEL; x++) { uint8_t r = frameBuffer[idx++]; uint8_t g = frameBuffer[idx++]; uint8_t b = frameBuffer[idx++]; uint32_t color = NeoPixel.Color(r, g, b); NeoPixel.setPixelColor(getPixelByCoordinate(x, y), color); } } return 1; } int readEntryByPathSetPixelFrame(fs::FS &fs, String path){ int x = 0; int y = 0; uint8_t frameBuffer[COLLUMMS_PANEL*ROWS_PANEL*3]; File file = fs.open(path); if(!file){ Serial.println("Could not open file on SD!"); return 0; } size_t bytesRead = file.read(frameBuffer, COLLUMMS_PANEL*ROWS_PANEL*3); file.close(); if(bytesRead != COLLUMMS_PANEL*ROWS_PANEL*3){ Serial.println("Frame size mismatch!"); return 0; } int idx = 0; for(int y = 0; y < ROWS_PANEL; y++){ for(int x = 0; x < COLLUMMS_PANEL; x++){ uint8_t r = frameBuffer[idx++]; uint8_t g = frameBuffer[idx++]; uint8_t b = frameBuffer[idx++]; uint32_t color = NeoPixel.Color(r, g, b); NeoPixel.setPixelColor(getPixelByCoordinate(x, y), color); } } Serial.println("Read File!"); return 1; } int readEntryByPath(fs::FS &fs,String &data, String path){ File file = SD.open(path); if(file){ Serial.println("Reading File..."); data = ""; while(file.available()){ int temp = file.read(); Serial.write(temp); ServerResponseBody2 += (char)temp; } file.close(); Serial.println("Read File!"); return 1; }else{ Serial.println("Could not read data from SD-Card"); return 0; } } String getEntryByIndex(fs::FS &fs, String path, int index, EntryType type) { File root = fs.open(path); if (!root || !root.isDirectory()) return ""; int currentIndex = 0; File file = root.openNextFile(); while (file) { bool match = (type == COUNT_FOLDERS && file.isDirectory()) || (type == COUNT_FILES && !file.isDirectory()); if (match) { if (currentIndex == index) { return String(path) + "/" + file.name(); } currentIndex++; } file = root.openNextFile(); } return ""; // index out of range } int countTopLevelEntries(fs::FS &fs, String path, EntryType type) { File root = fs.open(path); if (!root || !root.isDirectory()) return 0; int count = 0; File file = root.openNextFile(); while (file) { if (type == COUNT_FOLDERS && file.isDirectory()) { count++; } else if (type == COUNT_FILES && !file.isDirectory()) { count++; } file = root.openNextFile(); } return count; } void startSDCard(){ spiSD.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); for(int i = 0; i < 20; i++){ if (!SD.begin(SD_CS, spiSD, 80000000)) { Serial.println("❌ SD Card Mount Failed " + String(i)); if (!SD.begin(SD_CS, spiSD, 80000000) && i == 19) { return; } } } Serial.println("✅ SD Card initialized."); } void connectWifi(){ int wifiSymbol[7][9] = {{0,0,1,1,1,1,1,0,0}, {0,1,0,0,0,0,0,1,0}, {1,0,0,0,0,0,0,0,1}, {0,0,0,1,1,1,0,0,0}, {0,0,1,0,0,0,1,0,0}, {0,0,0,0,0,0,0,0,0}, {0,0,0,0,1,0,0,0,0}}; while (WiFi.status() != WL_CONNECTED) { //Connect to WIFI static int cycles = 0; delay(50); Serial.print("."); NeoPixel.fill(NeoPixel.Color(0,0,0)); for(int x = 0; x < 9; x++){ for(int y = 0; y < 7; y++){ if(wifiSymbol[y][x] && (cycles%10 > 7-y)) NeoPixel.setPixelColor(getPixelByCoordinate(x+5, y+6), NeoPixel.Color(255,100,0)); } } NeoPixel.show(); cycles++; } Serial.println("\nConnected!"); Serial.println(WiFi.localIP()); //Success Animation for(int j = 0; j < 3; j++){ //3 Times for(int x = 0; x < 9; x++){ for(int y = 0; y < 7; y++){ if(wifiSymbol[y][x]) NeoPixel.setPixelColor(getPixelByCoordinate(x+5, y+6), NeoPixel.Color(0,255,0)); } } NeoPixel.show(); delay(300); NeoPixel.fill(NeoPixel.Color(0,0,0)); NeoPixel.show(); delay(300); } } float PhillipsHueBrightness(String ServiceLightID){ //return float in % (0-100) of current lightbrightness input: id of light to read http.begin(PHILLIPS_HUE_URL); http.addHeader(PHILLIPS_HUE_APPLICATION_KEY_NAME, PHILLIPS_HUE_APPLICATION_KEY_VALUE); //Add security key only received when pressing the link button Serial.println("Sending GET"); int ResponseCodeHttp = http.GET(); ServerResponseBody = http.getString(); // Serial.println("HTTP Response Code" + toString(ResponseCodeHttp)); deserializeJson(doc, ServerResponseBody); return (float)doc["data"][0]["dimming"]["brightness"]; } int PhillipsHueOn(String ServiceLightID){ //Returns 1 when light is switched on input: id of light to read http.begin(PHILLIPS_HUE_URL); http.addHeader(PHILLIPS_HUE_APPLICATION_KEY_NAME, PHILLIPS_HUE_APPLICATION_KEY_VALUE); //Add security key only received when pressing the link button Serial.println("Sending GET"); int ResponseCodeHttp = http.GET(); ServerResponseBody = http.getString(); // Serial.println("HTTP Response Code" + toString(ResponseCodeHttp)); deserializeJson(doc, ServerResponseBody); return doc["data"][0]["on"]["on"] == true ? 1 : 0; } uint32_t PhillipsHueColor(String ServiceLightID){ http.begin(PHILLIPS_HUE_URL+ServiceLightID); http.addHeader(PHILLIPS_HUE_APPLICATION_KEY_NAME, PHILLIPS_HUE_APPLICATION_KEY_VALUE); //Add security key only received when pressing the link button Serial.println("Sending GET"); int ResponseCodeHttp = http.GET(); ServerResponseBody = http.getString(); // Serial.println("HTTP Response Code" + toString(ResponseCodeHttp)); deserializeJson(doc, ServerResponseBody); float x = (float)doc["data"][0]["color"]["xy"]["x"]; float y = (float)doc["data"][0]["color"]["xy"]["y"]; float brightness = (float)doc["data"][0]["dimming"]["brightness"]; doc["data"][0]["on"]["on"] == false ? brightness = 0 : brightness = brightness; return ConvertHueToRGB888Neo(x, y, brightness); } uint32_t PhillipsHueColorOnlyBrightness(String ServiceLightID){ http.begin(PHILLIPS_HUE_URL+ServiceLightID); http.addHeader(PHILLIPS_HUE_APPLICATION_KEY_NAME, PHILLIPS_HUE_APPLICATION_KEY_VALUE); //Add security key only received when pressing the link button Serial.println("Sending GET"); int ResponseCodeHttp = http.GET(); ServerResponseBody = http.getString(); // Serial.println("HTTP Response Code" + toString(ResponseCodeHttp)); deserializeJson(doc, ServerResponseBody); float brightness = (float)doc["data"][0]["dimming"]["brightness"]; doc["data"][0]["on"]["on"] == false ? brightness = 0 : brightness = brightness; int bri = (int)(brightness * 255.0 / 100.0); return NeoPixel.Color(bri,(int)(bri*0.8),(int)(bri*0.7)); } uint32_t ConvertHueToRGB888Neo(float x, float y, float brightness){ float bri = brightness/100.0; float Y = bri; // Luminance float X = (Y / y) * x; float Z = (Y / y) * (1.0 - x - y); float r = 1.656492 * X - 0.354851 * Y - 0.255038 * Z; float g = -0.707196 * X + 1.655397 * Y + 0.036152 * Z; float b = 0.051713 * X - 0.121364 * Y + 1.011530 * Z; r = gammaCorrect(r); g = gammaCorrect(g); b = gammaCorrect(b); r = constrain(r, 0.0, 1.0); g = constrain(g, 0.0, 1.0); b = constrain(b, 0.0, 1.0); uint8_t R = (uint8_t)(r * 255); uint8_t G = (uint8_t)(g * 255); uint8_t B = (uint8_t)(b * 255); return NeoPixel.Color(R,G,B); } float gammaCorrect(float c) { if (c <= 0.0031308) return 12.92 * c; else return (1.0 + 0.055) * pow(c, 1.0 / 2.4) - 0.055; } //Used for WebApp void parseJsonAndSetPixel(){ int prevmil = millis(); deserializeJson(doc, ServerResponseBody2); Serial.println(millis()-prevmil); for(int frame = 0; frame < doc.size(); frame++){ for(int y = 0; y < 20/*doc[0].size()*/; y++){ for(int x = 0; x < 20/*doc[0][y].size()*/; x++){ NeoPixel.setPixelColor(getPixelByCoordinate(x, y), NeoPixel.Color(doc[frame][y][x][0], doc[frame][y][x][1], doc[frame][y][x][2])); Serial.print("Pixel "); Serial.print(getPixelByCoordinate(x, y)); Serial.print(" Red "); Serial.println((int)doc[frame][y][x][0]); } } NeoPixel.show(); delay(1); } } int getPixelByCoordinate(int x, int y){ //Starts from top left 0,0 int pixel = x+y*COLLUMMS_PANEL; if(x < COLLUMMS_PANEL/2 && y < ROWS_PANEL/2){ //TOPLEFT return pixel = x+y*COLLUMMS_PANEL/2; } else if (x >= COLLUMMS_PANEL/2 && y < ROWS_PANEL/2){ //TOPRIGHT return pixel = (x-10)+y*COLLUMMS_PANEL/2+100; } else if(x < COLLUMMS_PANEL/2 && y >= ROWS_PANEL/2){ //BOTLEFT return pixel = x+(y-10)*COLLUMMS_PANEL/2 + 300; } else{ //BOTRIGHT return pixel = (x-10)+(y-10)*COLLUMMS_PANEL/2 + 200; } } #ifdef WebServerOn void handleSet() { if (server.hasArg("plain") == false) { server.send(400, "text/plain", "No ServerResponseBody received"); return; } serverFrameOverride = 1; ServerResponseBody2 = server.arg("plain"); // RAW POST ServerResponseBody server.send(200, "application/json", "Server recieved data!"); Serial.println(ServerResponseBody2); parseJsonAndSetPixel(); } void handleNormal(){ serverFrameOverride = 0; server.send(200, "application/json", "Server recieved data!"); } #endif #ifdef WebServerOn void handleRoot() { server.send(200, "text/html", R"rawliteral( Multi-Frame RGB Pixel Editor

Multi-Frame RGB Pixel Editor

Frame 1 / 1
Previous
Pixel: —
🧽
)rawliteral"); } #endif