From ab3cc5de7b15d77f41bd03e56ba8da23ca2f8394 Mon Sep 17 00:00:00 2001 From: witnessmenow Date: Thu, 3 Aug 2023 08:14:03 +0100 Subject: [PATCH 1/3] This should be commneted out by default --- F1-Notifications/F1-Notifications.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/F1-Notifications/F1-Notifications.ino b/F1-Notifications/F1-Notifications.ino index f09241c..bd7ec89 100644 --- a/F1-Notifications/F1-Notifications.ino +++ b/F1-Notifications/F1-Notifications.ino @@ -23,7 +23,7 @@ //#define YELLOW_DISPLAY // 2. Matrix Displays (Like the ESP32 Trinity) -#define MATRIX_DISPLAY +//#define MATRIX_DISPLAY // If no defines are set, it will default to CYD #if !defined(YELLOW_DISPLAY) && !defined(MATRIX_DISPLAY) From 5d17c4db7e5c4adc0d50595f66a23dcb4d2849a7 Mon Sep 17 00:00:00 2001 From: witnessmenow Date: Mon, 6 May 2024 13:31:02 +0100 Subject: [PATCH 2/3] Updating to 2024 and fixing up the project --- .github/workflows/build-deploy-webflash.yml | 173 ++++----- .github/workflows/test-build.yml | 100 ++--- .gitignore | 4 + F1-Notifications/F1-Notifications.ino | 89 +++-- F1-Notifications/cheapYellowLCD.h | 256 ++++++------- F1-Notifications/getImage.h | 83 +++-- F1-Notifications/githubCert.h | 42 +-- F1-Notifications/matrixDisplay.h | 343 +++++++++--------- F1-Notifications/raceLogic.h | 143 +++++--- F1-Notifications/util.h | 12 + .../{wifiManager.h => wifiManagerHandler.h} | 0 GitHubPages/index.html | 6 + README.md | 103 ++++-- images/320x240/Imola.png | Bin 0 -> 32558 bytes .../320x240/{6- Imola.png => calledOff.png} | Bin images/Readme.md | 10 + platformio.ini | 83 +++++ 17 files changed, 815 insertions(+), 632 deletions(-) create mode 100644 .gitignore create mode 100644 F1-Notifications/util.h rename F1-Notifications/{wifiManager.h => wifiManagerHandler.h} (100%) create mode 100644 images/320x240/Imola.png rename images/320x240/{6- Imola.png => calledOff.png} (100%) create mode 100644 images/Readme.md create mode 100644 platformio.ini diff --git a/.github/workflows/build-deploy-webflash.yml b/.github/workflows/build-deploy-webflash.yml index 804f53c..c8ca604 100644 --- a/.github/workflows/build-deploy-webflash.yml +++ b/.github/workflows/build-deploy-webflash.yml @@ -6,7 +6,7 @@ on: push: branches: ["main"] paths-ignore: - - '**.md' + - "**.md" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -28,110 +28,91 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - - # Install the Arduino CLI - - name: Install Arduino CLI - uses: arduino/setup-arduino-cli@v1 - - # Install TFT_eSPI - - name: Install TFT_eSPI library to prepare for modifications - run: | - arduino-cli lib install TFT_eSPI - - # Copy binaries to GitHubPages folder for publishing - - name: Copy User_Setup.h for TFT_eSPI + - uses: actions/checkout@v4 + - uses: actions/cache@v3 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install PlatformIO run: | - \cp -fR DisplayConfig/User_Setup.h ~/Arduino/libraries/TFT_eSPI/ - - # Build CYD Arduino Code - - uses: arduino/compile-sketches@v1 - name: Compile CYD code - with: - fqbn: "esp32:esp32:esp32" - platforms: | - - name: esp32:esp32 - source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - # No need to specify ESP-libraries as these are installed with the platform (on the line above). - # Downloading SpeedyStepper from source to get correct casing on import of Arduino.h (spelled arduino.h in the version from Arduino Library Manager) - libraries: | - - name: ImageFetcher - source-url: https://github.com/witnessmenow/file-fetcher-arduino.git - - name: WiFiManager - - name: ESP_DoubleResetDetector - - name: ArduinoJson - - name: ezTime - - name: UniversalTelegramBot - - name: PNGdec - sketch-paths: | - - F1-Notifications - enable-warnings-report: true - verbose: false - cli-compile-flags: | - - --export-binaries - - # Copy binaries to GitHubPages folder for publishing - - name: Copy compiled binaries to CYD + python -m pip install --upgrade pip + pip install --upgrade platformio + + #Build CYD + - name: Build CYD + run: platformio run -e cyd + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: CYD Firmware + path: .pio/build/cyd/firmware.bin + if-no-files-found: error + - name: Copy compiled binaries for webflash run: | - # mkdir GitHubPages/ESPWebTools/esp32Firmware mkdir GitHubPages/ESPWebTools/cyd # Copy the manifest file for the CYD cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/cyd - - # Copy compiled binaries that were exported by the `-e` flag in the arduino/compile-sketches task - # Maintain folder structure, for cases where we are compiling for multiple board versions - # If we were sure that we would never need to compile for anything other then esp32:esp32:esp32, we could have used this command instead: cp -r OpenMacroRail_Arduino/build/esp32.esp32.esp32/*.bin GitHubPages/ESPWebTools/esp32Firmware - cd F1-Notifications/build - find . -print | grep -i .bin$ | xargs -i cp --parent {} ../../GitHubPages/ESPWebTools/cyd - cd ../.. - - # Copy boot_app0.bin to the esp32Firmware folder. This file will be common for all esp32 boards (i think) - # Using a version agnostic search to find the file. grep -m 1 makes sure only one file is copied in case multiple versions are installed. - find /home/runner/.arduino15/packages/esp32/hardware/esp32/ -type f | grep -i -m 1 boot_app0.bin$ | xargs -i cp {} GitHubPages/ESPWebTools/cyd - - # Build Matrix Arduino Code - - uses: arduino/compile-sketches@v1 - name: Compile Matrix code - with: - fqbn: "esp32:esp32:esp32" - platforms: | - - name: esp32:esp32 - source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - # No need to specify ESP-libraries as these are installed with the platform (on the line above). - # Downloading SpeedyStepper from source to get correct casing on import of Arduino.h (spelled arduino.h in the version from Arduino Library Manager) - libraries: | - - name: ImageFetcher - source-url: https://github.com/witnessmenow/file-fetcher-arduino.git - - name: ESP32 HUB75 LED MATRIX PANEL DMA Display - source-url: https://github.com/witnessmenow/ESP32-HUB75-MatrixPanel-I2S-DMA.git - - name: Adafruit GFX Library - sketch-paths: | - - F1-Notifications - enable-warnings-report: true - verbose: false - cli-compile-flags: | - - --build-property - - compiler.cpp.extra_flags=-DMATRIX_DISPLAY - - --export-binaries - - # Copy binaries to GitHubPages folder for publishing - - name: Copy compiled binaries to Matrix + + cp .pio/build/cyd/bootloader.bin GitHubPages/ESPWebTools/cyd + cp .pio/build/cyd/partitions.bin GitHubPages/ESPWebTools/cyd + cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/cyd + cp .pio/build/cyd/firmware.bin GitHubPages/ESPWebTools/cyd + + #Build CYD2USB + - name: Build CYD2USB + run: platformio run -e cyd2usb + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: CYD Firmware + path: .pio/build/cyd2usb/firmware.bin + if-no-files-found: error + - name: Copy compiled binaries for webflash + run: | + mkdir GitHubPages/ESPWebTools/cyd2usb + + # Copy the manifest file for the cyd2usb + cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/cyd2usb + + cp .pio/build/cyd2usb/bootloader.bin GitHubPages/ESPWebTools/cyd2usb + cp .pio/build/cyd2usb/partitions.bin GitHubPages/ESPWebTools/cyd2usb + cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/cyd + cp .pio/build/cyd2usb/firmware.bin GitHubPages/ESPWebTools/cyd2usb + + #Build Matrix + - name: Build Matrix + run: platformio run -e trinity + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: CYD Firmware + path: .pio/build/trinity/firmware.bin + if-no-files-found: error + - name: Copy compiled binaries for webflash run: | - # Make matrix directory mkdir GitHubPages/ESPWebTools/matrix - # Copy the manifest file for the matrix + # Copy the manifest file for the Matrix cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/matrix - - cd F1-Notifications/build - find . -print | grep -i .bin$ | xargs -i cp --parent {} ../../GitHubPages/ESPWebTools/matrix - cd ../.. - - # Copy boot_app0.bin to the esp32Firmware folder. This file will be common for all esp32 boards (i think) - # Using a version agnostic search to find the file. grep -m 1 makes sure only one file is copied in case multiple versions are installed. - find /home/runner/.arduino15/packages/esp32/hardware/esp32/ -type f | grep -i -m 1 boot_app0.bin$ | xargs -i cp {} GitHubPages/ESPWebTools/matrix + + cp .pio/build/trinity/bootloader.bin GitHubPages/ESPWebTools/matrix + cp .pio/build/trinity/partitions.bin GitHubPages/ESPWebTools/matrix + cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/matrix + cp .pio/build/trinity/firmware.bin GitHubPages/ESPWebTools/matrix # Build GitHub Page - name: Setup Github Page @@ -156,4 +137,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 \ No newline at end of file + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index f89f4bd..321ab62 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -3,79 +3,51 @@ name: Test compiling arduino code on: # Runs on all branches but main, when a arduino file is changed push: - branches-ignore : ["main"] + branches-ignore: ["main"] paths: - - '**.ino' - - '**.h' - - '**.yml' + - "**.ino" + - "**.h" + - "**.yml" + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: # Build job build: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - - # Install the Arduino CLI - - name: Install Arduino CLI - uses: arduino/setup-arduino-cli@v1 - - # Install TFT_eSPI - - name: Install TFT_eSPI library to prepare for modifications + - uses: actions/checkout@v4 + - uses: actions/cache@v3 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install PlatformIO run: | - arduino-cli lib install TFT_eSPI + python -m pip install --upgrade pip + pip install --upgrade platformio - # Copy user_setup from repo to TFT_eSPI folder - - name: Copy User_Setup.h for TFT_eSPI - run: | - \cp -fR DisplayConfig/User_Setup.h ~/Arduino/libraries/TFT_eSPI/ + #Build CYD + - name: Build CYD + run: platformio run -e cyd - # Build CYD Arduino Code - - uses: arduino/compile-sketches@v1 - name: Compile CYD code - continue-on-error: true - with: - fqbn: "esp32:esp32:esp32" - platforms: | - - name: esp32:esp32 - source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - # No need to specify ESP-libraries as these are installed with the platform (on the line above). - libraries: | - - name: ImageFetcher - source-url: https://github.com/witnessmenow/file-fetcher-arduino.git - - name: WiFiManager - - name: ESP_DoubleResetDetector - - name: ArduinoJson - - name: ezTime - - name: UniversalTelegramBot - - name: PNGdec - sketch-paths: | - - F1-Notifications - enable-warnings-report: true - verbose: false + #Build CYD2USB + - name: Build CYD2USB + run: platformio run -e cyd2usb - # Build Matrix Arduino Code - - uses: arduino/compile-sketches@v1 - name: Compile Matrix code - with: - fqbn: "esp32:esp32:esp32" - platforms: | - - name: esp32:esp32 - source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - # No need to specify ESP-libraries as these are installed with the platform (on the line above). - # Downloading SpeedyStepper from source to get correct casing on import of Arduino.h (spelled arduino.h in the version from Arduino Library Manager) - libraries: | - - name: ImageFetcher - source-url: https://github.com/witnessmenow/file-fetcher-arduino.git - - name: ESP32 HUB75 LED MATRIX PANEL DMA Display - source-url: https://github.com/witnessmenow/ESP32-HUB75-MatrixPanel-I2S-DMA.git - - name: Adafruit GFX Library - sketch-paths: | - - F1-Notifications - enable-warnings-report: true - verbose: false - cli-compile-flags: | - - --build-property - - compiler.cpp.extra_flags=-DMATRIX_DISPLAY + #Build Matrix + - name: Build Matrix + run: platformio run -e trinity diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..144199b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pio +.vscode/* +.DS_Store + diff --git a/F1-Notifications/F1-Notifications.ino b/F1-Notifications/F1-Notifications.ino index bd7ec89..5e76928 100644 --- a/F1-Notifications/F1-Notifications.ino +++ b/F1-Notifications/F1-Notifications.ino @@ -20,22 +20,21 @@ // (Uncomment the required #define) // 1. Cheap yellow display (Using TFT-eSPI library) -//#define YELLOW_DISPLAY +// #define YELLOW_DISPLAY // 2. Matrix Displays (Like the ESP32 Trinity) -//#define MATRIX_DISPLAY +// #define MATRIX_DISPLAY // If no defines are set, it will default to CYD #if !defined(YELLOW_DISPLAY) && !defined(MATRIX_DISPLAY) #define YELLOW_DISPLAY // Default to Yellow Display for display type #endif - // ---------------------------- // Library Defines - Need to be defined before library import // ---------------------------- -#define ESP_DRD_USE_SPIFFS true +#define ESP_DRD_USE_SPIFFS true // ---------------------------- // Standard Libraries @@ -62,7 +61,7 @@ // A library for checking if the reset button has been pressed twice // Can be used to enable config mode // Can be installed from the library manager (Search for "ESP_DoubleResetDetector") -//https://github.com/khoih-prog/ESP_DoubleResetDetector +// https://github.com/khoih-prog/ESP_DoubleResetDetector #include // Library used for parsing Json from the API responses @@ -101,8 +100,7 @@ #include "raceLogic.h" -#include "wifiManager.h" - +#include "wifiManagerHandler.h" WiFiClientSecure secured_client; @@ -116,13 +114,13 @@ FileFetcher fileFetcher(secured_client); #include "cheapYellowLCD.h" CheapYellowDisplay cyd; -F1Display* f1Display = &cyd; +F1Display *f1Display = &cyd; #elif defined MATRIX_DISPLAY #include "matrixDisplay.h" MatrixDisplay matrixDisplay; -F1Display* f1Display = &matrixDisplay; +F1Display *f1Display = &matrixDisplay; #endif // ---------------------------- @@ -131,7 +129,8 @@ UniversalTelegramBot bot("", secured_client); F1Config f1Config; -void setup() { +void setup() +{ // put your setup code here, to run once: Serial.begin(115200); @@ -141,7 +140,8 @@ void setup() { bool forceConfig = false; drd = new DoubleResetDetector(DRD_TIMEOUT, DRD_ADDRESS); - if (drd->detectDoubleReset()) { + if (drd->detectDoubleReset()) + { Serial.println(F("Forcing config mode as there was a Double reset detected")); forceConfig = true; } @@ -160,7 +160,8 @@ void setup() { } Serial.println("\r\nInitialisation done."); - if (!f1Config.fetchConfigFile()) { + if (!f1Config.fetchConfigFile()) + { // Failed to fetch config file, need to launch Wifi Manager forceConfig = true; } @@ -171,10 +172,11 @@ void setup() { // Set WiFi to station mode and disconnect from an AP if it was Previously // connected - //WiFi.mode(WIFI_STA); - //WiFi.begin(ssid, password); + // WiFi.mode(WIFI_STA); + // WiFi.begin(ssid, password); - while (WiFi.status() != WL_CONNECTED) { + while (WiFi.status() != WL_CONNECTED) + { Serial.print("."); delay(500); } @@ -185,7 +187,8 @@ void setup() { Serial.println(WiFi.localIP()); secured_client.setCACert(github_server_cert); - while (fetchRaceJson(fileFetcher) != 1) { + while (fetchRaceJson(fileFetcher) != 1) + { Serial.println("failed to get Race Json"); Serial.println("will try again in 10 seconds"); delay(1000 * 10); @@ -193,7 +196,6 @@ void setup() { Serial.println("Fetched races.json File"); - Serial.println("Waiting for time sync"); waitForSync(); @@ -207,48 +209,56 @@ void setup() { Serial.println(myTZ.dateTime()); Serial.println("-------------------------"); - //sendNotificationOfNextRace(&bot, f1Config.roundOffset); - + // sendNotificationOfNextRace(&bot, f1Config.roundOffset); } bool notificaitonEventRaised = false; -void sendNotification() { +void sendNotification() +{ // Cause it could be set to the image one - if (f1Config.isTelegramConfigured()) { + if (f1Config.isTelegramConfigured()) + { secured_client.setCACert(TELEGRAM_CERTIFICATE_ROOT); Serial.println("Sending notifcation"); f1Config.currentRaceNotification = sendNotificationOfNextRace(&bot); - if (!f1Config.currentRaceNotification) { - //Notificaiton failed, raise event again + if (!f1Config.currentRaceNotification) + { + // Notificaiton failed, raise event again Serial.println("Notfication failed"); - setEvent( sendNotification, getNotifyTime() ); - } else { + setEvent(sendNotification, getNotifyTime()); + } + else + { notificaitonEventRaised = false; f1Config.saveConfigFile(); } - } else { + } + else + { Serial.println("Would have sent Notification now, but telegram is not configured"); - + notificaitonEventRaised = false; f1Config.currentRaceNotification = true; f1Config.saveConfigFile(); } - } bool first = true; -int minuteCounter = 60; //kick off fetch first time +int minuteCounter = 60; // kick off fetch first time -void loop() { +void loop() +{ drd->loop(); // Every hour we will refresh the Race JSON from Github - if (minuteCounter >= 60) { + if (minuteCounter >= 60) + { secured_client.setCACert(github_server_cert); - while (fetchRaceJson(fileFetcher) != 1) { + while (fetchRaceJson(fileFetcher) != 1) + { Serial.println("failed to get Race Json"); Serial.println("will try again in 10 seconds"); delay(1000 * 10); @@ -256,15 +266,18 @@ void loop() { minuteCounter = 0; } - if (first || minuteChanged()) { - minuteCounter ++; + if (first || minuteChanged()) + { + minuteCounter++; bool newRace = getNextRace(f1Config.roundOffset, f1Config.currentRaceNotification, f1Display, first); - if (newRace) { + if (newRace) + { f1Config.saveConfigFile(); } - if (!f1Config.currentRaceNotification && !notificaitonEventRaised) { - //we have never notified about this race yet, so we'll raise an event - setEvent( sendNotification, getNotifyTime() ); + if (!f1Config.currentRaceNotification && !notificaitonEventRaised) + { + // we have never notified about this race yet, so we'll raise an event + setEvent(sendNotification, getNotifyTime()); notificaitonEventRaised = true; Serial.print("Raised event for: "); Serial.println(myTZ.dateTime(getNotifyTime(), UTC_TIME, f1Config.timeFormat)); diff --git a/F1-Notifications/cheapYellowLCD.h b/F1-Notifications/cheapYellowLCD.h index 613e6cc..0ba1763 100644 --- a/F1-Notifications/cheapYellowLCD.h +++ b/F1-Notifications/cheapYellowLCD.h @@ -1,11 +1,12 @@ #include "display.h" #include "getImage.h" +#include "util.h" #include // A library for interfacing with LCD displays // // Can be installed from the library manager (Search for "TFT_eSPI") -//https://github.com/Bodmer/TFT_eSPI +// https://github.com/Bodmer/TFT_eSPI #include // For decoding png files @@ -24,20 +25,27 @@ PNG png; fs::File myfile; -void * myOpen(const char *filename, int32_t *size) { +void *myOpen(const char *filename, int32_t *size) +{ myfile = SPIFFS.open(filename); *size = myfile.size(); return &myfile; } -void myClose(void *handle) { - if (myfile) myfile.close(); +void myClose(void *handle) +{ + if (myfile) + myfile.close(); } -int32_t myRead(PNGFILE *handle, uint8_t *buffer, int32_t length) { - if (!myfile) return 0; +int32_t myRead(PNGFILE *handle, uint8_t *buffer, int32_t length) +{ + if (!myfile) + return 0; return myfile.read(buffer, length); } -int32_t mySeek(PNGFILE *handle, int32_t position) { - if (!myfile) return 0; +int32_t mySeek(PNGFILE *handle, int32_t position) +{ + if (!myfile) + return 0; return myfile.seek(position); } @@ -49,127 +57,131 @@ void PNGDraw(PNGDRAW *pDraw) tft.pushImage(0, pDraw->y, pDraw->iWidth, 1, usPixels); } -class CheapYellowDisplay: public F1Display { - public: - - void displaySetup() { - - Serial.println("cyd display setup"); - setWidth(320); - setHeight(240); - - // Start the tft display and set it to black - tft.init(); - tft.setRotation(1); - tft.fillScreen(TFT_BLACK); - - state = unset; - } - - void displayPlaceHolder(const char* raceName, JsonObject races_sessions) { - - if(!isSameRace(raceName) || state != placeholder){ - setRaceName(raceName); - int imageFileStatus = getImage(raceName); - if(imageFileStatus){ - int imageDisplayStatus = displayImage(TRACK_IMAGE); - if(imageDisplayStatus == PNG_SUCCESS){ - //Image is displayed - tft.setTextColor(TFT_WHITE, TFT_BLACK); - int yPos = 188; - tft.drawCentreString("Next Race", screenCenterX, yPos, 2); - yPos += 16; - tft.drawCentreString(raceName, screenCenterX, yPos, 2); - String tempStr = String(getConvertedTime(races_sessions["gp"], "M d")); - yPos += 16; - tft.drawCentreString(tempStr, screenCenterX, yPos, 2); - state = placeholder; - return; - } +class CheapYellowDisplay : public F1Display +{ +public: + void displaySetup() + { + + Serial.println("cyd display setup"); + setWidth(320); + setHeight(240); + + // Start the tft display and set it to black + tft.init(); + tft.setRotation(1); + tft.fillScreen(TFT_BLACK); + + state = unset; + } + + void displayPlaceHolder(const char *raceName, JsonObject races_sessions) + { + + if (!isSameRace(raceName) || state != placeholder) + { + setRaceName(raceName); + int imageFileStatus = getImage(raceName); + if (imageFileStatus) + { + int imageDisplayStatus = displayImage(TRACK_IMAGE); + if (imageDisplayStatus == PNG_SUCCESS) + { + // Image is displayed + tft.setTextColor(TFT_WHITE, TFT_BLACK); + int yPos = 215; + String gpStartDateStr = String(getConvertedTime(races_sessions["gp"], "M d")); + String displayMessage = String(convertRaceName(raceName)) + " | " + gpStartDateStr; + tft.drawCentreString(displayMessage, screenCenterX, yPos, 4); + state = placeholder; + return; } - //Failed to display the image - displayRaceWeek(raceName, races_sessions); // For now } - - // if we reach here, the screen doesn't need to be updated - Serial.println("No need to update display"); - + // Failed to display the image + displayRaceWeek(raceName, races_sessions); // For now } - void displayRaceWeek(const char* raceName, JsonObject races_sessions) { - Serial.println("prts"); - tft.fillRect(0, 0, screenWidth, screenHeight, TFT_BLACK); - - // It's race week! - String tempStr = "Next Race: "; - tempStr += String(raceName); - - tft.drawString(tempStr, 5, 5, 2); - - int yValue = 21; - for (JsonPair kv : races_sessions) { - printSession( 5, - yValue, - sessionCodeToString(kv.key().c_str()), - getConvertedTime(kv.value().as())); - yValue += 16; - } - - state = raceweek; - } - - int displayImage(char *imageFileUri) { - tft.fillScreen(TFT_BLACK); - unsigned long lTime = millis(); - lTime = millis(); - Serial.println(imageFileUri); - - - - - int rc = png.open((const char *) imageFileUri, myOpen, myClose, myRead, mySeek, PNGDraw); - if (rc == PNG_SUCCESS) { - Serial.printf("image specs: (%d x %d), %d bpp, pixel type: %d\n", png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType()); - rc = png.decode(NULL, 0); - png.close(); - } else { - Serial.print("error code: "); - Serial.println(rc); - } - - Serial.print("Time taken to decode and display Image (ms): "); - Serial.println(millis() - lTime); - - return rc; + // if we reach here, the screen doesn't need to be updated + Serial.println("No need to update display"); + } + + void displayRaceWeek(const char *raceName, JsonObject races_sessions) + { + Serial.println("prts"); + tft.fillRect(0, 0, screenWidth, screenHeight, TFT_BLACK); + + // It's race week! + String tempStr = "Next Race: "; + tempStr += String(convertRaceName(raceName)); + + tft.drawString(tempStr, 5, 5, 2); + + int yValue = 21; + for (JsonPair kv : races_sessions) + { + printSession(5, + yValue, + sessionCodeToString(kv.key().c_str()), + getConvertedTime(kv.value().as())); + yValue += 16; } - void drawWifiManagerMessage(WiFiManager *myWiFiManager) { - Serial.println("Entered Conf Mode"); - tft.fillScreen(TFT_BLACK); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawCentreString("Entered Conf Mode:", screenCenterX, 5, 2); - tft.drawString("Connect to the following WIFI AP:", 5, 28, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString(myWiFiManager->getConfigPortalSSID(), 20, 48, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawString("Password:", 5, 64, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString("nomikey1", 20, 82, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - - tft.drawString("If it doesn't AutoConnect, use this IP:", 5, 110, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString(WiFi.softAPIP().toString(), 20, 128, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - + state = raceweek; + } + + int displayImage(char *imageFileUri) + { + tft.fillScreen(TFT_BLACK); + unsigned long lTime = millis(); + lTime = millis(); + Serial.println(imageFileUri); + + int rc = png.open((const char *)imageFileUri, myOpen, myClose, myRead, mySeek, PNGDraw); + if (rc == PNG_SUCCESS) + { + Serial.printf("image specs: (%d x %d), %d bpp, pixel type: %d\n", png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType()); + rc = png.decode(NULL, 0); + png.close(); } - - private: - void printSession(int x, int y, const char* sessionName, String sessionStartTime) { - String tempStr = String(sessionName); - tempStr += " "; - tempStr += sessionStartTime; - tft.drawString(tempStr, x, y, 2); + else + { + Serial.print("error code: "); + Serial.println(rc); } + Serial.print("Time taken to decode and display Image (ms): "); + Serial.println(millis() - lTime); + + return rc; + } + + void drawWifiManagerMessage(WiFiManager *myWiFiManager) + { + Serial.println("Entered Conf Mode"); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.drawCentreString("Entered Conf Mode:", screenCenterX, 5, 2); + tft.drawString("Connect to the following WIFI AP:", 5, 28, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString(myWiFiManager->getConfigPortalSSID(), 20, 48, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.drawString("Password:", 5, 64, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString("nomikey1", 20, 82, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + + tft.drawString("If it doesn't AutoConnect, use this IP:", 5, 110, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString(WiFi.softAPIP().toString(), 20, 128, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + } + +private: + void printSession(int x, int y, const char *sessionName, String sessionStartTime) + { + String tempStr = String(sessionName); + tempStr += " "; + tempStr += sessionStartTime; + tft.drawString(tempStr, x, y, 2); + } }; diff --git a/F1-Notifications/getImage.h b/F1-Notifications/getImage.h index 51cf7a9..55acd53 100644 --- a/F1-Notifications/getImage.h +++ b/F1-Notifications/getImage.h @@ -43,85 +43,110 @@ jjxDah2nGN59PRbxYvnKkKj9 // file name for where to save the image. #define TRACK_IMAGE "/track.png" -const char* getImageUrlForRace(const char* raceName) { +const char *getImageUrlForRace(const char *raceName) +{ if (strcmp(raceName, "Bahrain") == 0) { return "https://i.imgur.com/zUqArqi.png"; - } else if (strcmp(raceName, "Saudi Arabian") == 0) + } + else if (strcmp(raceName, "Saudi Arabian") == 0) { return "https://i.imgur.com/vx6MDSF.png"; - } else if (strcmp(raceName, "Australian") == 0) + } + else if (strcmp(raceName, "Australian") == 0) { return "https://i.imgur.com/ewrhVKU.png"; - } else if (strcmp(raceName, "Azerbaijan") == 0) + } + else if (strcmp(raceName, "Azerbaijan") == 0) { return "https://i.imgur.com/H2C6G2Q.png"; - } else if (strcmp(raceName, "Miami") == 0) + } + else if (strcmp(raceName, "Miami") == 0) { return "https://i.imgur.com/mwoQzCm.png"; - } else if (strcmp(raceName, "Emilia Romagna Grand Prix") == 0) + } + else if (strcmp(raceName, "Emilia Romagna Grand Prix") == 0) { - return "https://i.imgur.com/tvLjDeo.png"; // This is a called off image - } else if (strcmp(raceName, "Monaco") == 0) + return "https://i.imgur.com/fm6IygV.png"; + } + else if (strcmp(raceName, "Monaco") == 0) { return "https://i.imgur.com/Q48IRF1.png"; - } else if (strcmp(raceName, "Spanish") == 0) + } + else if (strcmp(raceName, "Spanish") == 0) { return "https://i.imgur.com/GdnHo69.png"; - } else if (strcmp(raceName, "Canadian") == 0) + } + else if (strcmp(raceName, "Canadian") == 0) { return "https://i.imgur.com/QNAli6L.png"; - } else if (strcmp(raceName, "Austrian") == 0) + } + else if (strcmp(raceName, "Austrian") == 0) { return "https://i.imgur.com/Xu4I91f.png"; - } else if (strcmp(raceName, "British") == 0) + } + else if (strcmp(raceName, "British") == 0) { return "https://i.imgur.com/R0snf2W.png"; - } else if (strcmp(raceName, "Hungarian") == 0) + } + else if (strcmp(raceName, "Hungarian") == 0) { return "https://i.imgur.com/R0snf2W.png"; - } else if (strcmp(raceName, "Belgian") == 0) + } + else if (strcmp(raceName, "Belgian") == 0) { return "https://i.imgur.com/Hr3HUGP.png"; - } else if (strcmp(raceName, "Dutch") == 0) + } + else if (strcmp(raceName, "Dutch") == 0) { return "https://i.imgur.com/fwyHAy5.png"; - } else if (strcmp(raceName, "Italian") == 0) + } + else if (strcmp(raceName, "Italian") == 0) { return "https://i.imgur.com/KrRzWhh.png"; - } else if (strcmp(raceName, "Singapore") == 0) + } + else if (strcmp(raceName, "Singapore") == 0) { return "https://i.imgur.com/di1xFkV.png"; - } else if (strcmp(raceName, "Japanese") == 0) + } + else if (strcmp(raceName, "Japanese") == 0) { return "https://i.imgur.com/BINBSn3.png"; - } else if (strcmp(raceName, "Qatar") == 0) + } + else if (strcmp(raceName, "Qatar") == 0) { return "https://i.imgur.com/YdpmY5o.png"; - } else if (strcmp(raceName, "United States") == 0) + } + else if (strcmp(raceName, "United States") == 0) { return "https://i.imgur.com/NzZNjF6.png"; - } else if (strcmp(raceName, "Mexican") == 0) + } + else if (strcmp(raceName, "Mexican") == 0) { return "https://i.imgur.com/gvUauKO.png"; - } else if (strcmp(raceName, "Brazilian") == 0) + } + else if (strcmp(raceName, "Brazilian") == 0) { return "https://i.imgur.com/3g4zz17.png"; - } else if (strcmp(raceName, "Las Vegas") == 0) + } + else if (strcmp(raceName, "Las Vegas") == 0) { return "https://i.imgur.com/er9A6G8.png"; - } else if (strcmp(raceName, "Abu Dhabi") == 0) + } + else if (strcmp(raceName, "Abu Dhabi") == 0) { return "https://i.imgur.com/QDwWXna.png"; - } + } // Image not found - return "https://imgur.com/FRXJ4do.png"; + return "https://i.imgur.com/FRXJ4do.png"; + //"https://i.imgur.com/tvLjDeo.png"; // This is a called off image } -int getImage(const char* raceName){ +int getImage(const char *raceName) +{ - const char* imageUrl = getImageUrlForRace(raceName); + const char *imageUrl = getImageUrlForRace(raceName); // In this example I reuse the same filename // over and over @@ -139,7 +164,7 @@ int getImage(const char* raceName){ } secured_client.setCACert(IMGUR_CERTIFICATE_ROOT); - bool gotImage = fileFetcher.getFile((char*)imageUrl, &f); + bool gotImage = fileFetcher.getFile((char *)imageUrl, &f); // Make sure to close the file! f.close(); diff --git a/F1-Notifications/githubCert.h b/F1-Notifications/githubCert.h index c733ae2..0505ffc 100644 --- a/F1-Notifications/githubCert.h +++ b/F1-Notifications/githubCert.h @@ -1,23 +1,23 @@ // Digi Cert Global Root Cert - as of 07/05/2023 const char *github_server_cert = "-----BEGIN CERTIFICATE-----\n" - "MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" - "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" - "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" - "QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" - "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" - "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" - "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" - "CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" - "nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" - "43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" - "T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" - "gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" - "BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" - "TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" - "DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" - "hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" - "06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" - "PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" - "YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" - "CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" - "-----END CERTIFICATE-----\n"; + "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n" + "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n" + "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" + "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n" + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n" + "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n" + "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n" + "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n" + "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n" + "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n" + "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n" + "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n" + "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n" + "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n" + "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n" + "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n" + "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n" + "MrY=\n" + "-----END CERTIFICATE-----\n"; diff --git a/F1-Notifications/matrixDisplay.h b/F1-Notifications/matrixDisplay.h index 3f24402..e36345d 100644 --- a/F1-Notifications/matrixDisplay.h +++ b/F1-Notifications/matrixDisplay.h @@ -1,12 +1,13 @@ #include "display.h" +#include "util.h" + #include // This is the library for interfacing with the display // Can be installed from the library manager (Search for "ESP32 MATRIX DMA") // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA - // ---------------------------- // Dependency Libraries - each one of these will need to be installed. // ---------------------------- @@ -19,9 +20,9 @@ // ------- Matrix Config ------ // ------------------------------------- -const int panelResX = 64; // Number of pixels wide of each INDIVIDUAL panel module. -const int panelResY = 64; // Number of pixels tall of each INDIVIDUAL panel module. -const int panel_chain = 1; // Total number of panels chained one to another +const int panelResX = 64; // Number of pixels wide of each INDIVIDUAL panel module. +const int panelResY = 64; // Number of pixels tall of each INDIVIDUAL panel module. +const int panel_chain = 1; // Total number of panels chained one to another MatrixPanel_I2S_DMA *dma_display = nullptr; @@ -31,187 +32,189 @@ uint16_t myRED = dma_display->color565(255, 0, 0); uint16_t myGREEN = dma_display->color565(0, 255, 0); uint16_t myBLUE = dma_display->color565(0, 0, 255); -class MatrixDisplay: public F1Display { - public: - - void displaySetup() { - - Serial.println("matrix display setup"); - setWidth(panelResX * panel_chain); - setHeight(panelResY); - - HUB75_I2S_CFG mxconfig( - panelResX, // module width - panelResY, // module height - panel_chain // Chain length - ); - - // If you are using a 64x64 matrix you need to pass a value for the E pin - // The trinity connects GPIO 18 to E. - // This can be commented out for any smaller displays (but should work fine with it) - mxconfig.gpio.e = 18; - - // May or may not be needed depending on your matrix - // Example of what needing it looks like: - // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/134#issuecomment-866367216 - mxconfig.clkphase = false; - - // Some matrix panels use different ICs for driving them and some of them have strange quirks. - // If the display is not working right, try this. - //mxconfig.driver = HUB75_I2S_CFG::FM6126A; - - dma_display = new MatrixPanel_I2S_DMA(mxconfig); - dma_display->begin(); +class MatrixDisplay : public F1Display +{ +public: + void displaySetup() + { + + Serial.println("matrix display setup"); + setWidth(panelResX * panel_chain); + setHeight(panelResY); + + HUB75_I2S_CFG mxconfig( + panelResX, // module width + panelResY, // module height + panel_chain // Chain length + ); + + // If you are using a 64x64 matrix you need to pass a value for the E pin + // The trinity connects GPIO 18 to E. + // This can be commented out for any smaller displays (but should work fine with it) + mxconfig.gpio.e = 18; + + // May or may not be needed depending on your matrix + // Example of what needing it looks like: + // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/134#issuecomment-866367216 + mxconfig.clkphase = false; + + // Some matrix panels use different ICs for driving them and some of them have strange quirks. + // If the display is not working right, try this. + // mxconfig.driver = HUB75_I2S_CFG::FM6126A; + + dma_display = new MatrixPanel_I2S_DMA(mxconfig); + dma_display->begin(); + } + void displayRaceWeek(const char *raceName, JsonObject races_sessions) + { + + const char *raceNameChanged = convertRaceName(raceName); + + // It's race week! + dma_display->fillScreen(myBLACK); + dma_display->setTextSize(1); // size 2 == 16 pixels high + dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line + + int16_t xOne, yOne; + uint16_t w, h; + + // This method updates the variables with what width (w) and height (h) + // the give text will have. + + dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); + + int xPosition = screenCenterX - w / 2; + dma_display->setTextColor(myBLUE); + dma_display->setCursor(xPosition, 2); + dma_display->print(raceNameChanged); + + int yValue = 12; + for (JsonPair kv : races_sessions) + { + printSession(yValue, + matrixSessionCodeToString(kv.key().c_str()), + getConvertedTime(kv.value().as(), "H:i")); + yValue += 10; } - void displayRaceWeek(const char* raceName, JsonObject races_sessions) { - - const char* raceNameChanged = updateRaceName(raceName); - - // It's race week! - dma_display->fillScreen(myBLACK); - dma_display->setTextSize(1); // size 2 == 16 pixels high - dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line - - int16_t xOne, yOne; - uint16_t w, h; - - // This method updates the variables with what width (w) and height (h) - // the give text will have. - - dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); - - int xPosition = screenCenterX - w / 2; - dma_display->setTextColor(myBLUE); - dma_display->setCursor(xPosition, 2); - dma_display->print(raceNameChanged); - - int yValue = 12; - for (JsonPair kv : races_sessions) { - printSession( yValue, - matrixSessionCodeToString(kv.key().c_str()), - getConvertedTime(kv.value().as(), "H:i")); - yValue += 10; - } + } + + void displayPlaceHolder(const char *raceName, JsonObject races_sessions) + { + + const char *raceNameChanged = updateRaceName(raceName); + + // Not yet race week + dma_display->fillScreen(myBLACK); + dma_display->setTextSize(1); // size 2 == 16 pixels high + dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line + + int16_t xOne, yOne; + uint16_t w, h; + + // This method updates the variables with what width (w) and height (h) + // the give text will have. + dma_display->getTextBounds("Next Race:", 0, 0, &xOne, &yOne, &w, &h); + int xPosition = screenCenterX - w / 2; + dma_display->setTextColor(myGREEN); + dma_display->setCursor(xPosition, 2); + dma_display->print("Next Race:"); + + // This method updates the variables with what width (w) and height (h) + // the give text will have. + + dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); + + xPosition = screenCenterX - w / 2; + dma_display->setTextColor(myBLUE); + dma_display->setCursor(xPosition, 10); + dma_display->print(raceNameChanged); + + printSession(20, + "GP:", + getConvertedTime(races_sessions["gp"], "M d")); + } + + int displayImage(char *imageFileUri) + { + return 0; + } + + void drawWifiManagerMessage(WiFiManager *myWiFiManager) + { + Serial.println("Entered Conf Mode"); + dma_display->fillScreen(myBLACK); + dma_display->setTextSize(1); // size 1 == 8 pixels high + dma_display->setTextWrap(false); + dma_display->setTextColor(myBLUE); + dma_display->setCursor(0, 0); + dma_display->print(myWiFiManager->getConfigPortalSSID()); + + dma_display->setTextWrap(true); + dma_display->setTextColor(myRED); + dma_display->setCursor(0, 8); + dma_display->print(WiFi.softAPIP()); + } + +private: + const char *matrixSessionCodeToString(const char *sessionCode) + { + if (strcmp(sessionCode, "fp1") == 0) + { + return "FP1:"; } - - void displayPlaceHolder(const char* raceName, JsonObject races_sessions) { - - const char* raceNameChanged = updateRaceName(raceName); - - // Not yet race week - dma_display->fillScreen(myBLACK); - dma_display->setTextSize(1); // size 2 == 16 pixels high - dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line - - int16_t xOne, yOne; - uint16_t w, h; - - // This method updates the variables with what width (w) and height (h) - // the give text will have. - dma_display->getTextBounds("Next Race:", 0, 0, &xOne, &yOne, &w, &h); - int xPosition = screenCenterX - w / 2; - dma_display->setTextColor(myGREEN); - dma_display->setCursor(xPosition, 2); - dma_display->print("Next Race:"); - - // This method updates the variables with what width (w) and height (h) - // the give text will have. - - dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); - - xPosition = screenCenterX - w / 2; - dma_display->setTextColor(myBLUE); - dma_display->setCursor(xPosition, 10); - dma_display->print(raceNameChanged); - - printSession( 20, - "GP:", - getConvertedTime(races_sessions["gp"], "M d")); + else if (strcmp(sessionCode, "fp2") == 0) + { + return "FP2:"; } - - int displayImage(char *imageFileUri) { - return 0; + else if (strcmp(sessionCode, "fp3") == 0) + { + return "FP3:"; } - - void drawWifiManagerMessage(WiFiManager *myWiFiManager) { - Serial.println("Entered Conf Mode"); - dma_display->fillScreen(myBLACK); - dma_display->setTextSize(1); // size 1 == 8 pixels high - dma_display->setTextWrap(false); - dma_display->setTextColor(myBLUE); - dma_display->setCursor(0, 0); - dma_display->print(myWiFiManager->getConfigPortalSSID()); - - dma_display->setTextWrap(true); - dma_display->setTextColor(myRED); - dma_display->setCursor(0, 8); - dma_display->print(WiFi.softAPIP()); + else if (strcmp(sessionCode, "qualifying") == 0) + { + return "Qual:"; } - - private: - - const char* matrixSessionCodeToString(const char* sessionCode) { - if (strcmp(sessionCode, "fp1") == 0) - { - return "FP1:"; - } else if (strcmp(sessionCode, "fp2") == 0) - { - return "FP2:"; - } else if (strcmp(sessionCode, "fp3") == 0) - { - return "FP3:"; - } else if (strcmp(sessionCode, "qualifying") == 0) - { - return "Qual:"; - } else if (strcmp(sessionCode, "sprintQualifying") == 0) - { - return "Sp Q:"; - } else if (strcmp(sessionCode, "sprint") == 0) - { - return "Spr:"; - } else if (strcmp(sessionCode, "gp") == 0) - { - return "Race:"; - } - - return "UNKNOWN"; + else if (strcmp(sessionCode, "sprintQualifying") == 0) + { + return "Sp Q:"; } - - const char* updateRaceName(const char* sessionCode) { - if (strcmp(sessionCode, "Emilia Romagna Grand Prix") == 0) - { - return "Imola"; - } - - return sessionCode; + else if (strcmp(sessionCode, "sprint") == 0) + { + return "Spr:"; + } + else if (strcmp(sessionCode, "gp") == 0) + { + return "Race:"; } - void printSession(int y, const char* sessionName, String sessionStartTime) { - - // Print Session Name on the left - dma_display->setTextColor(myRED); - dma_display->setCursor(1, y); - dma_display->print(sessionName); + return "UNKNOWN"; + } - Serial.println(sessionName); + void printSession(int y, const char *sessionName, String sessionStartTime) + { - // Print time on the right + // Print Session Name on the left + dma_display->setTextColor(myRED); + dma_display->setCursor(1, y); + dma_display->print(sessionName); - int16_t xOne, yOne; - uint16_t w, h; + Serial.println(sessionName); - // This method updates the variables with what width (w) and height (h) - // the give text will have. - Serial.println(sessionStartTime); - dma_display->getTextBounds(sessionStartTime, 0, 0, &xOne, &yOne, &w, &h); + // Print time on the right - int xPosition = screenWidth - w; + int16_t xOne, yOne; + uint16_t w, h; - Serial.println(xPosition); + // This method updates the variables with what width (w) and height (h) + // the give text will have. + Serial.println(sessionStartTime); + dma_display->getTextBounds(sessionStartTime, 0, 0, &xOne, &yOne, &w, &h); - dma_display->setCursor(xPosition, y); - dma_display->print(sessionStartTime); + int xPosition = screenWidth - w; - } + Serial.println(xPosition); + dma_display->setCursor(xPosition, y); + dma_display->print(sessionStartTime); + } }; diff --git a/F1-Notifications/raceLogic.h b/F1-Notifications/raceLogic.h index 158128f..120b6ca 100644 --- a/F1-Notifications/raceLogic.h +++ b/F1-Notifications/raceLogic.h @@ -4,18 +4,20 @@ #define RACE_FILE_NAME "/races.json" #define CURRENT_RACE_FILE_NAME "/current_races.json" -#define RACE_JSON_URL "https://raw.githubusercontent.com/sportstimes/f1/main/_db/f1/2023.json" +#define RACE_JSON_URL "https://raw.githubusercontent.com/sportstimes/f1/main/_db/f1/2024.json" time_t nextRaceStartUtc; Timezone myTZ; F1Config rl_f1Config; -void raceLogicSetup(F1Config f1Config) { +void raceLogicSetup(F1Config f1Config) +{ rl_f1Config = f1Config; } -bool isSessionInFuture(const char* sessionStartTime) { +bool isSessionInFuture(const char *sessionStartTime) +{ struct tm tm = {0}; // Parse date from UTC and convert to an epoch strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); @@ -24,7 +26,8 @@ bool isSessionInFuture(const char* sessionStartTime) { return UTC.now() < sessionEpoch; } -bool isRaceWeek(const char* sessionStartTime) { +bool isRaceWeek(const char *sessionStartTime) +{ struct tm tm = {0}; // Parse date from UTC and convert to an epoch strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); @@ -33,48 +36,57 @@ bool isRaceWeek(const char* sessionStartTime) { return UTC.now() > sixDaysBeforeRaceEpoch; } -String getConvertedTime(const char* sessionStartTime, const char* timeFormat = "") { +String getConvertedTime(const char *sessionStartTime, const char *timeFormat = "") +{ struct tm tm = {0}; // Parse date from UTC and convert to an epoch strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); time_t sessionEpoch = mktime(&tm); String timeFormatStr = rl_f1Config.timeFormat; - if (timeFormat[0] != 0) { + if (timeFormat[0] != 0) + { timeFormatStr = String(timeFormat); } return myTZ.dateTime(sessionEpoch, UTC_TIME, timeFormatStr); } -void printConvertedTime(const char* sessionName, const char* sessionStartTime) { +void printConvertedTime(const char *sessionName, const char *sessionStartTime) +{ String timeStr = getConvertedTime(sessionStartTime, ""); Serial.print(sessionName); Serial.print(": "); Serial.println(timeStr); - } -const char* sessionCodeToString(const char* sessionCode) { +const char *sessionCodeToString(const char *sessionCode) +{ if (strcmp(sessionCode, "fp1") == 0) { return "FP1: "; - } else if (strcmp(sessionCode, "fp2") == 0) + } + else if (strcmp(sessionCode, "fp2") == 0) { return "FP2: "; - } else if (strcmp(sessionCode, "fp3") == 0) + } + else if (strcmp(sessionCode, "fp3") == 0) { return "FP3: "; - } else if (strcmp(sessionCode, "qualifying") == 0) + } + else if (strcmp(sessionCode, "qualifying") == 0) { return "Qualifying: "; - } else if (strcmp(sessionCode, "sprint") == 0) + } + else if (strcmp(sessionCode, "sprint") == 0) { return "Sprint: "; - } else if (strcmp(sessionCode, "sprintQualifying") == 0) + } + else if (strcmp(sessionCode, "sprintQualifying") == 0) { return "Sprint Qualifying: "; - } else if (strcmp(sessionCode, "gp") == 0) + } + else if (strcmp(sessionCode, "gp") == 0) { return "Race: "; } @@ -82,33 +94,37 @@ const char* sessionCodeToString(const char* sessionCode) { return "UNKNOWN"; } -void printRaceTimes(const char* raceName, JsonObject races_sessions) { +void printRaceTimes(const char *raceName, JsonObject races_sessions) +{ Serial.print("Next Race: "); Serial.println(raceName); - for (JsonPair kv : races_sessions) { + for (JsonPair kv : races_sessions) + { printConvertedTime(sessionCodeToString(kv.key().c_str()), - kv.value().as()); + kv.value().as()); } - } -String createTelegramMessageString(const char* raceName, JsonObject races_sessions) { +String createTelegramMessageString(const char *raceName, JsonObject races_sessions) +{ String message = "Next Race: "; message += raceName; message += "\n"; message += "---------------------\n"; - for (JsonPair kv : races_sessions) { + for (JsonPair kv : races_sessions) + { String sessionName = String(sessionCodeToString(kv.key().c_str())); message += sessionName; - message += getConvertedTime(kv.value().as(), ""); + message += getConvertedTime(kv.value().as(), ""); message += "\n"; } return message; } -bool sendNotificationOfNextRace(UniversalTelegramBot *bot) { +bool sendNotificationOfNextRace(UniversalTelegramBot *bot) +{ StaticJsonDocument<112> filter; filter["name"] = true; @@ -121,27 +137,27 @@ bool sendNotificationOfNextRace(UniversalTelegramBot *bot) { DeserializationError error = deserializeJson(race, racesJson, DeserializationOption::Filter(filter)); - if (error) { + if (error) + { Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); racesJson.close(); return false; } - const char* races_name = race["name"]; + const char *races_name = race["name"]; JsonObject races_sessions = race["sessions"]; - printRaceTimes(races_name, races_sessions); Serial.print("Sending message to "); Serial.println(rl_f1Config.chatId); racesJson.close(); return bot->sendPhoto(rl_f1Config.chatId, "https://i.imgur.com/q3qsfSi.png", createTelegramMessageString(races_name, races_sessions)); - } -int fetchRaceJson(FileFetcher fileFetcher) { +int fetchRaceJson(FileFetcher fileFetcher) +{ // In this example I reuse the same filename // over and over if (SPIFFS.exists(RACE_FILE_NAME) == true) @@ -165,22 +181,26 @@ int fetchRaceJson(FileFetcher fileFetcher) { return gotFile; } -bool saveCurrentRaceToFile(const JsonObject& raceJson) { +bool saveCurrentRaceToFile(const JsonObject &raceJson) +{ - if(raceJson.isNull()){ + if (raceJson.isNull()) + { Serial.println("Race data is null, nothing to save"); return false; } - + File currentRaceFile = SPIFFS.open(CURRENT_RACE_FILE_NAME, "w"); - if (!currentRaceFile) { + if (!currentRaceFile) + { Serial.println("failed to open config file for writing"); return false; } Serial.println("Saving Race Json"); serializeJsonPretty(raceJson, Serial); - if (serializeJson(raceJson, currentRaceFile) == 0) { + if (serializeJson(raceJson, currentRaceFile) == 0) + { Serial.println(F("Failed to write to file")); return false; } @@ -188,7 +208,8 @@ bool saveCurrentRaceToFile(const JsonObject& raceJson) { return true; } -bool getNextRace(int &offset, bool ¬ificationSent, F1Display* f1Display, bool forceRaceFileSave) { +bool getNextRace(int &offset, bool ¬ificationSent, F1Display *f1Display, bool forceRaceFileSave) +{ StaticJsonDocument<112> filter; @@ -204,7 +225,8 @@ bool getNextRace(int &offset, bool ¬ificationSent, F1Display* f1Display, bool DeserializationError error = deserializeJson(doc, racesJson, DeserializationOption::Filter(filter)); - if (error) { + if (error) + { Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); return false; @@ -215,49 +237,63 @@ bool getNextRace(int &offset, bool ¬ificationSent, F1Display* f1Display, bool time_t timeNow = UTC.now(); Serial.println(); Serial.println("UTC: " + UTC.dateTime()); - for (int i = 0; i < racesAmount; i++) { + for (int i = 0; i < racesAmount; i++) + { - //serializeJsonPretty(races[i], Serial); + // serializeJsonPretty(races[i], Serial); - const char* races_name = races[i]["name"]; + const char *races_name = races[i]["name"]; JsonObject races_sessions = races[i]["sessions"]; - const char* race_sessions_gp = races_sessions["gp"]; // "2023-03-05T15:00:00Z" + const char *race_sessions_gp = races_sessions["gp"]; // "2023-03-05T15:00:00Z" bool raceCanceled = races[i]["canceled"].as(); struct tm tm = {0}; // Convert to tm struct - //Sample format: 2023-03-17T13:30:00Z + // Sample format: 2023-03-17T13:30:00Z strptime(race_sessions_gp, "%Y-%m-%dT%H:%M:%S", &tm); nextRaceStartUtc = mktime(&tm); - if (!raceCanceled && timeNow < nextRaceStartUtc) { + if (!raceCanceled && timeNow < nextRaceStartUtc) + { bool newRace = false; int roundNumber = races[i]["round"]; - if (roundNumber != offset) { - if (saveCurrentRaceToFile(races[i])) { + if (roundNumber != offset) + { + if (saveCurrentRaceToFile(races[i])) + { offset = roundNumber; notificationSent = false; Serial.println("New Race"); newRace = true; - } else { + } + else + { Serial.println("Got new race, but couldn't save JSON to file"); } - - } else { + } + else + { Serial.println("Same Race as before"); - if (forceRaceFileSave) { - if (saveCurrentRaceToFile(races[i])) { + if (forceRaceFileSave) + { + if (saveCurrentRaceToFile(races[i])) + { Serial.println("(Forced Save) Saved race to file"); - } else { + } + else + { Serial.println("(Forced Save) Couldn't save JSON to file"); } } } - if (isRaceWeek(race_sessions_gp)) { + if (isRaceWeek(race_sessions_gp)) + { f1Display->displayRaceWeek(races_name, races_sessions); - } else { + } + else + { f1Display->displayPlaceHolder(races_name, races_sessions); } @@ -265,14 +301,13 @@ bool getNextRace(int &offset, bool ¬ificationSent, F1Display* f1Display, bool racesJson.close(); return newRace; } - } racesJson.close(); return false; - } -time_t getNotifyTime() { +time_t getNotifyTime() +{ time_t t = nextRaceStartUtc - (6 * SECS_PER_DAY); // Probably should make this smarter so it's not sending notifications in the middle of the night! diff --git a/F1-Notifications/util.h b/F1-Notifications/util.h new file mode 100644 index 0000000..f205d17 --- /dev/null +++ b/F1-Notifications/util.h @@ -0,0 +1,12 @@ + +const char *convertRaceName(const char *raceName) +{ + if (strcmp(raceName, "Emilia Romagna Grand Prix") == 0) + { + return "Imola"; + } + else + { + return raceName; + } +} \ No newline at end of file diff --git a/F1-Notifications/wifiManager.h b/F1-Notifications/wifiManagerHandler.h similarity index 100% rename from F1-Notifications/wifiManager.h rename to F1-Notifications/wifiManagerHandler.h diff --git a/GitHubPages/index.html b/GitHubPages/index.html index 335ad58..dd20c6d 100644 --- a/GitHubPages/index.html +++ b/GitHubPages/index.html @@ -22,6 +22,12 @@

F1 Notifications


+
+ CYD2USB + + +
+
Matrix diff --git a/README.md b/README.md index 21e4961..bd33b3f 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,15 @@ Currently this project runs on two types of hardware: ### "Cheap Yellow Display" (CYD) An ESP32 With Built in 320x240 LCD with Touch Screen (ESP32-2432S028R), buy from wherever works out cheapest for you: -- [Aliexpress*](https://s.click.aliexpress.com/e/_DkSpIjB) -- [Aliexpress*](https://s.click.aliexpress.com/e/_DkcmuCh) + +- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkSpIjB) +- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkcmuCh) - [Aliexpress](https://www.aliexpress.com/item/1005004502250619.htm) -- [Makerfabs](https://www.makerfabs.com/sunton-esp32-2-8-inch-tft-with-touch.html) +- [Makerfabs](https://www.makerfabs.com/sunton-esp32-2-8-inch-tft-with-touch.html) ### Matrix panel -It's built to work with the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity), an open source board I created for controlling Hub75 Matrix panels, but it will does work with any ESP32 that breaks out enough pins. +It's built to work with the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity), an open source board I created for controlling Hub75 Matrix panels, but it will does work with any ESP32 that breaks out enough pins. The display it uses is a 64x64 HUB75 Matrix Panel. @@ -48,9 +49,9 @@ All the parts can be purchased from Makerfabs.com: - [ESP32 Trinity](https://www.makerfabs.com/esp32-trinity.html) - [64 x 64 Matrix Panel](https://www.makerfabs.com/64x64-rgb-led-matrix-3mm-pitch.html) - Optional: [5V Power Supply](https://www.makerfabs.com/5v-6a-ac-dc-power-adapter-with-cable.html) - You can alternatively use a USB-C power supply - - \* = Affilate Link - + +\* = Affilate Link + ### BYOD (Bring your own display) I've tried to design this project to be modular and have abstracted the display code behind an interface, so it should be pretty easy to get it up and running with a different type of display. @@ -59,37 +60,43 @@ I've tried to design this project to be modular and have abstracted the display These steps will only need to be run once. -### Telegram Setup +### Step 1 - Telegram Setup (Optional) -To get notified about the races, you will need some things on Telegram. +To get notified about the races, you will need some things on Telegram. - Install Telegram and setup an account. - Search for a user called "botFather" and follow instructions to create a new bot. Keep note of this bot token - Start the chat with the created bot. - Search for a user called "myIdBot" and follow instructions to get your chat ID. Keep note of the chatID. -### Web Flash +### Step 2 - Flash the Project This project can be flashed directly from your browser [here](https://witnessmenow.github.io/F1-Arduino-Notifications/) (Chrome & Edge only) -- Click the "CYD" button for the "Cheap Yellow Display" -- or Click the "Matrix" button for the ESP32 Trinity + +- For the "Cheap Yellow Display" (CYD): + - If your CYD has one USB port, Click the "CYD" button + - If it has two USB ports, Click the "CYD2USB" button +- For ESP32 Trinity/Matrix panel + - Click the "Matrix" button This webflash code is automatically built from the main branch of this repo, so it will always be up to date. Note: If you want to program this project yourself without the webflash, follow the **Code** steps below. -### WiFiManager +### Step 3 - Adding your Wifi, Timezone and Telegram Details In order to enter your wifi details, the project will host it's own wifi network. Connect to it using your phone. + - SSID: f1Thing - Password: nomikey1 -You should be automatically redirected to the config page. +You should be automatically redirected to the config page. + - Click Config - Enter your WIfi details - Enter your **Time Zone** - More info [here](https://github.com/ropg/ezTime#setlocation) -- Enter your **Bot Token** that you retrieved at the earlier step. -- Enter your **Chat ID** that you retrieved at the earlier step. +- (Optional) Enter your **Bot Token** that you retrieved at the earlier step. +- (Optional) Enter your **Chat ID** that you retrieved at the earlier step. - You can leave the other options - Click save @@ -99,44 +106,64 @@ The project should now be setup and start displaying the next race details! ## Code -If you want to program the devices yourself using the Arudino IDE, you will need to do the following to get it working +If you want to program this project manually, there are two options -The following libraries need to be installed for this project to work: +### PlatformIO + +PlatformIO is the easiest way to code this project. + +In the [platformio.ini](platformio.ini), there are several environments defined for the different boards -### Common Libraries +| Environment | Description | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | +| env:cyd | For the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) | +| env:cyd2usb | For the Cheap Yellow Display with two USB ports | +| env:trinity | For the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity) (or generic ESP32 wired to the matrix panel the same) | -| Library Name/Link | Purpose | Library manager | -| ------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------- | -| [WifiManager - By Tzapu](https://github.com/tzapu/WiFiManager) | Captive portal for configuring the WiFi | Yes ("WifiManager" | -| [ESP_DoubleResetDetector](https://github.com/khoih-prog/ESP_DoubleResetDetector) | Detecting double pressing the reset button | Yes ("ESP_DoubleResetDetector") | -| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | For parsing JSON | Yes ("Arduino Json") | -| [ezTime](https://github.com/ropg/ezTime) | For handling timezones | Yes ("eztime") | -| [UniversalTelegramBot](https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot) | Telegram bots for your ESP | Yes ("UniversalTelegramBot") | -| [FileFetcher](https://github.com/witnessmenow/file-fetcher-arduino) | For fetching files/images from the web | No | +When you select the environment, it will automatically install the right libraries and set the configurations in the code. + +### Arduino IDE + +If you want to use the Arduino IDE, you will need to do the following to get it workin + +The following libraries need to be installed for this project to work: + +| Library Name/Link | Purpose | Library manager | +| -------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------- | +| [WifiManager - By Tzapu](https://github.com/tzapu/WiFiManager) | Captive portal for configuring the WiFi | Yes ("WifiManager" | +| [ESP_DoubleResetDetector](https://github.com/khoih-prog/ESP_DoubleResetDetector) | Detecting double pressing the reset button | Yes ("ESP_DoubleResetDetector") | +| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | For parsing JSON | Yes ("Arduino Json") | +| [ezTime](https://github.com/ropg/ezTime) | For handling timezones | Yes ("eztime") | +| [UniversalTelegramBot](https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot) | Telegram bots for your ESP | Yes ("UniversalTelegramBot") | +| [FileFetcher](https://github.com/witnessmenow/file-fetcher-arduino) | For fetching files/images from the web | No, download from Github | ### Cheap Yellow Display Specific libraries -| Library Name/Link | Purpose | Library manager | -| ------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------- | -| [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | For controlling the LCD Display | Yes ("tft_espi") | -| [PNGdec](https://github.com/bitbank2/PNGdec) | For decoding png images | Yes ("PNGdec") | +| Library Name/Link | Purpose | Library manager | +| ---------------------------------------------- | ------------------------------- | ---------------- | +| [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | For controlling the LCD Display | Yes ("tft_espi") | +| [PNGdec](https://github.com/bitbank2/PNGdec) | For decoding png images | Yes ("PNGdec") | ### Matrix Panel Specific libraries -| Library Name/Link | Purpose | Library manager | -| ------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------- | -| [ESP32-HUB75-MatrixPanel-I2S-DMA](https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA) | For controlling the LED Matrix | Yes ("ESP32 MATRIX DMA") | -| [Adafruit GFX library](https://github.com/adafruit/Adafruit-GFX-Library) | Dependancy of the Matrix library | Yes ("Adafruit GFX") | - +| Library Name/Link | Purpose | Library manager | +| ------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------ | +| [ESP32-HUB75-MatrixPanel-I2S-DMA](https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA) | For controlling the LED Matrix | Yes ("ESP32 MATRIX DMA") | +| [Adafruit GFX library](https://github.com/adafruit/Adafruit-GFX-Library) | Dependancy of the Matrix library | Yes ("Adafruit GFX") | ### Cheap Yellow Display Display Config The CYD version of the project makes use of [TFT_eSPI library by Bodmer](https://github.com/Bodmer/TFT_eSPI). -TFT_eSPI is configured using a "User_Setup.h" file in the library folder, you will need to replace this file with the one in the `DisplayConfig` folder of this repo. +TFT_eSPI is configured using a "User_Setup.h" file in the library folder, you will need to replace this file with the one from the CYD repo. + +- CYD (CYD with single USB): https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/User_Setup.h +- CYD2USB (CYD with 2 USB): https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/CYD2USB/User_Setup.h ### Display Selection At the top of the `F1-Notifications.ino` file, there is a section labeled "Display Type", follow the instructions there for how to enable the different displays. -Note: By default it will use the Cheap Yellow Display +By default it will use the Cheap Yellow Display + +Note: CYD and CYD2USB both use the `YELLOW_DISPLAY` define. diff --git a/images/320x240/Imola.png b/images/320x240/Imola.png new file mode 100644 index 0000000000000000000000000000000000000000..266b31245099595cdbdff0289ffa413bd32f4a0f GIT binary patch literal 32558 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 literal 0 HcmV?d00001 diff --git a/images/320x240/6- Imola.png b/images/320x240/calledOff.png similarity index 100% rename from images/320x240/6- Imola.png rename to images/320x240/calledOff.png diff --git a/images/Readme.md b/images/Readme.md new file mode 100644 index 0000000..68dfff5 --- /dev/null +++ b/images/Readme.md @@ -0,0 +1,10 @@ +## To Get The Images + +- Go to the Forumula1.com website and go to the schedule page. This year is https://www.formula1.com/en/racing/2024.html +- Right click on the track layout under the race you want and click "copy image" +- Paste the image into Paint.net, and resize the canvas to 320x240 pixels. +- Save the image. +- Upload the image to imgur.com +- Click the 3 dots when you hover over the image and click "Get Share links" +- Click "copy link" on the BBCode option +- Add an entry for the race based on the race name from the race source (this year is https://raw.githubusercontent.com/sportstimes/f1/main/_db/f1/2024.json) to the `getImage.h` file, Remove the "[img]" tags around the URL diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..3f65b88 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,83 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +src_dir = F1-Notifications +default_envs = cyd + +[env] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + khoih-prog/ESP_DoubleResetDetector@^1.3.2 + bblanchon/ArduinoJson@^6.21.3 + wnatth3/WiFiManager@^2.0.16-rc.2 + ropg/ezTime@^0.8.3 + witnessmenow/UniversalTelegramBot@^1.3.0 + https://github.com/witnessmenow/file-fetcher-arduino.git +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +upload_speed = 921600 +#lib_ldf_mode = deep+ + +[common_cyd] +lib_deps = + ${env.lib_deps} + bodmer/TFT_eSPI@^2.5.33 + bitbank2/PNGdec@^1.0.2 +build_flags = + -DYELLOW_DISPLAY + -DUSER_SETUP_LOADED + -DILI9341_2_DRIVER + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_MISO=12 + -DTFT_MOSI=13 + -DTFT_SCLK=14 + -DTFT_CS=15 + -DTFT_DC=2 + -DTFT_RST=-1 + -DTFT_BL=21 + -DTFT_BACKLIGHT_ON=HIGH + -DTFT_BACKLIGHT_OFF=LOW + -DLOAD_GLCD + -DSPI_FREQUENCY=55000000 + -DSPI_READ_FREQUENCY=20000000 + -DSPI_TOUCH_FREQUENCY=2500000 + -DLOAD_FONT2 + -DLOAD_FONT4 + -DLOAD_FONT6 + -DLOAD_FONT7 + -DLOAD_FONT8 + -DLOAD_GFXFF + -DUSE_HSPI_PORT + +[env:cyd] +lib_deps = + ${common_cyd.lib_deps} +build_flags = + ${common_cyd.build_flags} + -DTFT_INVERSION_OFF + +[env:cyd2usb] +lib_deps = + ${common_cyd.lib_deps} +build_flags = + ${common_cyd.build_flags} + -DTFT_INVERSION_ON + +[env:trinity] +lib_deps = + ${env.lib_deps} + mrfaptastic/ESP32 HUB75 LED MATRIX PANEL DMA Display@^3.0.9 + adafruit/Adafruit GFX Library@^1.11.9 +build_flags = + -DMATRIX_DISPLAY From f60d888624ad69ba387558ee9de4b2df1b7c6a50 Mon Sep 17 00:00:00 2001 From: witnessmenow Date: Mon, 6 May 2024 16:16:25 +0100 Subject: [PATCH 3/3] Fixing mistake in matrix code --- F1-Notifications/matrixDisplay.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/F1-Notifications/matrixDisplay.h b/F1-Notifications/matrixDisplay.h index e36345d..c2c609f 100644 --- a/F1-Notifications/matrixDisplay.h +++ b/F1-Notifications/matrixDisplay.h @@ -101,7 +101,7 @@ class MatrixDisplay : public F1Display void displayPlaceHolder(const char *raceName, JsonObject races_sessions) { - const char *raceNameChanged = updateRaceName(raceName); + const char *raceNameChanged = convertRaceName(raceName); // Not yet race week dma_display->fillScreen(myBLACK);