#include #include #include "MatrixDisplayUi.h" #include #include "icons.h" #include "Globals.h" #include #include "PeripheryManager.h" #include "MQTTManager.h" #include "GifPlayer.h" #include #include "Functions.h" #include "ServerManager.h" #include "MenuManager.h" #include "Apps.h" #include "Dictionary.h" #include #include "GifPlayer.h" Ticker AlarmTicker; Ticker TimerTicker; #ifdef ULANZI #define MATRIX_PIN 32 #else #define MATRIX_PIN D2 #endif #define MATRIX_WIDTH 32 #define MATRIX_HEIGHT 8 fs::File gifFile; GifPlayer gif; uint16_t gifX, gifY; CRGB leds[MATRIX_WIDTH * MATRIX_HEIGHT]; FastLED_NeoMatrix *matrix = new FastLED_NeoMatrix(leds, 8, 8, 4, 1, NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_PROGRESSIVE); MatrixDisplayUi *ui = new MatrixDisplayUi(matrix); uint8_t lastBrightness; DisplayManager_ &DisplayManager_::getInstance() { static DisplayManager_ instance; return instance; } DisplayManager_ &DisplayManager = DisplayManager.getInstance(); void DisplayManager_::setBrightness(uint8_t bri) { if (MATRIX_OFF && !ALARM_ACTIVE) { matrix->setBrightness(0); } else { matrix->setBrightness(bri); } } void DisplayManager_::setFPS(uint8_t fps) { ui->setTargetFPS(fps); } void DisplayManager_::setTextColor(uint16_t color) { matrix->setTextColor(color); } void DisplayManager_::MatrixState(bool on) { MATRIX_OFF = !on; setBrightness(BRIGHTNESS); } bool DisplayManager_::setAutoTransition(bool active) { if (ui->AppCount < 2) { ui->disablesetAutoTransition(); return false; } if (active && AUTO_TRANSITION) { ui->enablesetAutoTransition(); return true; } else { ui->disablesetAutoTransition(); return false; } showGif = false; } void DisplayManager_::drawJPG(uint16_t x, uint16_t y, fs::File jpgFile) { TJpgDec.drawFsJpg(x, y, jpgFile); } void DisplayManager_::drawBMP(int16_t x, int16_t y, const uint16_t bitmap[], int16_t w, int16_t h) { matrix->drawRGBBitmap(y, x, bitmap, w, h); } void DisplayManager_::applyAllSettings() { ui->setTargetFPS(MATRIX_FPS); ui->setTimePerApp(TIME_PER_APP); ui->setTimePerTransition(TIME_PER_TRANSITION); setBrightness(BRIGHTNESS); setTextColor(TEXTCOLOR_565); setAutoTransition(AUTO_TRANSITION); } void DisplayManager_::resetTextColor() { matrix->setTextColor(TEXTCOLOR_565); } void DisplayManager_::clearMatrix() { matrix->clear(); matrix->show(); } bool jpg_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { uint16_t bitmapIndex = 0; for (uint16_t row = 0; row < h; row++) { for (uint16_t col = 0; col < w; col++) { matrix->drawPixel(x + col, y + row, bitmap[bitmapIndex++]); } } return 0; } void DisplayManager_::printText(int16_t x, int16_t y, const char *text, bool centered, byte textCase) { if (centered) { uint16_t textWidth = getTextWidth(text, textCase); int16_t textX = ((32 - textWidth) / 2); matrix->setCursor(textX, y); } else { matrix->setCursor(x, y); } if ((UPPERCASE_LETTERS && textCase == 0) || textCase == 1) { size_t length = strlen(text); char upperText[length + 1]; // +1 for the null terminator for (size_t i = 0; i < length; ++i) { upperText[i] = toupper(text[i]); } upperText[length] = '\0'; // Null terminator matrix->print(upperText); } else { matrix->print(text); } } void DisplayManager_::HSVtext(int16_t x, int16_t y, const char *text, bool clear, byte textCase) { if (clear) matrix->clear(); static uint8_t hueOffset = 0; uint16_t xpos = 0; for (uint16_t i = 0; i < strlen(text); i++) { uint8_t hue = map(i, 0, strlen(text), 0, 255) + hueOffset; uint32_t textColor = hsvToRgb(hue, 255, 255); matrix->setTextColor(textColor); const char *myChar = &text[i]; matrix->setCursor(xpos + x, y); if ((UPPERCASE_LETTERS && textCase == 0) || textCase == 1) { matrix->print((char)toupper(text[i])); } else { matrix->print(&text[i]); } char temp_str[2] = {'\0', '\0'}; temp_str[0] = text[i]; xpos += getTextWidth(temp_str, textCase); } hueOffset++; if (clear) matrix->show(); } void pushCustomApp(String name, int position) { if (customApps.count(name) == 0) { ++customPagesCount; void (*customApps[20])(FastLED_NeoMatrix *, MatrixDisplayUiState *, int16_t, int16_t, bool, bool, GifPlayer *) = {CApp1, CApp2, CApp3, CApp4, CApp5, CApp6, CApp7, CApp8, CApp9, CApp10, CApp11, CApp12, CApp13, CApp14, CApp15, CApp16, CApp17, CApp18, CApp19, CApp20}; if (position < 0) // Insert at the end of the vector { Apps.push_back(std::make_pair(name, customApps[customPagesCount])); } else if (position < Apps.size()) // Insert at a specific position { Apps.insert(Apps.begin() + position, std::make_pair(name, customApps[customPagesCount])); } else // Invalid position, Insert at the end of the vector { Apps.push_back(std::make_pair(name, customApps[customPagesCount])); } ui->setApps(Apps); // Add Apps DisplayManager.getInstance().setAutoTransition(true); } } void removeCustomAppFromApps(const String &name) { auto it = std::find_if(Apps.begin(), Apps.end(), [&name](const std::pair &appPair) { return appPair.first == name; }); Apps.erase(it); ui->setApps(Apps); } void DisplayManager_::generateCustomPage(const String &name, const char *json) { if (strcmp(json, "") == 0 && customApps.count(name)) { Serial.println("delete"); customApps.erase(customApps.find(name)); removeCustomAppFromApps(name); showGif = false; return; } DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, json); if (error) { showGif = false; doc.clear(); return; } CustomApp customApp; if (customApps.find(name) != customApps.end()) { customApp = customApps[name]; } if (doc.containsKey("sound")) { #ifdef ULANZI customApp.sound = ("/" + doc["sound"].as() + ".txt"); #else customApp.sound = doc["sound"].as(); #endif } else { customApp.sound = ""; } if (doc.containsKey("bar")) { JsonArray barData = doc["bar"]; int i = 0; for (JsonVariant v : barData) { if (i >= 16) { break; } customApp.barData[i] = v.as(); i++; } customApp.barSize = i; } else { customApp.barSize = 0; } customApp.duration = doc.containsKey("duration") ? doc["duration"].as() * 1000 : 0; int pos = doc.containsKey("pos") ? doc["pos"].as() : -1; customApp.rainbow = doc.containsKey("rainbow") ? doc["rainbow"] : false; customApp.pushIcon = doc.containsKey("pushIcon") ? doc["pushIcon"] : 0; customApp.textCase = doc.containsKey("textCase") ? doc["textCase"] : 0; customApp.name = name; customApp.text = doc.containsKey("text") ? utf8ascii(doc["text"].as()) : ""; if (doc.containsKey("color")) { auto color = doc["color"]; if (color.is()) { customApp.color = hexToRgb565(color.as()); } else if (color.is() && color.size() == 3) { uint8_t r = color[0]; uint8_t g = color[1]; uint8_t b = color[2]; customApp.color = (r << 11) | (g << 5) | b; } else { customApp.color = TEXTCOLOR_565; } } else { customApp.color = TEXTCOLOR_565; } if (currentCustomApp != name) { customApp.scrollposition = 9; } customApp.repeat = doc.containsKey("repeat") ? doc["repeat"].as() : -1; if (doc.containsKey("icon")) { String iconFileName = String(doc["icon"].as()); if (customApp.icon && String(customApp.icon.name()).startsWith(iconFileName)) { } else { if (customApp.icon) { customApp.icon.close(); } if (LittleFS.exists("/ICONS/" + iconFileName + ".jpg")) { customApp.isGif = false; customApp.icon = LittleFS.open("/ICONS/" + iconFileName + ".jpg"); } else if (LittleFS.exists("/ICONS/" + iconFileName + ".gif")) { customApp.isGif = true; customApp.icon = LittleFS.open("/ICONS/" + iconFileName + ".gif"); } else { fs::File nullPointer; customApp.icon = nullPointer; } } } else { fs::File nullPointer; customApp.icon = nullPointer; } pushCustomApp(name, pos - 1); customApps[name] = customApp; doc.clear(); } void DisplayManager_::generateNotification(const char *json) { StaticJsonDocument<1024> doc; deserializeJson(doc, json); notify.duration = doc.containsKey("duration") ? doc["duration"].as() * 1000 : TIME_PER_APP; notify.text = doc.containsKey("text") ? utf8ascii(doc["text"].as()) : ""; notify.repeat = doc.containsKey("repeat") ? doc["repeat"].as() : -1; notify.rainbow = doc.containsKey("rainbow") ? doc["rainbow"].as() : false; notify.hold = doc.containsKey("hold") ? doc["hold"].as() : false; notify.pushIcon = doc.containsKey("pushIcon") ? doc["pushIcon"] : 0; notify.textCase = doc.containsKey("textCase") ? doc["textCase"] : 0; notify.startime = millis(); notify.scrollposition = 9; notify.iconWasPushed = false; notify.iconPosition = 0; if (doc.containsKey("sound")) { #ifdef ULANZI PeripheryManager.playFromFile("/MELODIES/" + doc["sound"].as() + ".txt"); #else PeripheryManager.playFromFile(doc["sound"].as()); #endif } if (doc.containsKey("bar")) { JsonArray barData = doc["bar"]; int i = 0; for (JsonVariant v : barData) { if (i >= 16) { break; } notify.barData[i] = v.as(); i++; } notify.barSize = i; } else { notify.barSize = 0; } if (doc.containsKey("color")) { auto color = doc["color"]; if (color.is()) { notify.color = hexToRgb565(color.as()); } else if (color.is() && color.size() == 3) { uint8_t r = color[0]; uint8_t g = color[1]; uint8_t b = color[2]; notify.color = (r << 11) | (g << 5) | b; } else { notify.color = TEXTCOLOR_565; } } else { notify.color = TEXTCOLOR_565; } notify.flag = true; showGif = false; if (doc.containsKey("icon")) { String iconFileName = doc["icon"].as(); if (LittleFS.exists("/ICONS/" + iconFileName + ".jpg")) { notify.isGif = false; notify.icon = LittleFS.open("/ICONS/" + iconFileName + ".jpg"); return; } else if (LittleFS.exists("/ICONS/" + iconFileName + ".gif")) { notify.isGif = true; notify.icon = LittleFS.open("/ICONS/" + iconFileName + ".gif"); return; } else { fs::File nullPointer; notify.icon = nullPointer; } } else { fs::File nullPointer; notify.icon = nullPointer; } } void DisplayManager_::loadNativeApps() { // Define a helper function to check and update an app auto updateApp = [&](const String &name, AppCallback callback, bool show, size_t position) { auto it = std::find_if(Apps.begin(), Apps.end(), [&](const std::pair &app) { return app.first == name; }); if (it != Apps.end()) { if (!show) { Apps.erase(it); } } else { if (show) { if (position >= Apps.size()) { Apps.push_back(std::make_pair(name, callback)); } else { Apps.insert(Apps.begin() + position, std::make_pair(name, callback)); } } } }; // Update the "time" app at position 0 updateApp("time", TimeApp, SHOW_TIME, 0); // Update the "date" app at position 1 updateApp("date", DateApp, SHOW_DATE, 1); // Update the "temp" app at position 2 updateApp("temp", TempApp, SHOW_TEMP, 2); // Update the "hum" app at position 3 updateApp("hum", HumApp, SHOW_HUM, 3); #ifdef ULANZI // Update the "bat" app at position 4 updateApp("bat", BatApp, SHOW_BAT, 4); #endif ui->setApps(Apps); setAutoTransition(true); } void DisplayManager_::setup() { TJpgDec.setCallback(jpg_output); TJpgDec.setJpgScale(1); FastLED.addLeds(leds, MATRIX_WIDTH * MATRIX_HEIGHT).setTemperature(OvercastSky); setMatrixLayout(MATRIX_LAYOUT); gif.setMatrix(matrix); ui->setAppAnimation(SLIDE_DOWN); ui->setTimePerApp(TIME_PER_APP); ui->setTimePerTransition(TIME_PER_TRANSITION); ui->setTargetFPS(MATRIX_FPS); ui->setOverlays(overlays, 4); setAutoTransition(AUTO_TRANSITION); ui->init(); } void DisplayManager_::tick() { if (AP_MODE) { HSVtext(2, 6, "AP MODE", true, 1); } else { ui->update(); if (ui->getUiState()->appState == IN_TRANSITION && !appIsSwitching) { appIsSwitching = true; showGif = false; } else if (ui->getUiState()->appState == FIXED && appIsSwitching) { appIsSwitching = false; MQTTManager.setCurrentApp(CURRENT_APP); setAppTime(TIME_PER_APP); } } } void DisplayManager_::clear() { matrix->clear(); } void DisplayManager_::show() { matrix->show(); } void DisplayManager_::leftButton() { if (!MenuManager.inMenu) ui->previousApp(); } void DisplayManager_::rightButton() { if (!MenuManager.inMenu) ui->nextApp(); } void DisplayManager_::nextApp() { if (!MenuManager.inMenu) ui->nextApp(); } void DisplayManager_::previousApp() { if (!MenuManager.inMenu) ui->previousApp(); } void snozzeTimerCallback() { ALARM_ACTIVE = true; AlarmTicker.detach(); } void DisplayManager_::selectButton() { if (!MenuManager.inMenu) { if (notify.flag && notify.hold) { DisplayManager.getInstance().dismissNotify(); } if (ALARM_ACTIVE && SNOOZE_TIME > 0) { PeripheryManager.stopSound(); ALARM_ACTIVE = false; AlarmTicker.once(SNOOZE_TIME * 60, snozzeTimerCallback); } if (TIMER_ACTIVE) { PeripheryManager.stopSound(); TIMER_ACTIVE = false; } } } void DisplayManager_::selectButtonLong() { if (ALARM_ACTIVE) { PeripheryManager.stopSound(); ALARM_ACTIVE = false; } } void DisplayManager_::dismissNotify() { notify.hold = false; notify.flag = false; showGif = false; } void timerCallback() { TIMER_ACTIVE = true; TimerTicker.detach(); } void DisplayManager_::gererateTimer(String Payload) { DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, Payload); if (error) return; int hours = doc["hours"] | 0; int minutes = doc["minutes"] | 0; int seconds = doc["seconds"] | 0; TIMER_SOUND = doc.containsKey("sound") ? doc["sound"].as() : ""; time_t now = time(nullptr); struct tm futureTimeinfo = *localtime(&now); futureTimeinfo.tm_hour += hours; futureTimeinfo.tm_min += minutes; futureTimeinfo.tm_sec += seconds; time_t futureTime = mktime(&futureTimeinfo); int interval = difftime(futureTime, now) * 1000; TimerTicker.once_ms(interval, timerCallback); } void DisplayManager_::switchToApp(const char *json) { DynamicJsonDocument doc(512); DeserializationError error = deserializeJson(doc, json); if (error) return; String name = doc["name"].as(); bool fast = doc["fast"] | false; int index = findAppIndexByName(name); if (index > -1) ui->transitionToApp(index); } void DisplayManager_::setNewSettings(const char *json) { DynamicJsonDocument doc(512); DeserializationError error = deserializeJson(doc, json); if (error) return; TIME_PER_APP = doc.containsKey("apptime") ? doc["apptime"] : TIME_PER_APP; TIME_PER_TRANSITION = doc.containsKey("transition") ? doc["transition"] : TIME_PER_TRANSITION; TEXTCOLOR_565 = doc.containsKey("textcolor") ? hexToRgb565(doc["textcolor"]) : TEXTCOLOR_565; MATRIX_FPS = doc.containsKey("fps") ? doc["fps"] : MATRIX_FPS; BRIGHTNESS = doc.containsKey("brightness") ? doc["brightness"] : BRIGHTNESS; AUTO_BRIGHTNESS = doc.containsKey("autobrightness") ? doc["autobrightness"] : AUTO_BRIGHTNESS; AUTO_TRANSITION = doc.containsKey("autotransition") ? doc["autotransition"] : AUTO_TRANSITION; UPPERCASE_LETTERS = doc.containsKey("uppercase") ? doc["uppercase"] : UPPERCASE_LETTERS; applyAllSettings(); saveSettings(); } void DisplayManager_::drawProgressBar(int cur, int total) { matrix->clear(); int progress = (cur * 100) / total; char progressStr[5]; snprintf(progressStr, 5, "%d%%", progress); printText(0, 6, progressStr, true, false); int leds_for_progress = (progress * MATRIX_WIDTH * MATRIX_HEIGHT) / 100; matrix->drawFastHLine(0, 7, MATRIX_WIDTH, matrix->Color(100, 100, 100)); matrix->drawFastHLine(0, 7, leds_for_progress / MATRIX_HEIGHT, matrix->Color(0, 255, 0)); matrix->show(); } void DisplayManager_::drawMenuIndicator(int cur, int total, uint16_t color) { int menuItemWidth = 1; int totalWidth = total * menuItemWidth + (total - 1); int leftMargin = (MATRIX_WIDTH - totalWidth) / 2; int pixelSpacing = 1; for (int i = 0; i < total; i++) { int x = leftMargin + i * (menuItemWidth + pixelSpacing); if (i == cur) { matrix->drawLine(x, 7, x + menuItemWidth - 1, 7, color); } else { matrix->drawLine(x, 7, x + menuItemWidth - 1, 7, matrix->Color(100, 100, 100)); } } } void DisplayManager_::drawBarChart(int16_t x, int16_t y, const int data[], byte dataSize, bool withIcon, uint16_t color) { int maximum = 0; int newData[dataSize]; for (int i = 0; i < dataSize; i++) { int d = data[i]; if (d > maximum) { maximum = d; } } for (int i = 0; i < dataSize; i++) { int d = data[i]; newData[i] = map(d, 0, maximum, 0, 8); } int barWidth; if (withIcon) { barWidth = ((32 - 9) / (dataSize)-1); } else { barWidth = (32 / (dataSize)-1); } int startX = withIcon ? 9 : 0; for (int i = 0; i < dataSize; i++) { int x1 = x + startX + (barWidth + 1) * i; int barHeight = newData[i]; int y1 = min(8 - barHeight, 7); matrix->fillRect(x1, y1 + y, barWidth, barHeight, color); } } std::pair getNativeAppByName(const String &appName) { if (appName == "time") { return std::make_pair("time", TimeApp); } else if (appName == "date") { return std::make_pair("date", DateApp); } else if (appName == "temp") { return std::make_pair("temp", TempApp); } else if (appName == "hum") { return std::make_pair("hum", HumApp); } #ifdef ULANZI else if (appName == "bat") { return std::make_pair("bat", BatApp); } #endif return std::make_pair("", nullptr); } void DisplayManager_::updateAppVector(const char *json) { StaticJsonDocument<512> doc; // Erhöhen Sie die Größe des Dokuments bei Bedarf deserializeJson(doc, json); JsonArray appArray; if (doc.is()) { JsonArray tempArray = doc.to(); tempArray.add(doc.as()); appArray = tempArray; } else if (doc.is()) { appArray = doc.as(); } for (JsonObject appObj : appArray) { String appName = appObj["name"].as(); bool show = appObj["show"].as(); int position = appObj.containsKey("pos") ? appObj["pos"].as() : Apps.size(); auto appIt = std::find_if(Apps.begin(), Apps.end(), [&appName](const std::pair &app) { return app.first == appName; }); std::pair nativeApp = getNativeAppByName(appName); if (!show) { if (appIt != Apps.end()) { Apps.erase(appIt); } } else { if (nativeApp.second != nullptr) { if (appIt != Apps.end()) { Apps.erase(appIt); } position = position < 0 ? 0 : position >= Apps.size() ? Apps.size() : position; Apps.insert(Apps.begin() + position, nativeApp); } else { if (appIt != Apps.end() && appObj.containsKey("pos")) { std::pair app = *appIt; Apps.erase(appIt); position = position < 0 ? 0 : position >= Apps.size() ? Apps.size() : position; Apps.insert(Apps.begin() + position, app); } } } } // Set the updated apps vector in the UI and save settings ui->setApps(Apps); saveSettings(); } String DisplayManager_::getStat() { StaticJsonDocument<256> doc; char buffer[5]; #ifdef ULANZI doc[BatKey] = BATTERY_PERCENT; doc[BatRawKey] = BATTERY_RAW; #endif snprintf(buffer, 5, "%.0f", CURRENT_LUX); doc[LuxKey] = buffer; doc[LDRRawKey] = LDR_RAW; doc["ram"] = ESP.getFreeHeap(); doc[BrightnessKey] = BRIGHTNESS; snprintf(buffer, 5, "%.0f", CURRENT_TEMP); doc[TempKey] = buffer; snprintf(buffer, 5, "%.0f", CURRENT_HUM); doc[HumKey] = buffer; doc[UpTimeKey] = PeripheryManager.readUptime(); doc[SignalStrengthKey] = WiFi.RSSI(); doc[UpdateKey] = UPDATE_AVAILABLE; doc["messages"] = RECEIVED_MESSAGES; String jsonString; return serializeJson(doc, jsonString), jsonString; } void DisplayManager_::setAppTime(uint16_t duration) { ui->setTimePerApp(duration); } void DisplayManager_::setMatrixLayout(int layout) { delete matrix; // Free memory from the current matrix object switch (layout) { case 0: matrix = new FastLED_NeoMatrix(leds, 32, 8, NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG); break; case 1: matrix = new FastLED_NeoMatrix(leds, 8, 8, 4, 1, NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_PROGRESSIVE); break; case 2: matrix = new FastLED_NeoMatrix(leds, 32, 8, NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG); break; default: break; } delete ui; // Free memory from the current ui object ui = new MatrixDisplayUi(matrix); // Create a new ui object with the new matrix } String DisplayManager_::getAppsAsJson() { // Create a JSON object to hold the app positions and names DynamicJsonDocument doc(1024); JsonObject appsObject = doc.to(); // Add each app position and name to the object for (size_t i = 0; i < Apps.size(); i++) { appsObject[String(i)] = Apps[i].first; } // Serialize the JSON object to a string and return it String json; serializeJson(appsObject, json); return json; } void DisplayManager_::onStateParse(const char *json) { DynamicJsonDocument doc(512); DeserializationError error = deserializeJson(doc, json); if (error) return; bool state = doc["state"].as(); onState(state); } void DisplayManager_::onState(bool state) { if (state) { MATRIX_OFF = false; setBrightness(lastBrightness); } else { MATRIX_OFF = true; lastBrightness = BRIGHTNESS; setBrightness(0); } }