diff --git a/.gitignore b/.gitignore index 6ed07e0a..e7add203 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -.pio -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.vscode/ipch -/Tools +.pio +.DS_Store +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +/Tools src/Globals.cpp diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 359a874a..26548c4d 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,6 +16,7 @@ - [Apps](apps.md) - [Native Apps](apps.md#native-apps) - [Custom Apps](apps.md#custom-apps) + - [Countdown](countdown.md) - [Effects](effects.md) - [Icons](icons.md) - [Sounds](sounds.md) diff --git a/docs/api.md b/docs/api.md index 9556a83f..713efde1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -390,21 +390,25 @@ You can adjust each property in the JSON object according to your preferences. I | `CTEMP` | array of ints | Color temperature for the matrix. | RGB array | N/A | | `TFORMAT` | string | Time format for the TimeApp. | Varies (see below) | N/A | | `DFORMAT` | string | Date format for the DateApp. | Varies (see below) | N/A | +| `CDATE` | string | Target date for the Countdown app. The target day shows only the icon; past dates turn Countdown off. | `YYYY-MM-DD` | empty | +| `CICON` | string | Icon ID or filename for the Countdown app, using the existing `/ICONS` icon storage. | Icon name without extension | empty | | `SOM` | boolean | Start the week on Monday. | `true`/`false` | true | | `CEL` | boolean | Shows the temperature in Celsius (Fahrenheit when false). | `true`/`false` | true | | `BLOCKN` | boolean | Block physical navigation keys (still sends input to MQTT). | `true`/`false` | false | | `UPPERCASE` | boolean | Display text in uppercase. | `true`/`false` | true | | `TIME_COL` | string/array of ints | Text color of the time app. Use 0 for global text color. | RGB array or hex color | N/A | | `DATE_COL` | string/array of ints | Text color of the date app. Use 0 for global text color. | RGB array or hex color | N/A | +| `COUNT_COL` | string/array of ints | Text color of the countdown app. Use 0 for global text color. | RGB array or hex color | N/A | | `TEMP_COL` | string/array of ints | Text color of the temperature app. Use 0 for global text color. | RGB array or hex color | N/A | | `HUM_COL` | string/array of ints | Text color of the humidity app. Use 0 for global text color. | RGB array or hex color | N/A | | `BAT_COL` | string/array of ints | Text color of the battery app. Use 0 for global text color. | RGB array or hex color | N/A | | `SSPEED` | integer | Scroll speed modification. | Percentage of original scroll speed | 100 | -| `TIM` | boolean | Enable or disable the native time app (requires reboot). | `true`/`false` | true | -| `DAT` | boolean | Enable or disable the native date app (requires reboot). | `true`/`false` | true | -| `HUM` | boolean | Enable or disable the native humidity app (requires reboot). | `true`/`false` | true | -| `TEMP` | boolean | Enable or disable the native temperature app (requires reboot). | `true`/`false` | true | -| `BAT` | boolean | Enable or disable the native battery app (requires reboot). | `true`/`false` | true | +| `TIM` | boolean | Enable or disable the native time app. | `true`/`false` | true | +| `DAT` | boolean | Enable or disable the native date app. | `true`/`false` | true | +| `COUNT` | boolean | Enable or disable the native countdown app. | `true`/`false` | false | +| `HUM` | boolean | Enable or disable the native humidity app. | `true`/`false` | true | +| `TEMP` | boolean | Enable or disable the native temperature app. | `true`/`false` | true | +| `BAT` | boolean | Enable or disable the native battery app. | `true`/`false` | true | | `MATP` | boolean | Enable or disable the matrix. Similar to `power` endpoint but without the animation. | `true`/`false` | true | | `VOL` | integer | Allows to set the volume of the buzzer and DFplayer. | 0–30 | true | | `OVERLAY` | string | Sets a global effect overlay (cannot be used with app specific overlays). | Varies (see below) | N/A | diff --git a/docs/apps.md b/docs/apps.md index db4f650a..d7bbfdf3 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -76,6 +76,25 @@ The Dateapp shows the current date of course. There are several dateformats 'DFO | `%d/%m` | `16/04` | Day/Month | | `%m-%d-%y` | `04-16-22` | Month-Day-Year | +--- +## Countdown + +The Countdown app shows how many calendar days remain until a configured target date. Before the target date, the left side displays the target day of month together with a custom icon from the existing icon manager, and the right side displays the remaining day count. On the target date, only the configured icon is shown. After the target date has passed, Countdown automatically turns itself off. + +Configure it through the Settings API: + +```json +{ + "COUNT": true, + "CDATE": "2026-06-01", + "CICON": "1234" +} +``` + +`CDATE` must use `YYYY-MM-DD`. `CICON` is the icon ID or filename without extension from the `/ICONS` folder. `COUNT_COL` can be used to set the countdown text color, or `0` to use the global text color. + +See [Countdown](countdown.md) for HTTP and MQTT setup examples. + --- ## Temperature @@ -330,5 +349,3 @@ This Node-RED flow retrieves and displays the subscriber count of a specified Yo To use this flow, replace the "XXX" in the "Data" node with your YouTube API key and ensure that the MQTT broker settings in the "MQTT out" node are correct. The flow will then retrieve the subscriber count of the specified YouTube channel and display it on your AWTRIX device along with the icon. This Flow uses icon 5029 from LM (Just download it from the awtrix webinterface). You can change the icon in the flow to your favorite one. - - diff --git a/docs/countdown.md b/docs/countdown.md new file mode 100644 index 00000000..be216769 --- /dev/null +++ b/docs/countdown.md @@ -0,0 +1,106 @@ +# Countdown App + +The Countdown app is a native app that shows how many calendar days remain until a configured target date. + +The display layout is: + +- Left: target day of month and the configured icon. +- Right: remaining days. + +On the target date, the app hides the target day and remaining-day count, and only shows the configured icon. After the target date has passed, the Countdown app turns itself off until a future target date is configured and the app is enabled again. + +## Enable Countdown + +Set `COUNT` to `true` through the Settings API. + +HTTP example: + +```bash +curl -X POST "http://[IP]/api/settings" \ + -H "Content-Type: application/json" \ + -d '{"COUNT":true,"CDATE":"2026-06-01","CICON":"1234"}' +``` + +MQTT example: + +```text +Topic: [PREFIX]/settings +Payload: {"COUNT":true,"CDATE":"2026-06-01","CICON":"1234"} +``` + +Replace: + +- `[IP]` with the AWTRIX IP address. +- `[PREFIX]` with the configured MQTT prefix. +- `2026-06-01` with your target date. +- `1234` with the icon ID or filename from the existing icon manager. + +## Set The Initial Target Date + +The target date is configured with `CDATE`. + +```json +{ + "CDATE": "2026-06-01" +} +``` + +`CDATE` must use `YYYY-MM-DD`. + +The app counts full calendar days based on the device's local date: + +- Today to tomorrow: `1` +- Today to today: only the configured icon is shown. +- Today to yesterday: Countdown automatically turns off. + +## Set The Icon + +The icon is configured with `CICON`. + +```json +{ + "CICON": "1234" +} +``` + +`CICON` uses the existing icon management system. Use the icon ID or filename without extension. The firmware looks for: + +- `/ICONS/1234.jpg` +- `/ICONS/1234.gif` + +If the icon is missing before the target date, the Countdown app still shows the date and remaining days. On the target date, a missing icon leaves the Countdown screen blank. + +## Optional Text Color + +Set `COUNT_COL` to customize the countdown text color. + +```json +{ + "COUNT_COL": "#00FF00" +} +``` + +Use `0` to fall back to the global text color. + +## Disable Countdown + +Set `COUNT` to `false`. + +```bash +curl -X POST "http://[IP]/api/settings" \ + -H "Content-Type: application/json" \ + -d '{"COUNT":false}' +``` + +## Full Example + +This enables Countdown, sets the initial target date, sets the icon, and sets the text color: + +```json +{ + "COUNT": true, + "CDATE": "2026-06-01", + "CICON": "1234", + "COUNT_COL": "#FFFFFF" +} +``` diff --git a/src/Apps.cpp b/src/Apps.cpp index 378915e7..d57193bf 100644 --- a/src/Apps.cpp +++ b/src/Apps.cpp @@ -11,6 +11,7 @@ #include "MQTTManager.h" #include "Overlays.h" #include "timer.h" +#include "CountdownUtils.h" const uint8_t bigdigits_mask[12][7] = { {132, 48, 48, 48, 48, 48, 132}, // 0 @@ -336,6 +337,115 @@ void DateApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, } } +void CountdownApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer) +{ + if (notifyFlag) + return; + CURRENT_APP = "Countdown"; + currentCustomApp = ""; + + CountdownDate target; + if (!parseCountdownDate(COUNTDOWN_TARGET.c_str(), target)) + { + return; + } + + struct tm *currentTime = timer_localtime(); + if (currentTime == nullptr) + { + return; + } + + long days = countdownDaysBetweenDates( + currentTime->tm_year + 1900, + currentTime->tm_mon + 1, + currentTime->tm_mday, + target.year, + target.month, + target.day); + CountdownDisplayState displayState = countdownDisplayState(days); + + if (displayState == CountdownDisplayState::Expired) + { + SHOW_COUNTDOWN = false; + DisplayManager.loadNativeApps(); + saveSettings(); + return; + } + + if (COUNTDOWN_COLOR > 0) + { + DisplayManager.setTextColor(COUNTDOWN_COLOR); + } + else + { + DisplayManager.getInstance().resetTextColor(); + } + + bool iconOnly = displayState == CountdownDisplayState::DueToday; + if (!iconOnly) + { + char targetDay[3]; + snprintf(targetDay, sizeof(targetDay), "%d", target.day); + + DisplayManager.drawFilledRect(x, y, 9, 8, CALENDAR_BODY_COLOR); + DisplayManager.drawFilledRect(x, y, 9, 2, CALENDAR_HEADER_COLOR); + DisplayManager.setTextColor(CALENDAR_TEXT_COLOR); + DisplayManager.setCursor((target.day < 10 ? 3 : 1) + x, 7 + y); + DisplayManager.matrixPrint(targetDay); + } + + static File countdownIcon; + static String openedIconName; + static bool isGif = false; + static uint8_t currentFrame = 0; + + if (COUNTDOWN_ICON != openedIconName) + { + countdownIcon.close(); + openedIconName = COUNTDOWN_ICON; + currentFrame = 0; + isGif = false; + + if (COUNTDOWN_ICON.length() > 0) + { + String jpgPath = "/ICONS/" + COUNTDOWN_ICON + ".jpg"; + String gifPath = "/ICONS/" + COUNTDOWN_ICON + ".gif"; + if (LittleFS.exists(jpgPath)) + { + countdownIcon = LittleFS.open(jpgPath); + } + else if (LittleFS.exists(gifPath)) + { + countdownIcon = LittleFS.open(gifPath); + isGif = true; + } + } + } + + if (countdownIcon) + { + int16_t iconX = (iconOnly ? 12 : 10) + x; + if (isGif) + { + gifPlayer->playGif(iconX, y, &countdownIcon, currentFrame); + currentFrame = gifPlayer->getFrame(); + } + else + { + DisplayManager.drawJPG(iconX, y, countdownIcon); + } + } + + if (!iconOnly) + { + char remainingDays[8]; + snprintf(remainingDays, sizeof(remainingDays), "%ld", days); + DisplayManager.setTextColor(COUNTDOWN_COLOR > 0 ? COUNTDOWN_COLOR : TEXTCOLOR_888); + DisplayManager.printText(20 + x, 6 + y, remainingDays, false, 2); + } +} + void TempApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer) { if (notifyFlag) diff --git a/src/Apps.h b/src/Apps.h index 9e44bb9a..dbbcbec8 100644 --- a/src/Apps.h +++ b/src/Apps.h @@ -77,6 +77,8 @@ void TimeApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, void DateApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); +void CountdownApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); + void TempApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); void HumApp(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); @@ -108,4 +110,4 @@ void CApp17(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, i void CApp18(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); void CApp19(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); void CApp20(FastLED_NeoMatrix *matrix, MatrixDisplayUiState *state, int16_t x, int16_t y, GifPlayer *gifPlayer); -#endif \ No newline at end of file +#endif diff --git a/src/CountdownUtils.cpp b/src/CountdownUtils.cpp new file mode 100644 index 00000000..1af07c6d --- /dev/null +++ b/src/CountdownUtils.cpp @@ -0,0 +1,93 @@ +#include "CountdownUtils.h" + +#include + +static bool isLeapYear(int year) +{ + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + +static int daysInMonth(int year, int month) +{ + static const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2 && isLeapYear(year)) + { + return 29; + } + return days[month - 1]; +} + +static bool isValidDate(int year, int month, int day) +{ + return year >= 1970 && month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month); +} + +static long daysFromCivil(int year, unsigned month, unsigned day) +{ + year -= month <= 2; + const long era = (year >= 0 ? year : year - 399) / 400; + const unsigned yoe = static_cast(year - era * 400); + const unsigned doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; +} + +bool parseCountdownDate(const char *value, CountdownDate &date) +{ + if (value == nullptr) + { + return false; + } + + for (int i = 0; i < 10; ++i) + { + if ((i == 4 || i == 7) ? value[i] != '-' : value[i] < '0' || value[i] > '9') + { + return false; + } + } + + if (value[10] != '\0') + { + return false; + } + + int year = atoi(value); + int month = atoi(value + 5); + int day = atoi(value + 8); + + if (!isValidDate(year, month, day)) + { + return false; + } + + date.year = year; + date.month = month; + date.day = day; + return true; +} + +long countdownDaysBetweenDates(int currentYear, int currentMonth, int currentDay, int targetYear, int targetMonth, int targetDay) +{ + if (!isValidDate(currentYear, currentMonth, currentDay) || !isValidDate(targetYear, targetMonth, targetDay)) + { + return 0; + } + + return daysFromCivil(targetYear, targetMonth, targetDay) - daysFromCivil(currentYear, currentMonth, currentDay); +} + +CountdownDisplayState countdownDisplayState(long daysRemaining) +{ + if (daysRemaining < 0) + { + return CountdownDisplayState::Expired; + } + + if (daysRemaining == 0) + { + return CountdownDisplayState::DueToday; + } + + return CountdownDisplayState::Active; +} diff --git a/src/CountdownUtils.h b/src/CountdownUtils.h new file mode 100644 index 00000000..38a20a8e --- /dev/null +++ b/src/CountdownUtils.h @@ -0,0 +1,22 @@ +#ifndef CountdownUtils_h +#define CountdownUtils_h + +struct CountdownDate +{ + int year; + int month; + int day; +}; + +enum class CountdownDisplayState +{ + Active, + DueToday, + Expired +}; + +bool parseCountdownDate(const char *value, CountdownDate &date); +long countdownDaysBetweenDates(int currentYear, int currentMonth, int currentDay, int targetYear, int targetMonth, int targetDay); +CountdownDisplayState countdownDisplayState(long daysRemaining); + +#endif diff --git a/src/DisplayManager.cpp b/src/DisplayManager.cpp index a3ef09d6..506c394b 100644 --- a/src/DisplayManager.cpp +++ b/src/DisplayManager.cpp @@ -1102,14 +1102,15 @@ void DisplayManager_::loadNativeApps() updateApp("Time", TimeApp, SHOW_TIME, 0); updateApp("Date", DateApp, SHOW_DATE, 1); + updateApp("Countdown", CountdownApp, SHOW_COUNTDOWN, 2); if (SENSOR_READING) { - updateApp("Temperature", TempApp, SHOW_TEMP, 2); - updateApp("Humidity", HumApp, SHOW_HUM, 3); + updateApp("Temperature", TempApp, SHOW_TEMP, 3); + updateApp("Humidity", HumApp, SHOW_HUM, 4); } #ifdef ULANZI - updateApp("Battery", BatApp, SHOW_BAT, 4); + updateApp("Battery", BatApp, SHOW_BAT, 5); #endif ui->setApps(Apps); @@ -1536,6 +1537,10 @@ std::pair getNativeAppByName(const String &appName) { return std::make_pair("Date", DateApp); } + else if (appName == "Countdown") + { + return std::make_pair("Countdown", CountdownApp); + } else if (appName == "Temperature") { return std::make_pair("Temperature", TempApp); @@ -1635,7 +1640,7 @@ void DisplayManager_::updateAppVector(const char *json) String DisplayManager_::getStats() { - StaticJsonDocument<1024> doc; + StaticJsonDocument<1536> doc; char buffer[20]; #ifdef awtrix2_upgrade @@ -2018,7 +2023,7 @@ String getOverlayName() String DisplayManager_::getSettings() { - StaticJsonDocument<1024> doc; + StaticJsonDocument<1536> doc; doc["MATP"] = !MATRIX_OFF; doc["ABRI"] = AUTO_BRIGHTNESS; doc["BRI"] = BRIGHTNESS; @@ -2033,6 +2038,8 @@ String DisplayManager_::getSettings() doc["CBCOL"] = CALENDAR_BODY_COLOR; doc["TFORMAT"] = TIME_FORMAT; doc["DFORMAT"] = DATE_FORMAT; + doc["CDATE"] = COUNTDOWN_TARGET; + doc["CICON"] = COUNTDOWN_ICON; doc["SOM"] = START_ON_MONDAY; doc["CEL"] = IS_CELSIUS; doc["BLOCKN"] = BLOCK_NAVIGATION; @@ -2048,12 +2055,14 @@ String DisplayManager_::getSettings() doc["WDCI"] = WDC_INACTIVE; doc["TIME_COL"] = TIME_COLOR; doc["DATE_COL"] = DATE_COLOR; + doc["COUNT_COL"] = COUNTDOWN_COLOR; doc["HUM_COL"] = HUM_COLOR; doc["TEMP_COL"] = TEMP_COLOR; doc["BAT_COL"] = BAT_COLOR; doc["SSPEED"] = SCROLL_SPEED; doc["TIM"] = SHOW_TIME; doc["DAT"] = SHOW_DATE; + doc["COUNT"] = SHOW_COUNTDOWN; doc["HUM"] = SHOW_HUM; doc["TEMP"] = SHOW_TEMP; doc["BAT"] = SHOW_BAT; @@ -2110,6 +2119,13 @@ void DisplayManager_::setNewSettings(const char *json) return; } + bool oldShowTime = SHOW_TIME; + bool oldShowDate = SHOW_DATE; + bool oldShowCountdown = SHOW_COUNTDOWN; + bool oldShowHum = SHOW_HUM; + bool oldShowTemp = SHOW_TEMP; + bool oldShowBat = SHOW_BAT; + TIME_MODE = doc.containsKey("TMODE") ? doc["TMODE"].as() : TIME_MODE; TRANS_EFFECT = doc.containsKey("TEFF") ? doc["TEFF"] : TRANS_EFFECT; TIME_PER_TRANSITION = doc.containsKey("TSPEED") ? doc["TSPEED"] : TIME_PER_TRANSITION; @@ -2121,6 +2137,8 @@ void DisplayManager_::setNewSettings(const char *json) TIME_FORMAT = doc.containsKey("TFORMAT") ? doc["TFORMAT"].as() : TIME_FORMAT; GAMMA = doc.containsKey("GAMMA") ? doc["GAMMA"].as() : GAMMA; DATE_FORMAT = doc.containsKey("DFORMAT") ? doc["DFORMAT"].as() : DATE_FORMAT; + COUNTDOWN_TARGET = doc.containsKey("CDATE") ? doc["CDATE"].as() : COUNTDOWN_TARGET; + COUNTDOWN_ICON = doc.containsKey("CICON") ? doc["CICON"].as() : COUNTDOWN_ICON; AUTO_BRIGHTNESS = doc.containsKey("ABRI") ? doc["ABRI"].as() : AUTO_BRIGHTNESS; AUTO_TRANSITION = doc.containsKey("ATRANS") ? doc["ATRANS"].as() : AUTO_TRANSITION; UPPERCASE_LETTERS = doc.containsKey("UPPERCASE") ? doc["UPPERCASE"].as() : UPPERCASE_LETTERS; @@ -2128,6 +2146,7 @@ void DisplayManager_::setNewSettings(const char *json) BLOCK_NAVIGATION = doc.containsKey("BLOCKN") ? doc["BLOCKN"].as() : BLOCK_NAVIGATION; SHOW_TIME = doc.containsKey("TIM") ? doc["TIM"].as() : SHOW_TIME; SHOW_DATE = doc.containsKey("DAT") ? doc["DAT"].as() : SHOW_DATE; + SHOW_COUNTDOWN = doc.containsKey("COUNT") ? doc["COUNT"].as() : SHOW_COUNTDOWN; SHOW_HUM = doc.containsKey("HUM") ? doc["HUM"].as() : SHOW_HUM; SHOW_TEMP = doc.containsKey("TEMP") ? doc["TEMP"].as() : SHOW_TEMP; SHOW_BAT = doc.containsKey("BAT") ? doc["BAT"].as() : SHOW_BAT; @@ -2232,6 +2251,11 @@ void DisplayManager_::setNewSettings(const char *json) auto DATE_COL = doc["DATE_COL"]; DATE_COLOR = getColorFromJsonVariant(DATE_COL, TEXTCOLOR_888); } + if (doc.containsKey("COUNT_COL")) + { + auto COUNT_COL = doc["COUNT_COL"]; + COUNTDOWN_COLOR = getColorFromJsonVariant(COUNT_COL, TEXTCOLOR_888); + } if (doc.containsKey("TEMP_COL")) { auto TEMP_COL = doc["TEMP_COL"]; @@ -2249,6 +2273,15 @@ void DisplayManager_::setNewSettings(const char *json) } doc.clear(); applyAllSettings(); + if (oldShowTime != SHOW_TIME || + oldShowDate != SHOW_DATE || + oldShowCountdown != SHOW_COUNTDOWN || + oldShowHum != SHOW_HUM || + oldShowTemp != SHOW_TEMP || + oldShowBat != SHOW_BAT) + { + loadNativeApps(); + } saveSettings(); if (DEBUG_MODE) DEBUG_PRINTLN("Settings loaded"); @@ -2298,6 +2331,10 @@ String DisplayManager_::getAppsWithIcon() { appObject["icon"] = customApp->iconName; } + else if (app.first == "Countdown") + { + appObject["icon"] = COUNTDOWN_ICON; + } } String jsonString; serializeJson(jsonArray, jsonString); diff --git a/src/Globals.cpp b/src/Globals.cpp index 4390b762..545023a7 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -263,6 +263,7 @@ void loadSettings() TIME_MODE = Settings.getUInt("TMODE", 1); TIME_COLOR = Settings.getUInt("TIME_COL", 0); DATE_COLOR = Settings.getUInt("DATE_COL", 0); + COUNTDOWN_COLOR = Settings.getUInt("COUNT_COL", 0); TEMP_COLOR = Settings.getUInt("TEMP_COL", 0); HUM_COLOR = Settings.getUInt("HUM_COL", 0); #ifdef ULANZI @@ -276,11 +277,14 @@ void loadSettings() TIME_PER_APP = Settings.getUInt("ATIME", 7000); TIME_FORMAT = Settings.getString("TFORMAT", "%H %M"); DATE_FORMAT = Settings.getString("DFORMAT", "%d.%m.%y"); + COUNTDOWN_TARGET = Settings.getString("CDATE", ""); + COUNTDOWN_ICON = Settings.getString("CICON", ""); START_ON_MONDAY = Settings.getBool("SOM", true); BLOCK_NAVIGATION = Settings.getBool("BLOCKN", false); IS_CELSIUS = Settings.getBool("CEL", true); SHOW_TIME = Settings.getBool("TIM", true); SHOW_DATE = Settings.getBool("DAT", false); + SHOW_COUNTDOWN = Settings.getBool("COUNT", false); SHOW_TEMP = Settings.getBool("TEMP", true); SHOW_HUM = Settings.getBool("HUM", true); MATRIX_LAYOUT = Settings.getUInt("MAT", 0); @@ -316,6 +320,7 @@ void saveSettings() Settings.putUInt("TMODE", TIME_MODE); Settings.putUInt("TIME_COL", TIME_COLOR); Settings.putUInt("DATE_COL", DATE_COLOR); + Settings.putUInt("COUNT_COL", COUNTDOWN_COLOR); Settings.putUInt("TEMP_COL", TEMP_COLOR); Settings.putUInt("HUM_COL", HUM_COLOR); #ifdef ULANZI @@ -327,10 +332,13 @@ void saveSettings() Settings.putUInt("ATIME", TIME_PER_APP); Settings.putString("TFORMAT", TIME_FORMAT); Settings.putString("DFORMAT", DATE_FORMAT); + Settings.putString("CDATE", COUNTDOWN_TARGET); + Settings.putString("CICON", COUNTDOWN_ICON); Settings.putBool("SOM", START_ON_MONDAY); Settings.putBool("CEL", IS_CELSIUS); Settings.putBool("TIM", SHOW_TIME); Settings.putBool("DAT", SHOW_DATE); + Settings.putBool("COUNT", SHOW_COUNTDOWN); Settings.putBool("TEMP", SHOW_TEMP); Settings.putBool("HUM", SHOW_HUM); Settings.putUInt("SSPEED", SCROLL_SPEED); @@ -358,6 +366,7 @@ bool IO_BROKER = false; bool NET_STATIC = false; bool SHOW_TIME = true; bool SHOW_DATE = true; +bool SHOW_COUNTDOWN = false; bool SHOW_WEATHER = true; bool SHOW_BAT = true; bool SHOW_TEMP = true; @@ -402,6 +411,8 @@ float HUM_OFFSET; uint16_t LDR_RAW; String TIME_FORMAT = "%H:%M:%S"; String DATE_FORMAT = "%d.%m.%y"; +String COUNTDOWN_TARGET = ""; +String COUNTDOWN_ICON = ""; int BACKGROUND_EFFECT = -1; bool START_ON_MONDAY; @@ -438,6 +449,7 @@ uint32_t CALENDAR_HEADER_COLOR = 0xFF0000; uint32_t CALENDAR_TEXT_COLOR = 0x000000; uint32_t CALENDAR_BODY_COLOR = 0xFFFFFF; uint32_t DATE_COLOR = 0; +uint32_t COUNTDOWN_COLOR = 0; uint32_t BAT_COLOR = 0; uint32_t TEMP_COLOR = 0; uint32_t HUM_COLOR = 0; diff --git a/src/Globals.h b/src/Globals.h index 87b344a4..752545cc 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -45,6 +45,7 @@ extern bool IO_BROKER; extern bool NET_STATIC; extern bool SHOW_TIME; extern bool SHOW_DATE; +extern bool SHOW_COUNTDOWN; extern bool SHOW_WEATHER; extern bool SHOW_BAT; extern String HA_PREFIX; @@ -88,6 +89,8 @@ extern bool MIRROR_DISPLAY; extern bool AUTO_TRANSITION; extern String TIME_FORMAT; extern String DATE_FORMAT; +extern String COUNTDOWN_TARGET; +extern String COUNTDOWN_ICON; extern bool START_ON_MONDAY; extern bool IS_CELSIUS; @@ -127,6 +130,7 @@ extern uint32_t CALENDAR_TEXT_COLOR; extern uint32_t CALENDAR_BODY_COLOR; extern uint32_t TIME_COLOR; extern uint32_t DATE_COLOR; +extern uint32_t COUNTDOWN_COLOR; extern uint32_t BAT_COLOR; extern uint32_t TEMP_COLOR; extern uint32_t HUM_COLOR; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 6544eb7f..be8f4e28 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -73,7 +73,11 @@ int8_t dateFormatIndex; uint8_t dateFormatCount = 9; int8_t appsIndex; +#ifdef awtrix2_upgrade uint8_t appsCount = 5; +#else +uint8_t appsCount = 6; +#endif MenuState currentState = MainMenu; @@ -122,8 +126,8 @@ int convertBRIPercentTo8Bit(int brightness_percent) { brightness = map(brightness_percent, 0, 100, 0, 255); } - return brightness; -} + return brightness; +} String MenuManager_::menutext() { @@ -183,13 +187,16 @@ String MenuManager_::menutext() DisplayManager.drawBMP(0, 0, icon_1158, 8, 8); return SHOW_DATE ? "ON" : "OFF"; case 2: + DisplayManager.drawBMP(0, 0, icon_1158, 8, 8); + return SHOW_COUNTDOWN ? "ON" : "OFF"; + case 3: DisplayManager.drawBMP(0, 0, icon_234, 8, 8); return SHOW_TEMP ? "ON" : "OFF"; - case 3: + case 4: DisplayManager.drawBMP(0, 0, icon_2075, 8, 8); return SHOW_HUM ? "ON" : "OFF"; #ifndef awtrix2_upgrade - case 4: + case 5: DisplayManager.drawBMP(0, 0, icon_1486, 8, 8); return SHOW_BAT ? "ON" : "OFF"; #endif @@ -382,13 +389,16 @@ void MenuManager_::selectButton() SHOW_DATE = !SHOW_DATE; break; case 2: - SHOW_TEMP = !SHOW_TEMP; + SHOW_COUNTDOWN = !SHOW_COUNTDOWN; break; case 3: + SHOW_TEMP = !SHOW_TEMP; + break; + case 4: SHOW_HUM = !SHOW_HUM; break; #ifndef awtrix2_upgrade - case 4: + case 5: SHOW_BAT = !SHOW_BAT; break; #endif diff --git a/test/countdown_utils_test.cpp b/test/countdown_utils_test.cpp new file mode 100644 index 00000000..2672b606 --- /dev/null +++ b/test/countdown_utils_test.cpp @@ -0,0 +1,18 @@ +#include "../src/CountdownUtils.h" + +#include + +int main() +{ + assert(countdownDisplayState(1) == CountdownDisplayState::Active); + assert(countdownDisplayState(0) == CountdownDisplayState::DueToday); + assert(countdownDisplayState(-1) == CountdownDisplayState::Expired); + + CountdownDate target; + assert(parseCountdownDate("2026-06-05", target)); + assert(countdownDaysBetweenDates(2026, 6, 5, target.year, target.month, target.day) == 0); + assert(countdownDaysBetweenDates(2026, 6, 4, target.year, target.month, target.day) == 1); + assert(countdownDaysBetweenDates(2026, 6, 6, target.year, target.month, target.day) == -1); + + return 0; +}