├── .DS_Store ├── .github └── workflows │ ├── build-deploy-webflash.yml │ └── test-build.yml ├── .gitignore ├── DisplayConfig └── User_Setup.h ├── F1-Notifications ├── F1-Notifications.ino ├── cheapYellowLCD.h ├── config.h ├── display.h ├── getImage.h ├── githubCert.h ├── matrixDisplay.h ├── raceLogic.h ├── races.h ├── util.h └── wifiManagerHandler.h ├── GitHubPages ├── ESPWebTools │ └── manifest.json └── index.html ├── LICENSE ├── README.md ├── images ├── 320x240 │ ├── 1 - Bahrain.png │ ├── 10 - Austria.png │ ├── 11 - British.png │ ├── 12 - Hungary.png │ ├── 13 - Belgium.png │ ├── 14 - Dutch.png │ ├── 15 - Monza.png │ ├── 16 - Singapore.png │ ├── 17 - Japan.png │ ├── 18 - Qatar.png │ ├── 19 - USA.png │ ├── 2 - SaudiArabia.png │ ├── 20 - Mexico.png │ ├── 21 - Brazil.png │ ├── 22 - Vegas.png │ ├── 23 - AbuDhabi.png │ ├── 3 - Australia.png │ ├── 4 - Azerbaijan.png │ ├── 5 - Miami.png │ ├── 7- Monaco.png │ ├── 8 - Spain.png │ ├── 9 - Canada.png │ ├── Imola.png │ ├── calledOff.png │ └── imageNotFound.png └── Readme.md └── platformio.ini /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/build-deploy-webflash.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Build firmware and deploy GitHub Page 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | paths-ignore: 9 | - "**.md" 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: read 17 | pages: write 18 | id-token: write 19 | 20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 22 | concurrency: 23 | group: "pages" 24 | cancel-in-progress: false 25 | 26 | jobs: 27 | # Build job 28 | build: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/cache@v3 33 | - name: Cache pip 34 | uses: actions/cache@v3 35 | with: 36 | path: ~/.cache/pip 37 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pip- 40 | - name: Cache PlatformIO 41 | uses: actions/cache@v3 42 | with: 43 | path: ~/.platformio 44 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 45 | - name: Set up Python 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: "3.10" 49 | - name: Install PlatformIO 50 | run: | 51 | python -m pip install --upgrade pip 52 | pip install --upgrade platformio 53 | 54 | #Build CYD 55 | - name: Build CYD 56 | run: platformio run -e cyd 57 | - name: Upload artifact 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: CYD Firmware 61 | path: .pio/build/cyd/firmware.bin 62 | if-no-files-found: error 63 | - name: Copy compiled binaries for webflash 64 | run: | 65 | mkdir GitHubPages/ESPWebTools/cyd 66 | 67 | # Copy the manifest file for the CYD 68 | cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/cyd 69 | 70 | cp .pio/build/cyd/bootloader.bin GitHubPages/ESPWebTools/cyd 71 | cp .pio/build/cyd/partitions.bin GitHubPages/ESPWebTools/cyd 72 | cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/cyd 73 | cp .pio/build/cyd/firmware.bin GitHubPages/ESPWebTools/cyd 74 | 75 | #Build CYD2USB 76 | - name: Build CYD2USB 77 | run: platformio run -e cyd2usb 78 | - name: Upload artifact 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: CYD2USB Firmware 82 | path: .pio/build/cyd2usb/firmware.bin 83 | if-no-files-found: error 84 | - name: Copy compiled binaries for webflash 85 | run: | 86 | mkdir GitHubPages/ESPWebTools/cyd2usb 87 | 88 | # Copy the manifest file for the cyd2usb 89 | cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/cyd2usb 90 | 91 | cp .pio/build/cyd2usb/bootloader.bin GitHubPages/ESPWebTools/cyd2usb 92 | cp .pio/build/cyd2usb/partitions.bin GitHubPages/ESPWebTools/cyd2usb 93 | cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/cyd 94 | cp .pio/build/cyd2usb/firmware.bin GitHubPages/ESPWebTools/cyd2usb 95 | 96 | #Build Matrix 97 | - name: Build Matrix 98 | run: platformio run -e trinity 99 | - name: Upload artifact 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: Matrix Firmware 103 | path: .pio/build/trinity/firmware.bin 104 | if-no-files-found: error 105 | - name: Copy compiled binaries for webflash 106 | run: | 107 | mkdir GitHubPages/ESPWebTools/matrix 108 | 109 | # Copy the manifest file for the Matrix 110 | cp GitHubPages/ESPWebTools/manifest.json GitHubPages/ESPWebTools/matrix 111 | 112 | cp .pio/build/trinity/bootloader.bin GitHubPages/ESPWebTools/matrix 113 | cp .pio/build/trinity/partitions.bin GitHubPages/ESPWebTools/matrix 114 | cp /home/runner/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin GitHubPages/ESPWebTools/matrix 115 | cp .pio/build/trinity/firmware.bin GitHubPages/ESPWebTools/matrix 116 | 117 | # Build GitHub Page 118 | - name: Setup Github Page 119 | uses: actions/configure-pages@v4 120 | 121 | - name: Build GitHub Page with Jekyll 122 | uses: actions/jekyll-build-pages@v1 123 | with: 124 | source: ./GitHubPages 125 | destination: ./_site 126 | 127 | - name: Upload GitHub Page Artifact 128 | uses: actions/upload-pages-artifact@v3 129 | 130 | # Deployment job 131 | deploy: 132 | environment: 133 | name: github-pages 134 | url: ${{ steps.deployment.outputs.page_url }} 135 | runs-on: ubuntu-latest 136 | needs: build 137 | steps: 138 | - name: Deploy to GitHub Pages 139 | id: deployment 140 | uses: actions/deploy-pages@v4 141 | -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | name: Test compiling arduino code 2 | 3 | on: 4 | # Runs on all branches but main, when a arduino file is changed 5 | push: 6 | branches-ignore: ["main"] 7 | paths: 8 | - "**.ino" 9 | - "**.h" 10 | - "**.yml" 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | # Build job 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/cache@v3 22 | - name: Cache pip 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.cache/pip 26 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 27 | restore-keys: | 28 | ${{ runner.os }}-pip- 29 | - name: Cache PlatformIO 30 | uses: actions/cache@v3 31 | with: 32 | path: ~/.platformio 33 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 34 | - name: Set up Python 35 | uses: actions/setup-python@v4 36 | with: 37 | python-version: "3.10" 38 | - name: Install PlatformIO 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install --upgrade platformio 42 | 43 | #Build CYD 44 | - name: Build CYD 45 | run: platformio run -e cyd 46 | 47 | #Build CYD2USB 48 | - name: Build CYD2USB 49 | run: platformio run -e cyd2usb 50 | 51 | #Build Matrix 52 | - name: Build Matrix 53 | run: platformio run -e trinity 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/* 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /DisplayConfig/User_Setup.h: -------------------------------------------------------------------------------- 1 | // USER DEFINED SETTINGS 2 | // Set driver type, fonts to be loaded, pins used and SPI control method etc 3 | // 4 | // See the User_Setup_Select.h file if you wish to be able to define multiple 5 | // setups and then easily select which setup file is used by the compiler. 6 | // 7 | // If this file is edited correctly then all the library example sketches should 8 | // run without the need to make any more changes for a particular hardware setup! 9 | // Note that some sketches are designed for a particular TFT pixel width/height 10 | 11 | // User defined information reported by "Read_User_Setup" test & diagnostics example 12 | #define USER_SETUP_INFO "User_Setup" 13 | 14 | // Define to disable all #warnings in library (can be put in User_Setup_Select.h) 15 | //#define DISABLE_ALL_LIBRARY_WARNINGS 16 | 17 | // ################################################################################## 18 | // 19 | // Section 1. Call up the right driver file and any options for it 20 | // 21 | // ################################################################################## 22 | 23 | // Define STM32 to invoke optimised processor support (only for STM32) 24 | //#define STM32 25 | 26 | // Defining the STM32 board allows the library to optimise the performance 27 | // for UNO compatible "MCUfriend" style shields 28 | //#define NUCLEO_64_TFT 29 | //#define NUCLEO_144_TFT 30 | 31 | // STM32 8 bit parallel only: 32 | // If STN32 Port A or B pins 0-7 are used for 8 bit parallel data bus bits 0-7 33 | // then this will improve rendering performance by a factor of ~8x 34 | //#define STM_PORTA_DATA_BUS 35 | //#define STM_PORTB_DATA_BUS 36 | 37 | // Tell the library to use parallel mode (otherwise SPI is assumed) 38 | //#define TFT_PARALLEL_8_BIT 39 | //#defined TFT_PARALLEL_16_BIT // **** 16 bit parallel ONLY for RP2040 processor **** 40 | 41 | // Display type - only define if RPi display 42 | //#define RPI_DISPLAY_TYPE // 20MHz maximum SPI 43 | 44 | // Only define one driver, the other ones must be commented out 45 | //#define ILI9341_DRIVER // Generic driver for common displays 46 | #define ILI9341_2_DRIVER // Alternative ILI9341 driver, see https://github.com/Bodmer/TFT_eSPI/issues/1172 47 | //#define ST7735_DRIVER // Define additional parameters below for this display 48 | //#define ILI9163_DRIVER // Define additional parameters below for this display 49 | //#define S6D02A1_DRIVER 50 | //#define RPI_ILI9486_DRIVER // 20MHz maximum SPI 51 | //#define HX8357D_DRIVER 52 | //#define ILI9481_DRIVER 53 | //#define ILI9486_DRIVER 54 | //#define ILI9488_DRIVER // WARNING: Do not connect ILI9488 display SDO to MISO if other devices share the SPI bus (TFT SDO does NOT tristate when CS is high) 55 | //#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display 56 | //#define ST7789_2_DRIVER // Minimal configuration option, define additional parameters below for this display 57 | //#define R61581_DRIVER 58 | //#define RM68140_DRIVER 59 | //#define ST7796_DRIVER 60 | //#define SSD1351_DRIVER 61 | //#define SSD1963_480_DRIVER 62 | //#define SSD1963_800_DRIVER 63 | //#define SSD1963_800ALT_DRIVER 64 | //#define ILI9225_DRIVER 65 | //#define GC9A01_DRIVER 66 | 67 | // Some displays support SPI reads via the MISO pin, other displays have a single 68 | // bi-directional SDA pin and the library will try to read this via the MOSI line. 69 | // To use the SDA line for reading data from the TFT uncomment the following line: 70 | 71 | // #define TFT_SDA_READ // This option is for ESP32 ONLY, tested with ST7789 and GC9A01 display only 72 | 73 | // For ST7735, ST7789 and ILI9341 ONLY, define the colour order IF the blue and red are swapped on your display 74 | // Try ONE option at a time to find the correct colour order for your display 75 | 76 | // #define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue 77 | // #define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red 78 | 79 | // For M5Stack ESP32 module with integrated ILI9341 display ONLY, remove // in line below 80 | 81 | // #define M5STACK 82 | 83 | // For ST7789, ST7735, ILI9163 and GC9A01 ONLY, define the pixel width and height in portrait orientation 84 | // #define TFT_WIDTH 80 85 | // #define TFT_WIDTH 128 86 | // #define TFT_WIDTH 172 // ST7789 172 x 320 87 | #define TFT_WIDTH 240 // ST7789 240 x 240 and 240 x 320 88 | // #define TFT_HEIGHT 160 89 | // #define TFT_HEIGHT 128 90 | // #define TFT_HEIGHT 240 // ST7789 240 x 240 91 | #define TFT_HEIGHT 320 // ST7789 240 x 320 92 | // #define TFT_HEIGHT 240 // GC9A01 240 x 240 93 | 94 | // For ST7735 ONLY, define the type of display, originally this was based on the 95 | // colour of the tab on the screen protector film but this is not always true, so try 96 | // out the different options below if the screen does not display graphics correctly, 97 | // e.g. colours wrong, mirror images, or stray pixels at the edges. 98 | // Comment out ALL BUT ONE of these options for a ST7735 display driver, save this 99 | // this User_Setup file, then rebuild and upload the sketch to the board again: 100 | 101 | // #define ST7735_INITB 102 | // #define ST7735_GREENTAB 103 | // #define ST7735_GREENTAB2 104 | // #define ST7735_GREENTAB3 105 | // #define ST7735_GREENTAB128 // For 128 x 128 display 106 | // #define ST7735_GREENTAB160x80 // For 160 x 80 display (BGR, inverted, 26 offset) 107 | // #define ST7735_ROBOTLCD // For some RobotLCD arduino shields (128x160, BGR, https://docs.arduino.cc/retired/getting-started-guides/TFT) 108 | // #define ST7735_REDTAB 109 | // #define ST7735_BLACKTAB 110 | // #define ST7735_REDTAB160x80 // For 160 x 80 display with 24 pixel offset 111 | 112 | // If colours are inverted (white shows as black) then uncomment one of the next 113 | // 2 lines try both options, one of the options should correct the inversion. 114 | 115 | // #define TFT_INVERSION_ON 116 | // #define TFT_INVERSION_OFF 117 | 118 | 119 | // ################################################################################## 120 | // 121 | // Section 2. Define the pins that are used to interface with the display here 122 | // 123 | // ################################################################################## 124 | 125 | // If a backlight control signal is available then define the TFT_BL pin in Section 2 126 | // below. The backlight will be turned ON when tft.begin() is called, but the library 127 | // needs to know if the LEDs are ON with the pin HIGH or LOW. If the LEDs are to be 128 | // driven with a PWM signal or turned OFF/ON then this must be handled by the user 129 | // sketch. e.g. with digitalWrite(TFT_BL, LOW); 130 | 131 | #define TFT_BL 21 // LED back-light control pin 132 | #define TFT_BACKLIGHT_ON HIGH // Level to turn ON back-light (HIGH or LOW) 133 | 134 | 135 | 136 | // We must use hardware SPI, a minimum of 3 GPIO pins is needed. 137 | // Typical setup for ESP8266 NodeMCU ESP-12 is : 138 | // 139 | // Display SDO/MISO to NodeMCU pin D6 (or leave disconnected if not reading TFT) 140 | // Display LED to NodeMCU pin VIN (or 5V, see below) 141 | // Display SCK to NodeMCU pin D5 142 | // Display SDI/MOSI to NodeMCU pin D7 143 | // Display DC (RS/AO)to NodeMCU pin D3 144 | // Display RESET to NodeMCU pin D4 (or RST, see below) 145 | // Display CS to NodeMCU pin D8 (or GND, see below) 146 | // Display GND to NodeMCU pin GND (0V) 147 | // Display VCC to NodeMCU 5V or 3.3V 148 | // 149 | // The TFT RESET pin can be connected to the NodeMCU RST pin or 3.3V to free up a control pin 150 | // 151 | // The DC (Data Command) pin may be labelled AO or RS (Register Select) 152 | // 153 | // With some displays such as the ILI9341 the TFT CS pin can be connected to GND if no more 154 | // SPI devices (e.g. an SD Card) are connected, in this case comment out the #define TFT_CS 155 | // line below so it is NOT defined. Other displays such at the ST7735 require the TFT CS pin 156 | // to be toggled during setup, so in these cases the TFT_CS line must be defined and connected. 157 | // 158 | // The NodeMCU D0 pin can be used for RST 159 | // 160 | // 161 | // Note: only some versions of the NodeMCU provide the USB 5V on the VIN pin 162 | // If 5V is not available at a pin you can use 3.3V but backlight brightness 163 | // will be lower. 164 | 165 | 166 | // ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP8266 SETUP ###### 167 | 168 | // For NodeMCU - use pin numbers in the form PIN_Dx where Dx is the NodeMCU pin designation 169 | //#define TFT_CS PIN_D8 // Chip select control pin D8 170 | //#define TFT_DC PIN_D3 // Data Command control pin 171 | //#define TFT_RST PIN_D4 // Reset pin (could connect to NodeMCU RST, see next line) 172 | //#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V 173 | 174 | //#define TFT_BL PIN_D1 // LED back-light (only for ST7789 with backlight control pin) 175 | 176 | //#define TOUCH_CS PIN_D2 // Chip select pin (T_CS) of touch screen 177 | 178 | //#define TFT_WR PIN_D2 // Write strobe for modified Raspberry Pi TFT only 179 | 180 | 181 | // ###### FOR ESP8266 OVERLAP MODE EDIT THE PIN NUMBERS IN THE FOLLOWING LINES ###### 182 | 183 | // Overlap mode shares the ESP8266 FLASH SPI bus with the TFT so has a performance impact 184 | // but saves pins for other functions. It is best not to connect MISO as some displays 185 | // do not tristate that line when chip select is high! 186 | // Note: Only one SPI device can share the FLASH SPI lines, so a SPI touch controller 187 | // cannot be connected as well to the same SPI signals. 188 | // On NodeMCU 1.0 SD0=MISO, SD1=MOSI, CLK=SCLK to connect to TFT in overlap mode 189 | // On NodeMCU V3 S0 =MISO, S1 =MOSI, S2 =SCLK 190 | // In ESP8266 overlap mode the following must be defined 191 | 192 | //#define TFT_SPI_OVERLAP 193 | 194 | // In ESP8266 overlap mode the TFT chip select MUST connect to pin D3 195 | //#define TFT_CS PIN_D3 196 | //#define TFT_DC PIN_D5 // Data Command control pin 197 | //#define TFT_RST PIN_D4 // Reset pin (could connect to NodeMCU RST, see next line) 198 | //#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V 199 | 200 | 201 | // ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP32 SETUP ###### 202 | 203 | // For ESP32 Dev board (only tested with ILI9341 display) 204 | // The hardware SPI can be mapped to any pins 205 | 206 | #define TFT_MISO 12 207 | #define TFT_MOSI 13 208 | #define TFT_SCLK 14 209 | #define TFT_CS 15 // Chip select control pin 210 | #define TFT_DC 2 // Data Command control pin 211 | //#define TFT_RST 4 // Reset pin (could connect to RST pin) 212 | #define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST 213 | 214 | // For ESP32 Dev board (only tested with GC9A01 display) 215 | // The hardware SPI can be mapped to any pins 216 | 217 | //#define TFT_MOSI 15 // In some display driver board, it might be written as "SDA" and so on. 218 | //#define TFT_SCLK 14 219 | //#define TFT_CS 5 // Chip select control pin 220 | //#define TFT_DC 27 // Data Command control pin 221 | //#define TFT_RST 33 // Reset pin (could connect to Arduino RESET pin) 222 | //#define TFT_BL 22 // LED back-light 223 | 224 | //#define TOUCH_CS 21 // Chip select pin (T_CS) of touch screen 225 | 226 | //#define TFT_WR 22 // Write strobe for modified Raspberry Pi TFT only 227 | 228 | // For the M5Stack module use these #define lines 229 | //#define TFT_MISO 19 230 | //#define TFT_MOSI 23 231 | //#define TFT_SCLK 18 232 | //#define TFT_CS 14 // Chip select control pin 233 | //#define TFT_DC 27 // Data Command control pin 234 | //#define TFT_RST 33 // Reset pin (could connect to Arduino RESET pin) 235 | //#define TFT_BL 32 // LED back-light (required for M5Stack) 236 | 237 | // ###### EDIT THE PINs BELOW TO SUIT YOUR ESP32 PARALLEL TFT SETUP ###### 238 | 239 | // The library supports 8 bit parallel TFTs with the ESP32, the pin 240 | // selection below is compatible with ESP32 boards in UNO format. 241 | // Wemos D32 boards need to be modified, see diagram in Tools folder. 242 | // Only ILI9481 and ILI9341 based displays have been tested! 243 | 244 | // Parallel bus is only supported for the STM32 and ESP32 245 | // Example below is for ESP32 Parallel interface with UNO displays 246 | 247 | // Tell the library to use 8 bit parallel mode (otherwise SPI is assumed) 248 | //#define TFT_PARALLEL_8_BIT 249 | 250 | // The ESP32 and TFT the pins used for testing are: 251 | //#define TFT_CS 33 // Chip select control pin (library pulls permanently low 252 | //#define TFT_DC 15 // Data Command control pin - must use a pin in the range 0-31 253 | //#define TFT_RST 32 // Reset pin, toggles on startup 254 | 255 | //#define TFT_WR 4 // Write strobe control pin - must use a pin in the range 0-31 256 | //#define TFT_RD 2 // Read strobe control pin 257 | 258 | //#define TFT_D0 12 // Must use pins in the range 0-31 for the data bus 259 | //#define TFT_D1 13 // so a single register write sets/clears all bits. 260 | //#define TFT_D2 26 // Pins can be randomly assigned, this does not affect 261 | //#define TFT_D3 25 // TFT screen update performance. 262 | //#define TFT_D4 17 263 | //#define TFT_D5 16 264 | //#define TFT_D6 27 265 | //#define TFT_D7 14 266 | 267 | // ###### EDIT THE PINs BELOW TO SUIT YOUR STM32 SPI TFT SETUP ###### 268 | 269 | // The TFT can be connected to SPI port 1 or 2 270 | //#define TFT_SPI_PORT 1 // SPI port 1 maximum clock rate is 55MHz 271 | //#define TFT_MOSI PA7 272 | //#define TFT_MISO PA6 273 | //#define TFT_SCLK PA5 274 | 275 | //#define TFT_SPI_PORT 2 // SPI port 2 maximum clock rate is 27MHz 276 | //#define TFT_MOSI PB15 277 | //#define TFT_MISO PB14 278 | //#define TFT_SCLK PB13 279 | 280 | // Can use Ardiuno pin references, arbitrary allocation, TFT_eSPI controls chip select 281 | //#define TFT_CS D5 // Chip select control pin to TFT CS 282 | //#define TFT_DC D6 // Data Command control pin to TFT DC (may be labelled RS = Register Select) 283 | //#define TFT_RST D7 // Reset pin to TFT RST (or RESET) 284 | // OR alternatively, we can use STM32 port reference names PXnn 285 | //#define TFT_CS PE11 // Nucleo-F767ZI equivalent of D5 286 | //#define TFT_DC PE9 // Nucleo-F767ZI equivalent of D6 287 | //#define TFT_RST PF13 // Nucleo-F767ZI equivalent of D7 288 | 289 | //#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to processor reset 290 | // Use an Arduino pin for initial testing as connecting to processor reset 291 | // may not work (pulse too short at power up?) 292 | 293 | // ################################################################################## 294 | // 295 | // Section 3. Define the fonts that are to be used here 296 | // 297 | // ################################################################################## 298 | 299 | // Comment out the #defines below with // to stop that font being loaded 300 | // The ESP8366 and ESP32 have plenty of memory so commenting out fonts is not 301 | // normally necessary. If all fonts are loaded the extra FLASH space required is 302 | // about 17Kbytes. To save FLASH space only enable the fonts you need! 303 | 304 | #define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH 305 | #define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters 306 | #define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters 307 | #define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm 308 | #define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-. 309 | #define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. 310 | //#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT 311 | #define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts 312 | 313 | // Comment out the #define below to stop the SPIFFS filing system and smooth font code being loaded 314 | // this will save ~20kbytes of FLASH 315 | #define SMOOTH_FONT 316 | 317 | 318 | // ################################################################################## 319 | // 320 | // Section 4. Other options 321 | // 322 | // ################################################################################## 323 | 324 | // For RP2040 processor and SPI displays, uncomment the following line to use the PIO interface. 325 | //#define RP2040_PIO_SPI // Leave commented out to use standard RP2040 SPI port interface 326 | 327 | // For RP2040 processor and 8 or 16 bit parallel displays: 328 | // The parallel interface write cycle period is derived from a division of the CPU clock 329 | // speed so scales with the processor clock. This means that the divider ratio may need 330 | // to be increased when overclocking. I may also need to be adjusted dependant on the 331 | // display controller type (ILI94341, HX8357C etc). If RP2040_PIO_CLK_DIV is not defined 332 | // the library will set default values which may not suit your display. 333 | // The display controller data sheet will specify the minimum write cycle period. The 334 | // controllers often work reliably for shorter periods, however if the period is too short 335 | // the display may not initialise or graphics will become corrupted. 336 | // PIO write cycle frequency = (CPU clock/(4 * RP2040_PIO_CLK_DIV)) 337 | //#define RP2040_PIO_CLK_DIV 1 // 32ns write cycle at 125MHz CPU clock 338 | //#define RP2040_PIO_CLK_DIV 2 // 64ns write cycle at 125MHz CPU clock 339 | //#define RP2040_PIO_CLK_DIV 3 // 96ns write cycle at 125MHz CPU clock 340 | 341 | // For the RP2040 processor define the SPI port channel used (default 0 if undefined) 342 | //#define TFT_SPI_PORT 1 // Set to 0 if SPI0 pins are used, or 1 if spi1 pins used 343 | 344 | // For the STM32 processor define the SPI port channel used (default 1 if undefined) 345 | //#define TFT_SPI_PORT 2 // Set to 1 for SPI port 1, or 2 for SPI port 2 346 | 347 | // Define the SPI clock frequency, this affects the graphics rendering speed. Too 348 | // fast and the TFT driver will not keep up and display corruption appears. 349 | // With an ILI9341 display 40MHz works OK, 80MHz sometimes fails 350 | // With a ST7735 display more than 27MHz may not work (spurious pixels and lines) 351 | // With an ILI9163 display 27 MHz works OK. 352 | 353 | // #define SPI_FREQUENCY 1000000 354 | // #define SPI_FREQUENCY 5000000 355 | // #define SPI_FREQUENCY 10000000 356 | // #define SPI_FREQUENCY 20000000 357 | //#define SPI_FREQUENCY 27000000 358 | // #define SPI_FREQUENCY 40000000 359 | #define SPI_FREQUENCY 55000000 // STM32 SPI1 only (SPI2 maximum is 27MHz) 360 | // #define SPI_FREQUENCY 80000000 361 | 362 | // Optional reduced SPI frequency for reading TFT 363 | #define SPI_READ_FREQUENCY 20000000 364 | 365 | // The XPT2046 requires a lower SPI clock rate of 2.5MHz so we define that here: 366 | #define SPI_TOUCH_FREQUENCY 2500000 367 | 368 | // The ESP32 has 2 free SPI ports i.e. VSPI and HSPI, the VSPI is the default. 369 | // If the VSPI port is in use and pins are not accessible (e.g. TTGO T-Beam) 370 | // then uncomment the following line: 371 | //#define USE_HSPI_PORT 372 | 373 | // Comment out the following #define if "SPI Transactions" do not need to be 374 | // supported. When commented out the code size will be smaller and sketches will 375 | // run slightly faster, so leave it commented out unless you need it! 376 | 377 | // Transaction support is needed to work with SD library but not needed with TFT_SdFat 378 | // Transaction support is required if other SPI devices are connected. 379 | 380 | // Transactions are automatically enabled by the library for an ESP32 (to use HAL mutex) 381 | // so changing it here has no effect 382 | 383 | // #define SUPPORT_TRANSACTIONS 384 | -------------------------------------------------------------------------------- /F1-Notifications/F1-Notifications.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | An Arduino Project for notifiying you of the start time of 3 | upcoming F1 sessions in your local timezone. 4 | 5 | Tested on an ESP32. 6 | 7 | If you find what I do useful and would like to support me, 8 | please consider becoming a sponsor on Github 9 | https://github.com/sponsors/witnessmenow/ 10 | 11 | Written by Brian Lough 12 | YouTube: https://www.youtube.com/brianlough 13 | Twitter: https://twitter.com/witnessmenow 14 | *******************************************************************/ 15 | // ---------------------------- 16 | // Display type 17 | // --------------------------- 18 | 19 | // This project currently supports the following displays 20 | // (Uncomment the required #define) 21 | 22 | // 1. Cheap yellow display (Using TFT-eSPI library) 23 | // #define YELLOW_DISPLAY 24 | 25 | // 2. Matrix Displays (Like the ESP32 Trinity) 26 | // #define MATRIX_DISPLAY 27 | 28 | // If no defines are set, it will default to CYD 29 | #if !defined(YELLOW_DISPLAY) && !defined(MATRIX_DISPLAY) 30 | #define YELLOW_DISPLAY // Default to Yellow Display for display type 31 | #endif 32 | 33 | // ---------------------------- 34 | // Library Defines - Need to be defined before library import 35 | // ---------------------------- 36 | 37 | #define ESP_DRD_USE_SPIFFS true 38 | 39 | // ---------------------------- 40 | // Standard Libraries 41 | // ---------------------------- 42 | 43 | #include 44 | 45 | #include 46 | 47 | #include 48 | #include "SPIFFS.h" 49 | 50 | // ---------------------------- 51 | // Additional Libraries - each one of these will need to be installed. 52 | // ---------------------------- 53 | 54 | #include 55 | // Captive portal for configuring the WiFi 56 | 57 | // If installing from the library manager (Search for "WifiManager") 58 | // https://github.com/tzapu/WiFiManager 59 | 60 | #include 61 | // A library for checking if the reset button has been pressed twice 62 | // Can be used to enable config mode 63 | // Can be installed from the library manager (Search for "ESP_DoubleResetDetector") 64 | // https://github.com/khoih-prog/ESP_DoubleResetDetector 65 | 66 | #include 67 | // Library used for parsing Json from the API responses 68 | 69 | // Search for "Arduino Json" in the Arduino Library manager 70 | // https://github.com/bblanchon/ArduinoJson 71 | 72 | #include 73 | // Library used for getting the time and converting session time 74 | // to users timezone 75 | 76 | // Search for "ezTime" in the Arduino Library manager 77 | // https://github.com/ropg/ezTime 78 | 79 | #include 80 | // Library used to send Telegram Message 81 | 82 | // Search for "Universal Telegram" in the Arduino Library manager 83 | // https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot 84 | 85 | #include 86 | // Library used to get files or images 87 | 88 | // Not on library manager yet 89 | // https://github.com/witnessmenow/file-fetcher-arduino 90 | 91 | // ---------------------------- 92 | // Internal includes 93 | // ---------------------------- 94 | 95 | #include "githubCert.h" 96 | 97 | #include "display.h" 98 | 99 | #include "config.h" 100 | 101 | #include "raceLogic.h" 102 | 103 | #include "wifiManagerHandler.h" 104 | 105 | WiFiClientSecure secured_client; 106 | 107 | FileFetcher fileFetcher(secured_client); 108 | 109 | // ---------------------------- 110 | // Display Handling Code 111 | // ---------------------------- 112 | 113 | #if defined YELLOW_DISPLAY 114 | 115 | #include "cheapYellowLCD.h" 116 | CheapYellowDisplay cyd; 117 | F1Display *f1Display = &cyd; 118 | 119 | #elif defined MATRIX_DISPLAY 120 | 121 | #include "matrixDisplay.h" 122 | MatrixDisplay matrixDisplay; 123 | F1Display *f1Display = &matrixDisplay; 124 | 125 | #endif 126 | // ---------------------------- 127 | 128 | UniversalTelegramBot bot("", secured_client); 129 | 130 | F1Config f1Config; 131 | 132 | void setup() 133 | { 134 | // put your setup code here, to run once: 135 | 136 | Serial.begin(115200); 137 | 138 | f1Display->displaySetup(); 139 | 140 | bool forceConfig = false; 141 | 142 | drd = new DoubleResetDetector(DRD_TIMEOUT, DRD_ADDRESS); 143 | if (drd->detectDoubleReset()) 144 | { 145 | Serial.println(F("Forcing config mode as there was a Double reset detected")); 146 | forceConfig = true; 147 | } 148 | 149 | // Initialise SPIFFS, if this fails try .begin(true) 150 | // NOTE: I believe this formats it though it will erase everything on 151 | // spiffs already! In this example that is not a problem. 152 | // I have found once I used the true flag once, I could use it 153 | // without the true flag after that. 154 | bool spiffsInitSuccess = SPIFFS.begin(false) || SPIFFS.begin(true); 155 | if (!spiffsInitSuccess) 156 | { 157 | Serial.println("SPIFFS initialisation failed!"); 158 | while (1) 159 | yield(); // Stay here twiddling thumbs waiting 160 | } 161 | Serial.println("\r\nInitialisation done."); 162 | 163 | if (!f1Config.fetchConfigFile()) 164 | { 165 | // Failed to fetch config file, need to launch Wifi Manager 166 | forceConfig = true; 167 | } 168 | 169 | setupWiFiManager(forceConfig, f1Config, f1Display); 170 | raceLogicSetup(f1Config); 171 | bot.updateToken(f1Config.botToken); 172 | 173 | // Set WiFi to station mode and disconnect from an AP if it was Previously 174 | // connected 175 | // WiFi.mode(WIFI_STA); 176 | // WiFi.begin(ssid, password); 177 | 178 | while (WiFi.status() != WL_CONNECTED) 179 | { 180 | Serial.print("."); 181 | delay(500); 182 | } 183 | 184 | Serial.println(""); 185 | Serial.println("WiFi connected"); 186 | Serial.print("IP address: "); 187 | Serial.println(WiFi.localIP()); 188 | 189 | secured_client.setCACert(github_server_cert); 190 | while (fetchRaceJson(fileFetcher) != 1) 191 | { 192 | Serial.println("failed to get Race Json"); 193 | Serial.println("will try again in 10 seconds"); 194 | delay(1000 * 10); 195 | } 196 | 197 | Serial.println("Fetched races.json File"); 198 | 199 | Serial.println("Waiting for time sync"); 200 | 201 | waitForSync(); 202 | 203 | Serial.println(); 204 | Serial.println("UTC: " + UTC.dateTime()); 205 | 206 | myTZ.setLocation(f1Config.timeZone); 207 | Serial.print(f1Config.timeZone); 208 | Serial.print(F(": ")); 209 | Serial.println(myTZ.dateTime()); 210 | Serial.println("-------------------------"); 211 | 212 | // sendNotificationOfNextRace(&bot, f1Config.roundOffset); 213 | } 214 | 215 | bool notificaitonEventRaised = false; 216 | 217 | void sendNotification() 218 | { 219 | // Cause it could be set to the image one 220 | if (f1Config.isTelegramConfigured()) 221 | { 222 | secured_client.setCACert(TELEGRAM_CERTIFICATE_ROOT); 223 | Serial.println("Sending notifcation"); 224 | f1Config.currentRaceNotification = sendNotificationOfNextRace(&bot); 225 | if (!f1Config.currentRaceNotification) 226 | { 227 | // Notificaiton failed, raise event again 228 | Serial.println("Notfication failed"); 229 | setEvent(sendNotification, getNotifyTime()); 230 | } 231 | else 232 | { 233 | notificaitonEventRaised = false; 234 | f1Config.saveConfigFile(); 235 | } 236 | } 237 | else 238 | { 239 | 240 | Serial.println("Would have sent Notification now, but telegram is not configured"); 241 | 242 | notificaitonEventRaised = false; 243 | f1Config.currentRaceNotification = true; 244 | f1Config.saveConfigFile(); 245 | } 246 | } 247 | 248 | bool first = true; 249 | 250 | int minuteCounter = 60; // kick off fetch first time 251 | 252 | void loop() 253 | { 254 | drd->loop(); 255 | 256 | // Every hour we will refresh the Race JSON from Github 257 | if (minuteCounter >= 60) 258 | { 259 | secured_client.setCACert(github_server_cert); 260 | while (fetchRaceJson(fileFetcher) != 1) 261 | { 262 | Serial.println("failed to get Race Json"); 263 | Serial.println("will try again in 10 seconds"); 264 | delay(1000 * 10); 265 | } 266 | minuteCounter = 0; 267 | } 268 | 269 | if (first || minuteChanged()) 270 | { 271 | minuteCounter++; 272 | bool newRace = getNextRace(f1Config.roundOffset, f1Config.currentRaceNotification, f1Display, first); 273 | if (newRace) 274 | { 275 | f1Config.saveConfigFile(); 276 | } 277 | if (!f1Config.currentRaceNotification && !notificaitonEventRaised) 278 | { 279 | // we have never notified about this race yet, so we'll raise an event 280 | setEvent(sendNotification, getNotifyTime()); 281 | notificaitonEventRaised = true; 282 | Serial.print("Raised event for: "); 283 | Serial.println(myTZ.dateTime(getNotifyTime(), UTC_TIME, f1Config.timeFormat)); 284 | } 285 | first = false; 286 | } 287 | 288 | events(); 289 | } 290 | -------------------------------------------------------------------------------- /F1-Notifications/cheapYellowLCD.h: -------------------------------------------------------------------------------- 1 | #include "display.h" 2 | #include "getImage.h" 3 | #include "util.h" 4 | 5 | #include 6 | // A library for interfacing with LCD displays 7 | // 8 | // Can be installed from the library manager (Search for "TFT_eSPI") 9 | // https://github.com/Bodmer/TFT_eSPI 10 | 11 | #include 12 | // For decoding png files 13 | // 14 | // Can be installed from the library manager (Search for "PNGdec") 15 | // https://github.com/bitbank2/PNGdec 16 | 17 | // ------------------------------- 18 | // Putting this stuff outside the class because 19 | // I can't easily pass member functions in as callbacks for pngdec 20 | 21 | // ------------------------------- 22 | 23 | #define SESSION_TEXT_SIZE 4 24 | 25 | TFT_eSPI tft = TFT_eSPI(); 26 | PNG png; 27 | 28 | fs::File myfile; 29 | 30 | void *myOpen(const char *filename, int32_t *size) 31 | { 32 | myfile = SPIFFS.open(filename); 33 | *size = myfile.size(); 34 | return &myfile; 35 | } 36 | void myClose(void *handle) 37 | { 38 | if (myfile) 39 | myfile.close(); 40 | } 41 | int32_t myRead(PNGFILE *handle, uint8_t *buffer, int32_t length) 42 | { 43 | if (!myfile) 44 | return 0; 45 | return myfile.read(buffer, length); 46 | } 47 | int32_t mySeek(PNGFILE *handle, int32_t position) 48 | { 49 | if (!myfile) 50 | return 0; 51 | return myfile.seek(position); 52 | } 53 | 54 | void PNGDraw(PNGDRAW *pDraw) 55 | { 56 | uint16_t usPixels[320]; 57 | 58 | png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_BIG_ENDIAN, 0xffffffff); 59 | tft.pushImage(0, pDraw->y, pDraw->iWidth, 1, usPixels); 60 | } 61 | 62 | class CheapYellowDisplay : public F1Display 63 | { 64 | public: 65 | void displaySetup() 66 | { 67 | 68 | Serial.println("cyd display setup"); 69 | setWidth(320); 70 | setHeight(240); 71 | 72 | // Start the tft display and set it to black 73 | tft.init(); 74 | tft.setRotation(1); 75 | tft.fillScreen(TFT_BLACK); 76 | 77 | state = unset; 78 | } 79 | 80 | void displayPlaceHolder(const char *raceName, JsonObject races_sessions) 81 | { 82 | 83 | if (!isSameRace(raceName) || state != placeholder) 84 | { 85 | setRaceName(raceName); 86 | int imageFileStatus = getImage(raceName); 87 | if (imageFileStatus) 88 | { 89 | int imageDisplayStatus = displayImage(TRACK_IMAGE); 90 | if (imageDisplayStatus == PNG_SUCCESS) 91 | { 92 | // Image is displayed 93 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 94 | int yPos = 215; 95 | String gpStartDateStr = String(getConvertedTime(races_sessions["gp"], "M d")); 96 | String displayMessage = String(convertRaceName(raceName)) + " | " + gpStartDateStr; 97 | tft.drawCentreString(displayMessage, screenCenterX, yPos, 4); 98 | state = placeholder; 99 | return; 100 | } 101 | } 102 | // Failed to display the image 103 | displayRaceWeek(raceName, races_sessions); // For now 104 | } 105 | 106 | // if we reach here, the screen doesn't need to be updated 107 | Serial.println("No need to update display"); 108 | } 109 | 110 | void displayRaceWeek(const char *raceName, JsonObject races_sessions) 111 | { 112 | Serial.println("prts"); 113 | tft.fillRect(0, 0, screenWidth, screenHeight, TFT_BLACK); 114 | 115 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 116 | int yPos = 5; 117 | String gpStartDateStr = String(getConvertedTime(races_sessions["gp"], "M d")); 118 | String displayMessage = String(convertRaceName(raceName)) + " | " + gpStartDateStr; 119 | tft.drawCentreString(displayMessage, screenCenterX, yPos, 4); 120 | 121 | int yValue = 46; 122 | for (JsonPair kv : races_sessions) 123 | { 124 | printSession(4, 125 | yValue, 126 | sessionCodeToString(kv.key().c_str()), 127 | getConvertedTime(kv.value().as())); 128 | yValue += (SESSION_TEXT_SIZE) * 8; 129 | } 130 | 131 | state = raceweek; 132 | } 133 | 134 | int displayImage(char *imageFileUri) 135 | { 136 | tft.fillScreen(TFT_BLACK); 137 | unsigned long lTime = millis(); 138 | lTime = millis(); 139 | Serial.println(imageFileUri); 140 | 141 | int rc = png.open((const char *)imageFileUri, myOpen, myClose, myRead, mySeek, PNGDraw); 142 | if (rc == PNG_SUCCESS) 143 | { 144 | Serial.printf("image specs: (%d x %d), %d bpp, pixel type: %d\n", png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType()); 145 | rc = png.decode(NULL, 0); 146 | png.close(); 147 | } 148 | else 149 | { 150 | Serial.print("error code: "); 151 | Serial.println(rc); 152 | } 153 | 154 | Serial.print("Time taken to decode and display Image (ms): "); 155 | Serial.println(millis() - lTime); 156 | 157 | return rc; 158 | } 159 | 160 | void drawWifiManagerMessage(WiFiManager *myWiFiManager) 161 | { 162 | Serial.println("Entered Conf Mode"); 163 | tft.fillScreen(TFT_BLACK); 164 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 165 | tft.drawCentreString("Entered Conf Mode:", screenCenterX, 5, 2); 166 | tft.drawString("Connect to the following WIFI AP:", 5, 28, 2); 167 | tft.setTextColor(TFT_BLUE, TFT_BLACK); 168 | tft.drawString(myWiFiManager->getConfigPortalSSID(), 20, 48, 2); 169 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 170 | tft.drawString("Password:", 5, 64, 2); 171 | tft.setTextColor(TFT_BLUE, TFT_BLACK); 172 | tft.drawString("nomikey1", 20, 82, 2); 173 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 174 | 175 | tft.drawString("If it doesn't AutoConnect, use this IP:", 5, 110, 2); 176 | tft.setTextColor(TFT_BLUE, TFT_BLACK); 177 | tft.drawString(WiFi.softAPIP().toString(), 20, 128, 2); 178 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 179 | } 180 | 181 | private: 182 | void printSession(int x, int y, const char *sessionName, String sessionStartTime) 183 | { 184 | String tempStr = String(sessionName); 185 | tempStr += " "; 186 | tempStr += sessionStartTime; 187 | tft.drawString(tempStr, x, y, SESSION_TEXT_SIZE); 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /F1-Notifications/config.h: -------------------------------------------------------------------------------- 1 | #define F1_CONFIG_JSON "/f1_notification_config.json" 2 | 3 | #define F1_TIME_ZONE_LABEL "timeZone" 4 | #define F1_TIME_FORMAT_LABEL "timeFormat" 5 | #define F1_BOT_TOKEN_LABEL "botToken" 6 | #define F1_CHAT_ID_LABEL "chatId" 7 | #define F1_ROUND_OFFSET_LABEL "roundOffset" 8 | #define F1_CURRENT_RACE_NOTIFICATION_LABEL "currentRaceNotification" 9 | 10 | class F1Config 11 | { 12 | public: 13 | // How the time will be displayed, see here for more info: https://github.com/ropg/ezTime#datetime 14 | String timeFormat = "D, H:i"; // Fri, 00:30 15 | String timeZone = "Europe/London"; // seems to be something wrong with Europe/Dublin 16 | 17 | // Telegram BOT Token (Get from Botfather) 18 | String botToken = ""; 19 | 20 | // Use @myidbot (IDBot) to find out the chat ID of an individual or a group 21 | // Also note that you need to click "start" on a bot before it can 22 | // message you 23 | String chatId = ""; 24 | 25 | int roundOffset = 0; 26 | 27 | bool currentRaceNotification = false; 28 | 29 | bool isTelegramConfigured() 30 | { 31 | return (botToken != "") && (chatId != ""); 32 | } 33 | 34 | bool fetchConfigFile() 35 | { 36 | if (SPIFFS.exists(F1_CONFIG_JSON)) 37 | { 38 | // file exists, reading and loading 39 | Serial.println("reading config file"); 40 | File configFile = SPIFFS.open(F1_CONFIG_JSON, "r"); 41 | if (configFile) 42 | { 43 | Serial.println("opened config file"); 44 | StaticJsonDocument<1024> json; 45 | DeserializationError error = deserializeJson(json, configFile); 46 | serializeJsonPretty(json, Serial); 47 | if (!error) 48 | { 49 | Serial.println("\nparsed json"); 50 | 51 | if (json.containsKey(F1_TIME_ZONE_LABEL)) 52 | { 53 | timeZone = String(json[F1_TIME_ZONE_LABEL].as()); 54 | } 55 | 56 | if (json.containsKey(F1_TIME_FORMAT_LABEL)) 57 | { 58 | timeFormat = String(json[F1_TIME_FORMAT_LABEL].as()); 59 | } 60 | 61 | if (json.containsKey(F1_BOT_TOKEN_LABEL)) 62 | { 63 | botToken = String(json[F1_BOT_TOKEN_LABEL].as()); 64 | } 65 | 66 | if (json.containsKey(F1_CHAT_ID_LABEL)) 67 | { 68 | chatId = String(json[F1_CHAT_ID_LABEL].as()); 69 | } 70 | 71 | if (json.containsKey(F1_ROUND_OFFSET_LABEL)) 72 | { 73 | roundOffset = json[F1_ROUND_OFFSET_LABEL].as(); 74 | } 75 | 76 | if (json.containsKey(F1_CURRENT_RACE_NOTIFICATION_LABEL)) 77 | { 78 | currentRaceNotification = json[F1_CURRENT_RACE_NOTIFICATION_LABEL].as(); 79 | } 80 | 81 | return true; 82 | } 83 | else 84 | { 85 | Serial.println("failed to load json config"); 86 | return false; 87 | } 88 | } 89 | } 90 | 91 | Serial.println("Config file does not exist"); 92 | return false; 93 | } 94 | 95 | bool saveConfigFile() 96 | { 97 | Serial.println(F("Saving config")); 98 | StaticJsonDocument<1024> json; 99 | json[F1_TIME_ZONE_LABEL] = timeZone; 100 | json[F1_TIME_FORMAT_LABEL] = timeFormat; 101 | json[F1_BOT_TOKEN_LABEL] = botToken; 102 | json[F1_CHAT_ID_LABEL] = chatId; 103 | json[F1_ROUND_OFFSET_LABEL] = roundOffset; 104 | json[F1_CURRENT_RACE_NOTIFICATION_LABEL] = currentRaceNotification; 105 | 106 | File configFile = SPIFFS.open(F1_CONFIG_JSON, "w"); 107 | if (!configFile) 108 | { 109 | Serial.println("failed to open config file for writing"); 110 | return false; 111 | } 112 | 113 | serializeJsonPretty(json, Serial); 114 | if (serializeJson(json, configFile) == 0) 115 | { 116 | Serial.println(F("Failed to write to file")); 117 | return false; 118 | } 119 | configFile.close(); 120 | return true; 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /F1-Notifications/display.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef F1DISPLAY_H 3 | #define F1DISPLAY_H 4 | 5 | enum F1DisplaySate 6 | { 7 | unset, 8 | placeholder, 9 | raceweek 10 | }; 11 | 12 | class F1Display { 13 | public: 14 | virtual void displaySetup() = 0; 15 | 16 | // For when its more than a week before race day 17 | virtual void displayPlaceHolder(const char* raceName, JsonObject races_sessions) = 0; 18 | virtual void displayRaceWeek(const char* raceName, JsonObject races_sessions) = 0; 19 | virtual void drawWifiManagerMessage(WiFiManager *myWiFiManager) = 0; 20 | 21 | F1DisplaySate state; 22 | 23 | void setRaceName(const char* raceName){ 24 | strcpy(_currentRaceName, raceName); 25 | } 26 | 27 | bool isSameRace(const char* raceName){ 28 | return strcmp(_currentRaceName, raceName) == 0; 29 | } 30 | 31 | void setWidth(int w) { 32 | screenWidth = w; 33 | screenCenterX = screenWidth / 2; 34 | } 35 | 36 | void setHeight(int h) { 37 | screenHeight = h; 38 | } 39 | 40 | protected: 41 | int screenWidth; 42 | int screenHeight; 43 | int screenCenterX; 44 | char _currentRaceName[200]; 45 | }; 46 | #endif 47 | -------------------------------------------------------------------------------- /F1-Notifications/getImage.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // i.imgur.com 4 | // USERTrust RSA Certification Authority 5 | 6 | const char IMGUR_CERTIFICATE_ROOT[] = R"=EOF=( 7 | -----BEGIN CERTIFICATE----- 8 | MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB 9 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 10 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 11 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw 12 | MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV 13 | BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU 14 | aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy 15 | dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 16 | AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B 17 | 3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY 18 | tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ 19 | Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 20 | VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT 21 | 79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 22 | c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT 23 | Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l 24 | c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee 25 | UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE 26 | Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd 27 | BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G 28 | A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF 29 | Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO 30 | VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 31 | ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs 32 | 8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR 33 | iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze 34 | Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ 35 | XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ 36 | qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB 37 | VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB 38 | L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG 39 | jjxDah2nGN59PRbxYvnKkKj9 40 | -----END CERTIFICATE----- 41 | )=EOF="; 42 | 43 | // file name for where to save the image. 44 | #define TRACK_IMAGE "/track.png" 45 | 46 | const char *getImageUrlForRace(const char *raceName) 47 | { 48 | if (strcmp(raceName, "Bahrain") == 0) 49 | { 50 | return "https://i.imgur.com/zUqArqi.png"; 51 | } 52 | else if (strcmp(raceName, "Saudi Arabian") == 0) 53 | { 54 | return "https://i.imgur.com/vx6MDSF.png"; 55 | } 56 | else if (strcmp(raceName, "Australian") == 0) 57 | { 58 | return "https://i.imgur.com/ewrhVKU.png"; 59 | } 60 | else if (strcmp(raceName, "Azerbaijan") == 0) 61 | { 62 | return "https://i.imgur.com/H2C6G2Q.png"; 63 | } 64 | else if (strcmp(raceName, "Miami") == 0) 65 | { 66 | return "https://i.imgur.com/mwoQzCm.png"; 67 | } 68 | else if (strcmp(raceName, "Emilia Romagna Grand Prix") == 0) 69 | { 70 | return "https://i.imgur.com/fm6IygV.png"; 71 | } 72 | else if (strcmp(raceName, "Monaco") == 0) 73 | { 74 | return "https://i.imgur.com/Q48IRF1.png"; 75 | } 76 | else if (strcmp(raceName, "Spanish") == 0) 77 | { 78 | return "https://i.imgur.com/GdnHo69.png"; 79 | } 80 | else if (strcmp(raceName, "Canadian") == 0) 81 | { 82 | return "https://i.imgur.com/QNAli6L.png"; 83 | } 84 | else if (strcmp(raceName, "Austrian") == 0) 85 | { 86 | return "https://i.imgur.com/Xu4I91f.png"; 87 | } 88 | else if (strcmp(raceName, "British") == 0) 89 | { 90 | return "https://i.imgur.com/R0snf2W.png"; 91 | } 92 | else if (strcmp(raceName, "Hungarian") == 0) 93 | { 94 | return "https://i.imgur.com/R0snf2W.png"; 95 | } 96 | else if (strcmp(raceName, "Belgian") == 0) 97 | { 98 | return "https://i.imgur.com/Hr3HUGP.png"; 99 | } 100 | else if (strcmp(raceName, "Dutch") == 0) 101 | { 102 | return "https://i.imgur.com/fwyHAy5.png"; 103 | } 104 | else if (strcmp(raceName, "Italian") == 0) 105 | { 106 | return "https://i.imgur.com/KrRzWhh.png"; 107 | } 108 | else if (strcmp(raceName, "Singapore") == 0) 109 | { 110 | return "https://i.imgur.com/di1xFkV.png"; 111 | } 112 | else if (strcmp(raceName, "Japanese") == 0) 113 | { 114 | return "https://i.imgur.com/BINBSn3.png"; 115 | } 116 | else if (strcmp(raceName, "Qatar") == 0) 117 | { 118 | return "https://i.imgur.com/YdpmY5o.png"; 119 | } 120 | else if (strcmp(raceName, "United States") == 0) 121 | { 122 | return "https://i.imgur.com/NzZNjF6.png"; 123 | } 124 | else if (strcmp(raceName, "Mexican") == 0) 125 | { 126 | return "https://i.imgur.com/gvUauKO.png"; 127 | } 128 | else if (strcmp(raceName, "Brazilian") == 0) 129 | { 130 | return "https://i.imgur.com/3g4zz17.png"; 131 | } 132 | else if (strcmp(raceName, "Las Vegas") == 0) 133 | { 134 | return "https://i.imgur.com/er9A6G8.png"; 135 | } 136 | else if (strcmp(raceName, "Abu Dhabi") == 0) 137 | { 138 | return "https://i.imgur.com/QDwWXna.png"; 139 | } 140 | else if (strcmp(raceName, "Chinese") == 0) 141 | { 142 | return "https://i.imgur.com/dpoPdoD.png"; 143 | } 144 | 145 | // Image not found 146 | return "https://i.imgur.com/FRXJ4do.png"; 147 | //"https://i.imgur.com/tvLjDeo.png"; // This is a called off image 148 | } 149 | 150 | int getImage(const char *raceName) 151 | { 152 | 153 | const char *imageUrl = getImageUrlForRace(raceName); 154 | 155 | // In this example I reuse the same filename 156 | // over and over 157 | if (SPIFFS.exists(TRACK_IMAGE) == true) 158 | { 159 | Serial.println("Removing existing image"); 160 | SPIFFS.remove(TRACK_IMAGE); 161 | } 162 | 163 | fs::File f = SPIFFS.open(TRACK_IMAGE, "w+"); 164 | if (!f) 165 | { 166 | Serial.println("file open failed"); 167 | return -1; 168 | } 169 | 170 | secured_client.setCACert(IMGUR_CERTIFICATE_ROOT); 171 | bool gotImage = fileFetcher.getFile((char *)imageUrl, &f); 172 | 173 | // Make sure to close the file! 174 | f.close(); 175 | 176 | return gotImage; 177 | } 178 | -------------------------------------------------------------------------------- /F1-Notifications/githubCert.h: -------------------------------------------------------------------------------- 1 | // USERTrust RSA Certification Authority - as of 21/05/2025 2 | const char *github_server_cert = "-----BEGIN CERTIFICATE-----\n" 3 | "MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\n" 4 | "iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" 5 | "cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" 6 | "BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\n" 7 | "MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\n" 8 | "BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\n" 9 | "aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\n" 10 | "dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n" 11 | "AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n" 12 | "3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\n" 13 | "tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\n" 14 | "Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\n" 15 | "VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n" 16 | "79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\n" 17 | "c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\n" 18 | "Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\n" 19 | "c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\n" 20 | "UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\n" 21 | "Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\n" 22 | "BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\n" 23 | "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\n" 24 | "Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\n" 25 | "VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\n" 26 | "ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n" 27 | "8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\n" 28 | "iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\n" 29 | "Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\n" 30 | "XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\n" 31 | "qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\n" 32 | "VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\n" 33 | "L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\n" 34 | "jjxDah2nGN59PRbxYvnKkKj9\n" 35 | "-----END CERTIFICATE-----\n"; 36 | -------------------------------------------------------------------------------- /F1-Notifications/matrixDisplay.h: -------------------------------------------------------------------------------- 1 | #include "display.h" 2 | 3 | #include "util.h" 4 | 5 | #include 6 | // This is the library for interfacing with the display 7 | 8 | // Can be installed from the library manager (Search for "ESP32 MATRIX DMA") 9 | // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA 10 | 11 | // ---------------------------- 12 | // Dependency Libraries - each one of these will need to be installed. 13 | // ---------------------------- 14 | 15 | // Adafruit GFX library is a dependency for the matrix Library 16 | // Can be installed from the library manager 17 | // https://github.com/adafruit/Adafruit-GFX-Library 18 | 19 | // ------------------------------------- 20 | // ------- Matrix Config ------ 21 | // ------------------------------------- 22 | 23 | const int panelResX = 64; // Number of pixels wide of each INDIVIDUAL panel module. 24 | const int panelResY = 64; // Number of pixels tall of each INDIVIDUAL panel module. 25 | const int panel_chain = 1; // Total number of panels chained one to another 26 | 27 | MatrixPanel_I2S_DMA *dma_display = nullptr; 28 | 29 | uint16_t myBLACK = dma_display->color565(0, 0, 0); 30 | uint16_t myWHITE = dma_display->color565(255, 255, 255); 31 | uint16_t myRED = dma_display->color565(255, 0, 0); 32 | uint16_t myGREEN = dma_display->color565(0, 255, 0); 33 | uint16_t myBLUE = dma_display->color565(0, 0, 255); 34 | 35 | class MatrixDisplay : public F1Display 36 | { 37 | public: 38 | void displaySetup() 39 | { 40 | 41 | Serial.println("matrix display setup"); 42 | setWidth(panelResX * panel_chain); 43 | setHeight(panelResY); 44 | 45 | HUB75_I2S_CFG mxconfig( 46 | panelResX, // module width 47 | panelResY, // module height 48 | panel_chain // Chain length 49 | ); 50 | 51 | // If you are using a 64x64 matrix you need to pass a value for the E pin 52 | // The trinity connects GPIO 18 to E. 53 | // This can be commented out for any smaller displays (but should work fine with it) 54 | mxconfig.gpio.e = 18; 55 | 56 | // May or may not be needed depending on your matrix 57 | // Example of what needing it looks like: 58 | // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/134#issuecomment-866367216 59 | mxconfig.clkphase = false; 60 | 61 | // Some matrix panels use different ICs for driving them and some of them have strange quirks. 62 | // If the display is not working right, try this. 63 | // mxconfig.driver = HUB75_I2S_CFG::FM6126A; 64 | 65 | dma_display = new MatrixPanel_I2S_DMA(mxconfig); 66 | dma_display->begin(); 67 | } 68 | void displayRaceWeek(const char *raceName, JsonObject races_sessions) 69 | { 70 | 71 | const char *raceNameChanged = convertRaceName(raceName); 72 | 73 | // It's race week! 74 | dma_display->fillScreen(myBLACK); 75 | dma_display->setTextSize(1); // size 2 == 16 pixels high 76 | dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line 77 | 78 | int16_t xOne, yOne; 79 | uint16_t w, h; 80 | 81 | // This method updates the variables with what width (w) and height (h) 82 | // the give text will have. 83 | 84 | dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); 85 | 86 | int xPosition = screenCenterX - w / 2; 87 | dma_display->setTextColor(myBLUE); 88 | dma_display->setCursor(xPosition, 2); 89 | dma_display->print(raceNameChanged); 90 | 91 | int yValue = 12; 92 | for (JsonPair kv : races_sessions) 93 | { 94 | printSession(yValue, 95 | matrixSessionCodeToString(kv.key().c_str()), 96 | getConvertedTime(kv.value().as(), "H:i")); 97 | yValue += 10; 98 | } 99 | } 100 | 101 | void displayPlaceHolder(const char *raceName, JsonObject races_sessions) 102 | { 103 | 104 | const char *raceNameChanged = convertRaceName(raceName); 105 | 106 | // Not yet race week 107 | dma_display->fillScreen(myBLACK); 108 | dma_display->setTextSize(1); // size 2 == 16 pixels high 109 | dma_display->setTextWrap(false); // N.B!! Don't wrap at end of line 110 | 111 | int16_t xOne, yOne; 112 | uint16_t w, h; 113 | 114 | // This method updates the variables with what width (w) and height (h) 115 | // the give text will have. 116 | dma_display->getTextBounds("Next Race:", 0, 0, &xOne, &yOne, &w, &h); 117 | int xPosition = screenCenterX - w / 2; 118 | dma_display->setTextColor(myGREEN); 119 | dma_display->setCursor(xPosition, 2); 120 | dma_display->print("Next Race:"); 121 | 122 | // This method updates the variables with what width (w) and height (h) 123 | // the give text will have. 124 | 125 | dma_display->getTextBounds(raceNameChanged, 0, 0, &xOne, &yOne, &w, &h); 126 | 127 | xPosition = screenCenterX - w / 2; 128 | dma_display->setTextColor(myBLUE); 129 | dma_display->setCursor(xPosition, 10); 130 | dma_display->print(raceNameChanged); 131 | 132 | printSession(20, 133 | "GP:", 134 | getConvertedTime(races_sessions["gp"], "M d")); 135 | } 136 | 137 | int displayImage(char *imageFileUri) 138 | { 139 | return 0; 140 | } 141 | 142 | void drawWifiManagerMessage(WiFiManager *myWiFiManager) 143 | { 144 | Serial.println("Entered Conf Mode"); 145 | dma_display->fillScreen(myBLACK); 146 | dma_display->setTextSize(1); // size 1 == 8 pixels high 147 | dma_display->setTextWrap(false); 148 | dma_display->setTextColor(myBLUE); 149 | dma_display->setCursor(0, 0); 150 | dma_display->print(myWiFiManager->getConfigPortalSSID()); 151 | 152 | dma_display->setTextWrap(true); 153 | dma_display->setTextColor(myRED); 154 | dma_display->setCursor(0, 8); 155 | dma_display->print(WiFi.softAPIP()); 156 | } 157 | 158 | private: 159 | const char *matrixSessionCodeToString(const char *sessionCode) 160 | { 161 | if (strcmp(sessionCode, "fp1") == 0) 162 | { 163 | return "FP1:"; 164 | } 165 | else if (strcmp(sessionCode, "fp2") == 0) 166 | { 167 | return "FP2:"; 168 | } 169 | else if (strcmp(sessionCode, "fp3") == 0) 170 | { 171 | return "FP3:"; 172 | } 173 | else if (strcmp(sessionCode, "qualifying") == 0) 174 | { 175 | return "Qual:"; 176 | } 177 | else if (strcmp(sessionCode, "sprintQualifying") == 0) 178 | { 179 | return "Sp Q:"; 180 | } 181 | else if (strcmp(sessionCode, "sprint") == 0) 182 | { 183 | return "Spr:"; 184 | } 185 | else if (strcmp(sessionCode, "gp") == 0) 186 | { 187 | return "Race:"; 188 | } 189 | 190 | return "UNKNOWN"; 191 | } 192 | 193 | void printSession(int y, const char *sessionName, String sessionStartTime) 194 | { 195 | 196 | // Print Session Name on the left 197 | dma_display->setTextColor(myRED); 198 | dma_display->setCursor(1, y); 199 | dma_display->print(sessionName); 200 | 201 | Serial.println(sessionName); 202 | 203 | // Print time on the right 204 | 205 | int16_t xOne, yOne; 206 | uint16_t w, h; 207 | 208 | // This method updates the variables with what width (w) and height (h) 209 | // the give text will have. 210 | Serial.println(sessionStartTime); 211 | dma_display->getTextBounds(sessionStartTime, 0, 0, &xOne, &yOne, &w, &h); 212 | 213 | int xPosition = screenWidth - w; 214 | 215 | Serial.println(xPosition); 216 | 217 | dma_display->setCursor(xPosition, y); 218 | dma_display->print(sessionStartTime); 219 | } 220 | }; 221 | -------------------------------------------------------------------------------- /F1-Notifications/raceLogic.h: -------------------------------------------------------------------------------- 1 | #ifndef RACELOGIC_H 2 | #define RACELOGIC_H 3 | 4 | #define RACE_FILE_NAME "/races.json" 5 | #define CURRENT_RACE_FILE_NAME "/current_races.json" 6 | 7 | // path to the races schedule, needs to be updated each year 8 | #define RACE_JSON_URL "https://raw.githubusercontent.com/sportstimes/f1/main/_db/f1/2025.json" 9 | // Number of days before the race to display circuit image rather than sessions schedule 10 | #define DaysBeforeRace 3 11 | 12 | time_t nextRaceStartUtc; 13 | 14 | Timezone myTZ; 15 | F1Config rl_f1Config; 16 | 17 | void raceLogicSetup(F1Config f1Config) 18 | { 19 | rl_f1Config = f1Config; 20 | } 21 | 22 | bool isSessionInFuture(const char *sessionStartTime) 23 | { 24 | struct tm tm = {0}; 25 | // Parse date from UTC and convert to an epoch 26 | strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); 27 | time_t sessionEpoch = mktime(&tm); 28 | 29 | return UTC.now() < sessionEpoch; 30 | } 31 | 32 | bool isRaceWeek(const char *sessionStartTime) 33 | { 34 | struct tm tm = {0}; 35 | // Parse date from UTC and convert to an epoch 36 | strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); 37 | 38 | time_t DaysBeforeRaceEpoch = mktime(&tm) - (DaysBeforeRace * SECS_PER_DAY); 39 | return UTC.now() > DaysBeforeRaceEpoch; 40 | } 41 | 42 | String getConvertedTime(const char *sessionStartTime, const char *timeFormat = "") 43 | { 44 | struct tm tm = {0}; 45 | // Parse date from UTC and convert to an epoch 46 | strptime(sessionStartTime, "%Y-%m-%dT%H:%M:%S", &tm); 47 | time_t sessionEpoch = mktime(&tm); 48 | 49 | String timeFormatStr = rl_f1Config.timeFormat; 50 | if (timeFormat[0] != 0) 51 | { 52 | timeFormatStr = String(timeFormat); 53 | } 54 | return myTZ.dateTime(sessionEpoch, UTC_TIME, timeFormatStr); 55 | } 56 | 57 | void printConvertedTime(const char *sessionName, const char *sessionStartTime) 58 | { 59 | 60 | String timeStr = getConvertedTime(sessionStartTime, ""); 61 | Serial.print(sessionName); 62 | Serial.print(": "); 63 | Serial.println(timeStr); 64 | } 65 | 66 | const char *sessionCodeToString(const char *sessionCode) 67 | { 68 | if (strcmp(sessionCode, "fp1") == 0) 69 | { 70 | return "FP1: "; 71 | } 72 | else if (strcmp(sessionCode, "fp2") == 0) 73 | { 74 | return "FP2: "; 75 | } 76 | else if (strcmp(sessionCode, "fp3") == 0) 77 | { 78 | return "FP3: "; 79 | } 80 | else if (strcmp(sessionCode, "qualifying") == 0) 81 | { 82 | return "Qualifying: "; 83 | } 84 | else if (strcmp(sessionCode, "sprint") == 0) 85 | { 86 | return "Sprint: "; 87 | } 88 | else if (strcmp(sessionCode, "sprintQualifying") == 0) 89 | { 90 | return "Sprint Quali: "; 91 | } 92 | else if (strcmp(sessionCode, "gp") == 0) 93 | { 94 | return "Race: "; 95 | } 96 | 97 | return "UNKNOWN"; 98 | } 99 | 100 | void printRaceTimes(const char *raceName, JsonObject races_sessions) 101 | { 102 | Serial.print("Next Race: "); 103 | Serial.println(raceName); 104 | 105 | for (JsonPair kv : races_sessions) 106 | { 107 | printConvertedTime(sessionCodeToString(kv.key().c_str()), 108 | kv.value().as()); 109 | } 110 | } 111 | 112 | String createTelegramMessageString(const char *raceName, JsonObject races_sessions) 113 | { 114 | String message = "Next Race: "; 115 | message += raceName; 116 | message += "\n"; 117 | message += "---------------------\n"; 118 | 119 | for (JsonPair kv : races_sessions) 120 | { 121 | String sessionName = String(sessionCodeToString(kv.key().c_str())); 122 | message += sessionName; 123 | message += getConvertedTime(kv.value().as(), ""); 124 | message += "\n"; 125 | } 126 | return message; 127 | } 128 | 129 | bool sendNotificationOfNextRace(UniversalTelegramBot *bot) 130 | { 131 | 132 | StaticJsonDocument<112> filter; 133 | filter["name"] = true; 134 | filter["location"] = true; 135 | filter["round"] = true; 136 | filter["sessions"] = true; 137 | 138 | File racesJson = SPIFFS.open(CURRENT_RACE_FILE_NAME); 139 | DynamicJsonDocument race(1000); 140 | 141 | DeserializationError error = deserializeJson(race, racesJson, DeserializationOption::Filter(filter)); 142 | 143 | if (error) 144 | { 145 | Serial.print("deserializeJson() failed: "); 146 | Serial.println(error.c_str()); 147 | racesJson.close(); 148 | return false; 149 | } 150 | 151 | const char *races_name = race["name"]; 152 | JsonObject races_sessions = race["sessions"]; 153 | 154 | printRaceTimes(races_name, races_sessions); 155 | 156 | Serial.print("Sending message to "); 157 | Serial.println(rl_f1Config.chatId); 158 | racesJson.close(); 159 | return bot->sendPhoto(rl_f1Config.chatId, "https://i.imgur.com/q3qsfSi.png", createTelegramMessageString(races_name, races_sessions)); 160 | } 161 | 162 | int fetchRaceJson(FileFetcher fileFetcher) 163 | { 164 | // In this example I reuse the same filename 165 | // over and over 166 | if (SPIFFS.exists(RACE_FILE_NAME) == true) 167 | { 168 | Serial.println("Removing existing image"); 169 | SPIFFS.remove(RACE_FILE_NAME); 170 | } 171 | 172 | fs::File f = SPIFFS.open(RACE_FILE_NAME, "w+"); 173 | if (!f) 174 | { 175 | Serial.println("file open failed"); 176 | return -1; 177 | } 178 | 179 | bool gotFile = fileFetcher.getFile(RACE_JSON_URL, &f); 180 | 181 | // Make sure to close the file! 182 | f.close(); 183 | 184 | return gotFile; 185 | } 186 | 187 | bool saveCurrentRaceToFile(const JsonObject &raceJson) 188 | { 189 | 190 | if (raceJson.isNull()) 191 | { 192 | Serial.println("Race data is null, nothing to save"); 193 | return false; 194 | } 195 | 196 | File currentRaceFile = SPIFFS.open(CURRENT_RACE_FILE_NAME, "w"); 197 | if (!currentRaceFile) 198 | { 199 | Serial.println("failed to open config file for writing"); 200 | return false; 201 | } 202 | 203 | Serial.println("Saving Race Json"); 204 | serializeJsonPretty(raceJson, Serial); 205 | if (serializeJson(raceJson, currentRaceFile) == 0) 206 | { 207 | Serial.println(F("Failed to write to file")); 208 | return false; 209 | } 210 | currentRaceFile.close(); 211 | return true; 212 | } 213 | 214 | bool getNextRace(int &offset, bool ¬ificationSent, F1Display *f1Display, bool forceRaceFileSave) 215 | { 216 | 217 | StaticJsonDocument<112> filter; 218 | 219 | JsonObject filter_races_0 = filter["races"].createNestedObject(); 220 | filter_races_0["name"] = true; 221 | filter_races_0["location"] = true; 222 | filter_races_0["round"] = true; 223 | filter_races_0["sessions"] = true; 224 | filter_races_0["canceled"] = true; 225 | 226 | File racesJson = SPIFFS.open(RACE_FILE_NAME); 227 | DynamicJsonDocument doc(12288); 228 | 229 | DeserializationError error = deserializeJson(doc, racesJson, DeserializationOption::Filter(filter)); 230 | 231 | if (error) 232 | { 233 | Serial.print("deserializeJson() failed: "); 234 | Serial.println(error.c_str()); 235 | return false; 236 | } 237 | JsonArray races = doc["races"]; 238 | 239 | int racesAmount = races.size(); 240 | time_t timeNow = UTC.now(); 241 | Serial.println(); 242 | Serial.println("UTC: " + UTC.dateTime()); 243 | for (int i = 0; i < racesAmount; i++) 244 | { 245 | 246 | // serializeJsonPretty(races[i], Serial); 247 | 248 | const char *races_name = races[i]["name"]; 249 | JsonObject races_sessions = races[i]["sessions"]; 250 | const char *race_sessions_gp = races_sessions["gp"]; // "2023-03-05T15:00:00Z" 251 | bool raceCanceled = races[i]["canceled"].as(); 252 | 253 | struct tm tm = {0}; 254 | 255 | // Convert to tm struct 256 | // Sample format: 2023-03-17T13:30:00Z 257 | strptime(race_sessions_gp, "%Y-%m-%dT%H:%M:%S", &tm); 258 | 259 | nextRaceStartUtc = mktime(&tm); 260 | if (!raceCanceled && timeNow < nextRaceStartUtc) 261 | { 262 | bool newRace = false; 263 | int roundNumber = races[i]["round"]; 264 | if (roundNumber != offset) 265 | { 266 | if (saveCurrentRaceToFile(races[i])) 267 | { 268 | offset = roundNumber; 269 | notificationSent = false; 270 | Serial.println("New Race"); 271 | newRace = true; 272 | } 273 | else 274 | { 275 | Serial.println("Got new race, but couldn't save JSON to file"); 276 | } 277 | } 278 | else 279 | { 280 | Serial.println("Same Race as before"); 281 | if (forceRaceFileSave) 282 | { 283 | if (saveCurrentRaceToFile(races[i])) 284 | { 285 | Serial.println("(Forced Save) Saved race to file"); 286 | } 287 | else 288 | { 289 | Serial.println("(Forced Save) Couldn't save JSON to file"); 290 | } 291 | } 292 | } 293 | 294 | if (isRaceWeek(race_sessions_gp)) 295 | { 296 | f1Display->displayRaceWeek(races_name, races_sessions); 297 | } 298 | else 299 | { 300 | f1Display->displayPlaceHolder(races_name, races_sessions); 301 | } 302 | 303 | printRaceTimes(races_name, races_sessions); 304 | racesJson.close(); 305 | return newRace; 306 | } 307 | } 308 | racesJson.close(); 309 | return false; 310 | } 311 | 312 | time_t getNotifyTime() 313 | { 314 | 315 | time_t t = nextRaceStartUtc - (6 * SECS_PER_DAY); 316 | // Probably should make this smarter so it's not sending notifications in the middle of the night! 317 | return t; 318 | } 319 | 320 | #endif 321 | -------------------------------------------------------------------------------- /F1-Notifications/races.h: -------------------------------------------------------------------------------- 1 | // This file is old and not used anymore, it now fetches this file from Github 2 | // but it can be useful for debugging, so I'll leave it here 3 | 4 | R"( 5 | { 6 | "races": [ 7 | { 8 | "name": "Bahrain", 9 | "location": "Sakhir", 10 | "latitude": 26.037, 11 | "longitude": 50.5112, 12 | "round": 1, 13 | "slug": "bahrain-grand-prix", 14 | "localeKey": "bahrain-grand-prix", 15 | "circuitImage": "https://imgur.com/zUqArqi.png", 16 | "sessions": { 17 | "fp1": "2023-03-03T11:30:00Z", 18 | "fp2": "2023-03-03T15:00:00Z", 19 | "fp3": "2023-03-04T11:30:00Z", 20 | "qualifying": "2023-03-04T15:00:00Z", 21 | "gp": "2023-03-05T15:00:00Z" 22 | } 23 | }, 24 | { 25 | "name": "Saudi Arabian", 26 | "location": "Jeddah", 27 | "latitude": 21.485811, 28 | "longitude": 39.192505, 29 | "round": 2, 30 | "slug": "saudi-arabia-grand-prix", 31 | "localeKey": "saudi-arabia-grand-prix", 32 | "circuitImage": "https://imgur.com/vx6MDSF.png", 33 | "sessions": { 34 | "fp1": "2023-03-17T13:30:00Z", 35 | "fp2": "2023-03-17T17:00:00Z", 36 | "fp3": "2023-03-18T13:30:00Z", 37 | "qualifying": "2023-03-18T17:00:00Z", 38 | "gp": "2023-03-19T17:00:00Z" 39 | } 40 | }, 41 | { 42 | "name": "Australian", 43 | "location": "Melbourne", 44 | "latitude": -37.8373, 45 | "longitude": 144.9666, 46 | "round": 3, 47 | "slug": "australian-grand-prix", 48 | "localeKey": "australian-grand-prix", 49 | "circuitImage": "https://i.imgur.com/ewrhVKU.png", 50 | "sessions": { 51 | "fp1": "2023-03-31T01:30:00Z", 52 | "fp2": "2023-03-31T05:00:00Z", 53 | "fp3": "2023-04-01T01:30:00Z", 54 | "qualifying": "2023-04-01T05:00:00Z", 55 | "gp": "2023-04-02T05:00:00Z" 56 | } 57 | }, 58 | { 59 | "name": "Azerbaijan", 60 | "location": "Baku", 61 | "latitude": 40.3699, 62 | "longitude": 49.8433, 63 | "round": 5, 64 | "slug": "azerbaijan-grand-prix", 65 | "localeKey": "azerbaijan-grand-prix", 66 | "circuitImage": "https://imgur.com/H2C6G2Q.png", 67 | "sessions": { 68 | "fp1": "2023-04-28T09:30:00Z", 69 | "qualifying": "2023-04-28T13:00:00Z", 70 | "fp2": "2023-04-29T09:30:00Z", 71 | "sprint": "2023-04-29T13:30:00Z", 72 | "gp": "2023-04-30T11:00:00Z" 73 | } 74 | }, 75 | { 76 | "name": "Miami", 77 | "location": "Miami", 78 | "latitude": 25.957764, 79 | "longitude": -80.238835, 80 | "round": 6, 81 | "slug": "miami-grand-prix", 82 | "localeKey": "miami-grand-prix", 83 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245035/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Miami%20carbon.png.transform/3col/image.png", 84 | "sessions": { 85 | "fp1": "2023-05-05T18:30:00Z", 86 | "fp2": "2023-05-05T22:00:00Z", 87 | "fp3": "2023-05-06T16:30:00Z", 88 | "qualifying": "2023-05-06T20:00:00Z", 89 | "gp": "2023-05-07T19:30:00Z" 90 | } 91 | }, 92 | { 93 | "name": "Emilia Romagna Grand Prix", 94 | "location": "Imola", 95 | "latitude": 44.344576, 96 | "longitude": 11.713808, 97 | "round": 7, 98 | "slug": "emilia-romagna-grand-prix", 99 | "localeKey": "emilia-romagna-grand-prix", 100 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245032/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Emilia%20Romagna%20carbon.png.transform/3col/image.png", 101 | "sessions": { 102 | "fp1": "2023-05-19T11:30:00Z", 103 | "fp2": "2023-05-19T15:00:00Z", 104 | "fp3": "2023-05-20T10:30:00Z", 105 | "qualifying": "2023-05-20T14:00:00Z", 106 | "gp": "2023-05-21T13:00:00Z" 107 | } 108 | }, 109 | { 110 | "name": "Monaco", 111 | "location": "Monte Carlo", 112 | "latitude": 43.7338, 113 | "longitude": 7.4215, 114 | "round": 8, 115 | "slug": "monaco-grand-prix", 116 | "localeKey": "monaco-grand-prix", 117 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245032/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Monte%20Carlo%20carbon.png.transform/3col/image.png", 118 | "sessions": { 119 | "fp1": "2023-05-26T11:30:00Z", 120 | "fp2": "2023-05-26T15:00:00Z", 121 | "fp3": "2023-05-27T10:30:00Z", 122 | "qualifying": "2023-05-27T14:00:00Z", 123 | "gp": "2023-05-28T13:00:00Z" 124 | } 125 | }, 126 | { 127 | "name": "Spanish", 128 | "location": "Catalunya", 129 | "latitude": 41.5638, 130 | "longitude": 2.2585, 131 | "round": 9, 132 | "slug": "spanish-grand-prix", 133 | "localeKey": "spanish-grand-prix", 134 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245030/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Spain%20carbon.png.transform/3col/image.png", 135 | "sessions": { 136 | "fp1": "2023-06-02T11:30:00Z", 137 | "fp2": "2023-06-02T15:00:00Z", 138 | "fp3": "2023-06-03T10:30:00Z", 139 | "qualifying": "2023-06-03T14:00:00Z", 140 | "gp": "2023-06-04T13:00:00Z" 141 | } 142 | }, 143 | { 144 | "name": "Canadian", 145 | "location": "Montreal", 146 | "latitude": 45.5034, 147 | "longitude": -73.5267, 148 | "round": 10, 149 | "slug": "canadian-grand-prix", 150 | "localeKey": "canadian-grand-prix", 151 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Canada%20carbon.png.transform/3col/image.png", 152 | "sessions": { 153 | "fp1": "2023-06-16T17:30:00Z", 154 | "fp2": "2023-06-16T21:00:00Z", 155 | "fp3": "2023-06-17T16:30:00Z", 156 | "qualifying": "2023-06-17T20:00:00Z", 157 | "gp": "2023-06-18T18:00:00Z" 158 | } 159 | }, 160 | { 161 | "name": "Austrian", 162 | "location": "Spielberg", 163 | "latitude": 47.2225, 164 | "longitude": 14.7607, 165 | "round": 11, 166 | "slug": "austrian-grand-prix", 167 | "localeKey": "austrian-grand-prix", 168 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Austria%20carbon.png.transform/3col/image.png", 169 | "sessions": { 170 | "fp1": "2023-06-30T11:30:00Z", 171 | "qualifying": "2023-06-30T15:00:00Z", 172 | "fp2": "2023-07-01T10:30:00Z", 173 | "sprint": "2023-07-01T14:30:00Z", 174 | "gp": "2023-07-02T13:00:00Z" 175 | } 176 | }, 177 | { 178 | "name": "British", 179 | "location": "Silverstone", 180 | "latitude": 52.0706, 181 | "longitude": -1.0174, 182 | "round": 12, 183 | "slug": "british-grand-prix", 184 | "localeKey": "british-grand-prix", 185 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Great%20Britain%20carbon.png.transform/3col/image.png", 186 | "sessions": { 187 | "fp1": "2023-07-07T11:30:00Z", 188 | "fp2": "2023-07-07T15:00:00Z", 189 | "fp3": "2023-07-08T10:30:00Z", 190 | "qualifying": "2023-07-08T14:00:00Z", 191 | "gp": "2023-07-09T14:00:00Z" 192 | } 193 | }, 194 | { 195 | "name": "Hungarian", 196 | "location": "Budapest", 197 | "latitude": 47.583, 198 | "longitude": 19.2526, 199 | "round": 13, 200 | "slug": "hungarian-grand-prix", 201 | "localeKey": "hungarian-grand-prix", 202 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Hungar%20carbon.png.transform/3col/image.png", 203 | "sessions": { 204 | "fp1": "2023-07-21T11:30:00Z", 205 | "fp2": "2023-07-21T15:00:00Z", 206 | "fp3": "2023-07-22T10:30:00Z", 207 | "qualifying": "2023-07-22T14:00:00Z", 208 | "gp": "2023-07-23T13:00:00Z" 209 | } 210 | }, 211 | { 212 | "name": "Belgian", 213 | "location": "Spa-Francorchamps", 214 | "latitude": 50.444, 215 | "longitude": 5.9687, 216 | "round": 14, 217 | "slug": "belgian-grand-prix", 218 | "localeKey": "belgian-grand-prix", 219 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Belgium%20carbon.png.transform/3col/image.png", 220 | "sessions": { 221 | "fp1": "2023-07-28T11:30:00Z", 222 | "qualifying": "2023-07-28T15:00:00Z", 223 | "fp2": "2023-07-29T10:30:00Z", 224 | "sprint": "2023-07-29T14:30:00Z", 225 | "gp": "2023-07-30T13:00:00Z" 226 | } 227 | }, 228 | { 229 | "name": "Dutch", 230 | "location": "Zandvoort", 231 | "latitude": 52.388408, 232 | "longitude": 4.547122, 233 | "round": 15, 234 | "slug": "dutch-grand-prix", 235 | "localeKey": "dutch-grand-prix", 236 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245033/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Netherlands%20carbon.png.transform/3col/image.png", 237 | "sessions": { 238 | "fp1": "2023-08-25T10:30:00Z", 239 | "fp2": "2023-08-25T14:00:00Z", 240 | "fp3": "2023-08-26T09:30:00Z", 241 | "qualifying": "2023-08-26T13:00:00Z", 242 | "gp": "2023-08-27T13:00:00Z" 243 | } 244 | }, 245 | { 246 | "name": "Italian", 247 | "location": "Monza", 248 | "latitude": 45.6169, 249 | "longitude": 9.2825, 250 | "round": 16, 251 | "slug": "italian-grand-prix", 252 | "localeKey": "italian-grand-prix", 253 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245031/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Italy%20carbon.png.transform/3col/image.png", 254 | "sessions": { 255 | "fp1": "2023-09-01T11:30:00Z", 256 | "fp2": "2023-09-01T15:00:00Z", 257 | "fp3": "2023-09-02T10:30:00Z", 258 | "qualifying": "2023-09-02T14:00:00Z", 259 | "gp": "2023-09-03T13:00:00Z" 260 | } 261 | }, 262 | { 263 | "name": "Singapore", 264 | "location": "Singapore", 265 | "latitude": 1.2857, 266 | "longitude": 103.8575, 267 | "round": 17, 268 | "slug": "singapore-grand-prix", 269 | "localeKey": "singapore-grand-prix", 270 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245031/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Singapor%20carbon.png.transform/3col/image.png", 271 | "sessions": { 272 | "fp1": "2023-09-15T09:30:00Z", 273 | "fp2": "2023-09-15T13:00:00Z", 274 | "fp3": "2023-09-16T09:30:00Z", 275 | "qualifying": "2023-09-16T13:00:00Z", 276 | "gp": "2023-09-17T12:00:00Z" 277 | } 278 | }, 279 | { 280 | "name": "Japanese", 281 | "location": "Suzuka", 282 | "latitude": 35.3689, 283 | "longitude": 138.9256, 284 | "round": 18, 285 | "slug": "japanese-grand-prix", 286 | "localeKey": "japanese-grand-prix", 287 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245031/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Japan%20carbon.png.transform/3col/image.png", 288 | "sessions": { 289 | "fp1": "2023-09-22T02:30:00Z", 290 | "fp2": "2023-09-22T06:00:00Z", 291 | "fp3": "2023-09-23T02:30:00Z", 292 | "qualifying": "2023-09-23T06:00:00Z", 293 | "gp": "2023-09-24T05:00:00Z" 294 | } 295 | }, 296 | { 297 | "name": "Qatar", 298 | "location": "Doha", 299 | "latitude": 25.490292, 300 | "longitude": 51.453030, 301 | "round": 19, 302 | "slug": "qatar-grand-prix", 303 | "localeKey": "qatar-grand-prix", 304 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245031/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Qatar%20carbon.png.transform/3col/image.png", 305 | "sessions": { 306 | "fp1": "2023-10-06T13:30:00Z", 307 | "qualifying": "2023-10-06T17:00:00Z", 308 | "fp2": "2023-10-07T13:30:00Z", 309 | "sprint": "2023-10-07T17:30:00Z", 310 | "gp": "2023-10-08T17:00:00Z" 311 | } 312 | }, 313 | { 314 | "name": "United States", 315 | "location": "Austin", 316 | "latitude": 30.1328, 317 | "longitude": -97.6411, 318 | "round": 20, 319 | "slug": "us-grand-prix", 320 | "localeKey": "us-grand-prix", 321 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677245035/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/USA%20carbon.png.transform/3col/image.png", 322 | "sessions": { 323 | "fp1": "2023-10-20T17:30:00Z", 324 | "qualifying": "2023-10-20T21:00:00Z", 325 | "fp2": "2023-10-21T18:00:00Z", 326 | "sprint": "2023-10-21T22:00:00Z", 327 | "gp": "2023-10-22T19:00:00Z" 328 | } 329 | }, 330 | { 331 | "name": "Mexican", 332 | "location": "Mexico City", 333 | "latitude": 19.4028, 334 | "longitude": -99.0986, 335 | "round": 21, 336 | "slug": "mexican-grand-prix", 337 | "localeKey": "mexico-grand-prix", 338 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Mexico%20carbon.png.transform/3col/image.png", 339 | "sessions": { 340 | "fp1": "2023-10-27T18:30:00Z", 341 | "fp2": "2023-10-27T22:00:00Z", 342 | "fp3": "2023-10-28T17:30:00Z", 343 | "qualifying": "2023-10-28T21:00:00Z", 344 | "gp": "2023-10-29T20:00:00Z" 345 | } 346 | }, 347 | { 348 | "name": "Brazilian", 349 | "location": "Sao Paulo", 350 | "latitude": -23.7014, 351 | "longitude": -46.6969, 352 | "round": 22, 353 | "slug": "brazilian-grand-prix", 354 | "localeKey": "brazilian-grand-prix", 355 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Brazil%20carbon.png.transform/3col/image.png", 356 | "sessions": { 357 | "fp1": "2023-11-03T14:30:00Z", 358 | "qualifying": "2023-11-03T18:00:00Z", 359 | "fp2": "2023-11-04T14:30:00Z", 360 | "sprint": "2023-11-04T18:30:00Z", 361 | "gp": "2023-11-05T17:00:00Z" 362 | } 363 | }, 364 | { 365 | "name": "Las Vegas", 366 | "location": "Las Vegas", 367 | "latitude": 36.166747, 368 | "longitude": -115.148708, 369 | "round": 23, 370 | "slug": "las-vegas-grand-prix", 371 | "localeKey": "las-vegas-grand-prix", 372 | "circuitImage": "https://media.formula1.com/image/upload/f_auto/q_auto/v1677249931/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Las%20Vegas%20carbon.png.transform/3col/image.png", 373 | "sessions": { 374 | "fp1": "2023-11-17T04:30:00Z", 375 | "fp2": "2023-11-17T08:00:00Z", 376 | "fp3": "2023-11-18T04:30:00Z", 377 | "qualifying": "2023-11-18T08:00:00Z", 378 | "gp": "2023-11-19T06:00:00Z" 379 | } 380 | }, 381 | { 382 | "name": "Abu Dhabi", 383 | "location": "Yas Marina", 384 | "latitude": 24.4821, 385 | "longitude": 54.3482, 386 | "round": 24, 387 | "slug": "abu-dhabi-grand-prix", 388 | "localeKey": "abu-dhabi-grand-prix", 389 | "circuitImage": "https://media.formula1.com/image/upload/content/dam/fom-website/2018-redesign-assets/Track%20icons%204x3/Abu%20Dhab%20carbon.png.transform/3col/image.png", 390 | "sessions": { 391 | "fp1": "2023-11-24T09:30:00Z", 392 | "fp2": "2023-11-24T13:00:00Z", 393 | "fp3": "2023-11-25T10:30:00Z", 394 | "qualifying": "2023-11-25T14:00:00Z", 395 | "gp": "2023-11-26T13:00:00Z" 396 | } 397 | } 398 | ] 399 | } 400 | )" 401 | -------------------------------------------------------------------------------- /F1-Notifications/util.h: -------------------------------------------------------------------------------- 1 | 2 | const char *convertRaceName(const char *raceName) 3 | { 4 | if (strcmp(raceName, "Emilia Romagna Grand Prix") == 0) 5 | { 6 | return "Imola"; 7 | } 8 | else 9 | { 10 | return raceName; 11 | } 12 | } -------------------------------------------------------------------------------- /F1-Notifications/wifiManagerHandler.h: -------------------------------------------------------------------------------- 1 | 2 | // Number of seconds after reset during which a 3 | // subseqent reset will be considered a double reset. 4 | #define DRD_TIMEOUT 10 5 | 6 | // RTC Memory Address for the DoubleResetDetector to use 7 | #define DRD_ADDRESS 0 8 | 9 | #define WM_F1_TIME_ZONE_LABEL "timeZone" 10 | #define WM_F1_TIME_FORMAT_LABEL "timeFormat" 11 | #define WM_F1_BOT_TOKEN_LABEL "botToken" 12 | #define WM_F1_CHAT_ID_LABEL "chatId" 13 | #define WM_F1_NOTIFCATION_LABEL "notification" 14 | 15 | DoubleResetDetector* drd; 16 | F1Display* wm_Display; 17 | 18 | //flag for saving data 19 | bool shouldSaveConfig = false; 20 | 21 | //callback notifying us of the need to save config 22 | void saveConfigCallback () { 23 | Serial.println("Should save config"); 24 | shouldSaveConfig = true; 25 | } 26 | 27 | void configModeCallback (WiFiManager *myWiFiManager) { 28 | wm_Display->drawWifiManagerMessage(myWiFiManager); 29 | } 30 | 31 | void setupWiFiManager(bool forceConfig, F1Config f1Config, F1Display* theDisplay) { 32 | wm_Display = theDisplay; 33 | WiFiManager wm; 34 | //set config save notify callback 35 | wm.setSaveConfigCallback(saveConfigCallback); 36 | //set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode 37 | wm.setAPCallback(configModeCallback); 38 | 39 | WiFiManagerParameter timeZoneParam(WM_F1_TIME_ZONE_LABEL, "Time Zone", f1Config.timeZone.c_str(), 60); 40 | WiFiManagerParameter timeFormatParam(WM_F1_TIME_FORMAT_LABEL, "Time Format", f1Config.timeFormat.c_str(), 40); 41 | WiFiManagerParameter telegramBotParam(WM_F1_BOT_TOKEN_LABEL, "Bot Token", f1Config.botToken.c_str(), 60); 42 | WiFiManagerParameter telegramChatIdParam(WM_F1_CHAT_ID_LABEL, "Chat ID", f1Config.chatId.c_str(), 40); 43 | 44 | char checkBox[] = "type=\"checkbox\""; 45 | char checkBoxChecked[] = "type=\"checkbox\" checked"; 46 | char* customHtml; 47 | 48 | if (f1Config.currentRaceNotification) { 49 | customHtml = checkBoxChecked; 50 | } else { 51 | customHtml = checkBox; 52 | } 53 | WiFiManagerParameter isNotificationSent(WM_F1_NOTIFCATION_LABEL, "Notification Sent", "T", 2, customHtml); 54 | 55 | wm.addParameter(&timeZoneParam); 56 | wm.addParameter(&timeFormatParam); 57 | wm.addParameter(&telegramBotParam); 58 | wm.addParameter(&telegramChatIdParam); 59 | wm.addParameter(&isNotificationSent); 60 | 61 | if (forceConfig) { 62 | // IF we forced config this time, lets stop the double reset so it doesn't get stuck in a loop 63 | drd->stop(); 64 | if (!wm.startConfigPortal("f1Thing", "nomikey1")) { 65 | Serial.println("failed to connect and hit timeout"); 66 | delay(3000); 67 | //reset and try again, or maybe put it to deep sleep 68 | ESP.restart(); 69 | delay(5000); 70 | } 71 | } else { 72 | if (!wm.autoConnect("f1Thing", "nomikey1")) { 73 | Serial.println("failed to connect and hit timeout"); 74 | delay(3000); 75 | // if we still have not connected restart and try all over again 76 | ESP.restart(); 77 | delay(5000); 78 | } 79 | } 80 | 81 | //save the custom parameters to FS 82 | if (shouldSaveConfig) 83 | { 84 | 85 | f1Config.timeZone = String(timeZoneParam.getValue()); 86 | f1Config.timeFormat = String(timeFormatParam.getValue()); 87 | f1Config.botToken = String(telegramBotParam.getValue()); 88 | f1Config.chatId = String(telegramChatIdParam.getValue()); 89 | f1Config.currentRaceNotification = (strncmp(isNotificationSent.getValue(), "T", 1) == 0); 90 | 91 | f1Config.saveConfigFile(); 92 | drd->stop(); 93 | ESP.restart(); 94 | delay(5000); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /GitHubPages/ESPWebTools/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "F1 Notifications Firmware", 3 | "new_install_prompt_erase": false, 4 | "builds": [ 5 | { 6 | "chipFamily": "ESP32", 7 | "parts": [ 8 | { 9 | "path": "boot_app0.bin", 10 | "offset": 57344 11 | }, 12 | { 13 | "path": "firmware.bin", 14 | "offset": 65536 15 | }, 16 | { 17 | "path": "bootloader.bin", 18 | "offset": 4096 19 | }, 20 | { 21 | "path": "partitions.bin", 22 | "offset": 32768 23 | } 24 | ] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /GitHubPages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

F1 Notifications

7 | 8 |

Display F1 Session times and get notified by telegram

9 | 10 | 36 | 39 | 40 | 41 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brian Lough 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F1-Arduino-Notifications 2 | 3 | An ESP32 project to display and notify of when F1 races are and what time the sessions start at in your local timezone. 4 | 5 | ![image](https://github.com/witnessmenow/F1-Arduino-Notifications/assets/1562562/ddc0c493-4931-4c45-a793-b55f95eb355a) 6 | 7 | ![image](https://github.com/witnessmenow/F1-Arduino-Notifications/assets/1562562/383d98ef-5887-4b78-b792-485d60d4a772) 8 | 9 | ![image](https://github.com/witnessmenow/F1-Arduino-Notifications/assets/1562562/3bcfdb33-3c6f-4777-8bc6-21c821566a0f) 10 | 11 | This project can flashed directly from a webpage, so can be setup in minutes! 12 | 13 | ## Features 14 | 15 | - Connects to your Wifi to automatically fetch data about the next race 16 | - When the race is more than a week away, it will display the date of the next race (with an image on the "Cheap Yellow Display") 17 | - When it's race week, it will display the start time of all the sessions in your local timezone 18 | - It will send you notification on Telegram on the Monday of race week with the start times. 19 | 20 | This project is a Work in Progress! 21 | 22 | ## Help Support what I do! 23 | 24 | [If you enjoy my work, please consider becoming a Github sponsor!](https://github.com/sponsors/witnessmenow/) 25 | 26 | ## Hardware Required 27 | 28 | This project is designed to make use of basically ready to go hardware, so is very easy to get up and running 29 | 30 | Currently this project runs on two types of hardware: 31 | 32 | ### "Cheap Yellow Display" (CYD) 33 | 34 | An ESP32 With Built in 320x240 LCD with Touch Screen (ESP32-2432S028R), buy from wherever works out cheapest for you: 35 | 36 | - [Aliexpress\*](https://s.click.aliexpress.com/e/_DkSpIjB) 37 | - [Aliexpress\*](https://s.click.aliexpress.com/e/_DkcmuCh) 38 | - [Aliexpress](https://www.aliexpress.com/item/1005004502250619.htm) 39 | - [Makerfabs](https://www.makerfabs.com/sunton-esp32-2-8-inch-tft-with-touch.html) 40 | 41 | ### Matrix panel 42 | 43 | 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. 44 | 45 | The display it uses is a 64x64 HUB75 Matrix Panel. 46 | 47 | All the parts can be purchased from Makerfabs.com: 48 | 49 | - [ESP32 Trinity](https://www.makerfabs.com/esp32-trinity.html) 50 | - [64 x 64 Matrix Panel](https://www.makerfabs.com/64x64-rgb-led-matrix-3mm-pitch.html) 51 | - 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 52 | 53 | \* = Affilate Link 54 | 55 | ### BYOD (Bring your own display) 56 | 57 | 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. 58 | 59 | ## Project Setup 60 | 61 | These steps will only need to be run once. 62 | 63 | ### Step 1 - Telegram Setup (Optional) 64 | 65 | To get notified about the races, you will need some things on Telegram. 66 | 67 | - Install Telegram and setup an account. 68 | - Search for a user called "botFather" and follow instructions to create a new bot. Keep note of this bot token 69 | - Start the chat with the created bot. 70 | - Search for a user called "myIdBot" and follow instructions to get your chat ID. Keep note of the chatID. 71 | 72 | ### Step 2 - Flash the Project 73 | 74 | This project can be flashed directly from your browser [here](https://witnessmenow.github.io/F1-Arduino-Notifications/) (Chrome & Edge only) 75 | 76 | - For the "Cheap Yellow Display" (CYD): 77 | - If your CYD has one USB port, Click the "CYD" button 78 | - If it has two USB ports, Click the "CYD2USB" button 79 | - For ESP32 Trinity/Matrix panel 80 | - Click the "Matrix" button 81 | 82 | This webflash code is automatically built from the main branch of this repo, so it will always be up to date. 83 | 84 | Note: If you want to program this project yourself without the webflash, follow the **Code** steps below. 85 | 86 | ### Step 3 - Adding your Wifi, Timezone and Telegram Details 87 | 88 | In order to enter your wifi details, the project will host it's own wifi network. Connect to it using your phone. 89 | 90 | - SSID: f1Thing 91 | - Password: nomikey1 92 | 93 | You should be automatically redirected to the config page. 94 | 95 | - Click Config 96 | - Enter your WIfi details 97 | - Enter your **Time Zone** - More info [here](https://github.com/ropg/ezTime#setlocation) 98 | - (Optional) Enter your **Bot Token** that you retrieved at the earlier step. 99 | - (Optional) Enter your **Chat ID** that you retrieved at the earlier step. 100 | - You can leave the other options 101 | - Click save 102 | 103 | Note: If you ever need to get back into WiFiManager, click reset button twice. 104 | 105 | The project should now be setup and start displaying the next race details! 106 | 107 | ## Code 108 | 109 | If you want to program this project manually, there are two options 110 | 111 | ### PlatformIO 112 | 113 | PlatformIO is the easiest way to code this project. 114 | 115 | In the [platformio.ini](platformio.ini), there are several environments defined for the different boards 116 | 117 | | Environment | Description | 118 | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | 119 | | env:cyd | For the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) | 120 | | env:cyd2usb | For the Cheap Yellow Display with two USB ports | 121 | | env:trinity | For the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity) (or generic ESP32 wired to the matrix panel the same) | 122 | 123 | When you select the environment, it will automatically install the right libraries and set the configurations in the code. 124 | 125 | ### Arduino IDE 126 | 127 | If you want to use the Arduino IDE, you will need to do the following to get it workin 128 | 129 | The following libraries need to be installed for this project to work: 130 | 131 | | Library Name/Link | Purpose | Library manager | 132 | | -------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------- | 133 | | [WifiManager - By Tzapu](https://github.com/tzapu/WiFiManager) | Captive portal for configuring the WiFi | Yes ("WifiManager" | 134 | | [ESP_DoubleResetDetector](https://github.com/khoih-prog/ESP_DoubleResetDetector) | Detecting double pressing the reset button | Yes ("ESP_DoubleResetDetector") | 135 | | [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | For parsing JSON | Yes ("Arduino Json") | 136 | | [ezTime](https://github.com/ropg/ezTime) | For handling timezones | Yes ("eztime") | 137 | | [UniversalTelegramBot](https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot) | Telegram bots for your ESP | Yes ("UniversalTelegramBot") | 138 | | [FileFetcher](https://github.com/witnessmenow/file-fetcher-arduino) | For fetching files/images from the web | No, download from Github | 139 | 140 | #### Cheap Yellow Display Specific libraries 141 | 142 | | Library Name/Link | Purpose | Library manager | 143 | | ---------------------------------------------- | ------------------------------- | ---------------- | 144 | | [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | For controlling the LCD Display | Yes ("tft_espi") | 145 | | [PNGdec](https://github.com/bitbank2/PNGdec) | For decoding png images | Yes ("PNGdec") | 146 | 147 | #### Matrix Panel Specific libraries 148 | 149 | | Library Name/Link | Purpose | Library manager | 150 | | ------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------ | 151 | | [ESP32-HUB75-MatrixPanel-I2S-DMA](https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA) | For controlling the LED Matrix | Yes ("ESP32 MATRIX DMA") | 152 | | [Adafruit GFX library](https://github.com/adafruit/Adafruit-GFX-Library) | Dependancy of the Matrix library | Yes ("Adafruit GFX") | 153 | 154 | #### Cheap Yellow Display Display Config 155 | 156 | The CYD version of the project makes use of [TFT_eSPI library by Bodmer](https://github.com/Bodmer/TFT_eSPI). 157 | 158 | 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. 159 | 160 | - CYD (CYD with single USB): https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/User_Setup.h 161 | - CYD2USB (CYD with 2 USB): https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/CYD2USB/User_Setup.h 162 | 163 | #### Display Selection 164 | 165 | 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. 166 | 167 | By default it will use the Cheap Yellow Display 168 | 169 | Note: CYD and CYD2USB both use the `YELLOW_DISPLAY` define. 170 | -------------------------------------------------------------------------------- /images/320x240/1 - Bahrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/1 - Bahrain.png -------------------------------------------------------------------------------- /images/320x240/10 - Austria.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/10 - Austria.png -------------------------------------------------------------------------------- /images/320x240/11 - British.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/11 - British.png -------------------------------------------------------------------------------- /images/320x240/12 - Hungary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/12 - Hungary.png -------------------------------------------------------------------------------- /images/320x240/13 - Belgium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/13 - Belgium.png -------------------------------------------------------------------------------- /images/320x240/14 - Dutch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/14 - Dutch.png -------------------------------------------------------------------------------- /images/320x240/15 - Monza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/15 - Monza.png -------------------------------------------------------------------------------- /images/320x240/16 - Singapore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/16 - Singapore.png -------------------------------------------------------------------------------- /images/320x240/17 - Japan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/17 - Japan.png -------------------------------------------------------------------------------- /images/320x240/18 - Qatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/18 - Qatar.png -------------------------------------------------------------------------------- /images/320x240/19 - USA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/19 - USA.png -------------------------------------------------------------------------------- /images/320x240/2 - SaudiArabia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/2 - SaudiArabia.png -------------------------------------------------------------------------------- /images/320x240/20 - Mexico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/20 - Mexico.png -------------------------------------------------------------------------------- /images/320x240/21 - Brazil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/21 - Brazil.png -------------------------------------------------------------------------------- /images/320x240/22 - Vegas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/22 - Vegas.png -------------------------------------------------------------------------------- /images/320x240/23 - AbuDhabi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/23 - AbuDhabi.png -------------------------------------------------------------------------------- /images/320x240/3 - Australia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/3 - Australia.png -------------------------------------------------------------------------------- /images/320x240/4 - Azerbaijan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/4 - Azerbaijan.png -------------------------------------------------------------------------------- /images/320x240/5 - Miami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/5 - Miami.png -------------------------------------------------------------------------------- /images/320x240/7- Monaco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/7- Monaco.png -------------------------------------------------------------------------------- /images/320x240/8 - Spain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/8 - Spain.png -------------------------------------------------------------------------------- /images/320x240/9 - Canada.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/9 - Canada.png -------------------------------------------------------------------------------- /images/320x240/Imola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/Imola.png -------------------------------------------------------------------------------- /images/320x240/calledOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/calledOff.png -------------------------------------------------------------------------------- /images/320x240/imageNotFound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witnessmenow/F1-Arduino-Notifications/f7db8c18f0dbe41d485720dbe838cc3b9b30d13c/images/320x240/imageNotFound.png -------------------------------------------------------------------------------- /images/Readme.md: -------------------------------------------------------------------------------- 1 | ## To Get The Images 2 | 3 | - Go to the Forumula1.com website and go to the schedule page. This year is https://www.formula1.com/en/racing/2024.html 4 | - Right click on the track layout under the race you want and click "copy image" 5 | - Paste the image into Paint.net, and resize the canvas to 320x240 pixels. 6 | - Save the image. 7 | - Upload the image to imgur.com 8 | - Click the 3 dots when you hover over the image and click "Get Share links" 9 | - Click "copy link" on the BBCode option 10 | - 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 11 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | src_dir = F1-Notifications 13 | default_envs = cyd 14 | 15 | [env] 16 | platform = espressif32@6.5.0 17 | board = esp32dev 18 | framework = arduino 19 | lib_deps = 20 | khoih-prog/ESP_DoubleResetDetector@^1.3.2 21 | bblanchon/ArduinoJson@^6.21.3 22 | https://github.com/tzapu/WiFiManager.git#v2.0.17 23 | ropg/ezTime@^0.8.3 24 | witnessmenow/UniversalTelegramBot@^1.3.0 25 | https://github.com/witnessmenow/file-fetcher-arduino.git 26 | monitor_speed = 115200 27 | monitor_filters = esp32_exception_decoder 28 | upload_speed = 921600 29 | #lib_ldf_mode = deep+ 30 | 31 | [common_cyd] 32 | lib_deps = 33 | ${env.lib_deps} 34 | bodmer/TFT_eSPI@^2.5.33 35 | bitbank2/PNGdec@^1.0.2 36 | build_flags = 37 | -DYELLOW_DISPLAY 38 | -DUSER_SETUP_LOADED 39 | -DILI9341_2_DRIVER 40 | -DTFT_WIDTH=240 41 | -DTFT_HEIGHT=320 42 | -DTFT_MISO=12 43 | -DTFT_MOSI=13 44 | -DTFT_SCLK=14 45 | -DTFT_CS=15 46 | -DTFT_DC=2 47 | -DTFT_RST=-1 48 | -DTFT_BL=21 49 | -DTFT_BACKLIGHT_ON=HIGH 50 | -DTFT_BACKLIGHT_OFF=LOW 51 | -DLOAD_GLCD 52 | -DSPI_FREQUENCY=55000000 53 | -DSPI_READ_FREQUENCY=20000000 54 | -DSPI_TOUCH_FREQUENCY=2500000 55 | -DLOAD_FONT2 56 | -DLOAD_FONT4 57 | -DLOAD_FONT6 58 | -DLOAD_FONT7 59 | -DLOAD_FONT8 60 | -DLOAD_GFXFF 61 | -DUSE_HSPI_PORT 62 | 63 | [env:cyd] 64 | lib_deps = 65 | ${common_cyd.lib_deps} 66 | build_flags = 67 | ${common_cyd.build_flags} 68 | -DTFT_INVERSION_OFF 69 | 70 | [env:cyd2usb] 71 | lib_deps = 72 | ${common_cyd.lib_deps} 73 | build_flags = 74 | ${common_cyd.build_flags} 75 | -DTFT_INVERSION_ON 76 | 77 | [env:trinity] 78 | lib_deps = 79 | ${env.lib_deps} 80 | mrfaptastic/ESP32 HUB75 LED MATRIX PANEL DMA Display@^3.0.9 81 | adafruit/Adafruit GFX Library@^1.11.9 82 | build_flags = 83 | -DMATRIX_DISPLAY 84 | --------------------------------------------------------------------------------