├── .github └── FUNDING.yml ├── esptool └── esptool.exe ├── stl ├── Departures Board OLED Case Top v1.2.stl ├── Departures Board OLED Case Bottom v1.2.stl └── README.md ├── LICENSE ├── custom_partitions.csv ├── lib ├── xmlListener │ ├── xmlListener.cpp │ └── xmlListener.h ├── md5Utils │ ├── md5Utils.h │ └── md5Utils.cpp ├── weatherClient │ ├── weatherClient.h │ └── weatherClient.cpp ├── stationData │ └── stationData.h ├── githubClient │ ├── githubClient.h │ └── githubClient.cpp ├── TfLdataClient │ ├── TfLdataClient.h │ └── TfLdataClient.cpp ├── xmlStreamingParser │ ├── xmlStreamingParser.h │ └── xmlStreamingParser.cpp ├── raildataXmlClient │ ├── raildataXmlClient.h │ └── raildataXmlClient.cpp └── HTTPUpdateGitHub │ ├── HTTPUpdateGitHub.h │ └── HTTPUpdateGitHub.cpp ├── platformio.ini ├── web ├── keys.htm └── index.htm ├── README.md └── include └── webgui ├── keys.h └── webgraphics.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: gadec.uk 2 | -------------------------------------------------------------------------------- /esptool/esptool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadec-uk/departures-board/HEAD/esptool/esptool.exe -------------------------------------------------------------------------------- /stl/Departures Board OLED Case Top v1.2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadec-uk/departures-board/HEAD/stl/Departures Board OLED Case Top v1.2.stl -------------------------------------------------------------------------------- /stl/Departures Board OLED Case Bottom v1.2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadec-uk/departures-board/HEAD/stl/Departures Board OLED Case Bottom v1.2.stl -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 2 | 3 | Copyright (c) 2025 Gadec Software 4 | 5 | To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -------------------------------------------------------------------------------- /custom_partitions.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x1E0000, 5 | app1, app, ota_1, 0x1F0000,0x1E0000, 6 | spiffs, data, spiffs, 0x3D0000,0x20000, 7 | coredump, data, coredump,0x3F0000,0x10000, 8 | -------------------------------------------------------------------------------- /lib/xmlListener/xmlListener.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * xmlListener Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ -------------------------------------------------------------------------------- /stl/README.md: -------------------------------------------------------------------------------- 1 | # Departures Board Custom Case 2 | 3 | This updated version of the custom case for the Departures Board is now compatible with both Micro-USB and USB-C versions of the processor board. The ESP32 D1 Mini processor is held securely in place by both halves of the case. The OLED screen should be a snug tight fit. Optionally you may wish to fix the screen firmly in place with a few dabs from a hot glue gun (after fully testing!). 4 | 5 | You will need to enable supports when printing both halves of the case (for the USB socket and for the processor retaining clip). 6 | -------------------------------------------------------------------------------- /lib/md5Utils/md5Utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * MD5 Utilities Library - calculate the MD5 hash of a file on the LittleFS file system. Convert a base64 MD5 hash to Hex. 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #pragma once 13 | #include 14 | 15 | class md5Utils { 16 | private: 17 | 18 | public: 19 | md5Utils(); 20 | String calculateFileMD5(const char* filePath); 21 | String base64ToHex(String base64Hash); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/xmlListener/xmlListener.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * xmlListener Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #pragma once 13 | #include 14 | 15 | class xmlListener { 16 | private: 17 | 18 | public: 19 | 20 | virtual void startTag(const char *tagName) = 0; 21 | virtual void endTag(const char *tagName) = 0; 22 | virtual void parameter(const char *param) = 0; 23 | virtual void value(const char *value) = 0; 24 | virtual void attribute(const char *attribute) = 0; 25 | }; 26 | -------------------------------------------------------------------------------- /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 | [env:esp32dev] 12 | platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip 13 | board = esp32dev 14 | board_build.partitions = custom_partitions.csv 15 | build_flags = -DCORE_DEBUG_LEVEL=0 16 | board_build.f_cpu = 240000000L 17 | framework = arduino 18 | lib_deps = 19 | tzapu/WiFiManager@2.0.17 20 | olikraus/U8g2@2.36.5 21 | bblanchon/ArduinoJson@7.2.0 22 | squix78/JsonStreamingParser@1.0.5 23 | -------------------------------------------------------------------------------- /lib/weatherClient/weatherClient.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * OpenWeatherMap Weather Client Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | 15 | class weatherClient: public JsonListener { 16 | 17 | private: 18 | const char* apiHost = "api.openweathermap.org"; 19 | String currentKey = ""; 20 | String currentObject = ""; 21 | int weatherItem = 0; 22 | 23 | String description; 24 | float temperature; 25 | float windSpeed; 26 | 27 | public: 28 | String currentWeather = ""; 29 | String lastErrorMsg = ""; 30 | 31 | weatherClient(); 32 | 33 | bool updateWeather(String apiKey, String lat, String lon); 34 | 35 | virtual void whitespace(char c); 36 | virtual void startDocument(); 37 | virtual void key(String key); 38 | virtual void value(String value); 39 | virtual void endArray(); 40 | virtual void endObject(); 41 | virtual void endDocument(); 42 | virtual void startArray(); 43 | virtual void startObject(); 44 | }; -------------------------------------------------------------------------------- /lib/stationData/stationData.h: -------------------------------------------------------------------------------- 1 | // Common station data structures shared by both data clients 2 | #pragma once 3 | #include 4 | 5 | #define MAXBOARDMESSAGES 4 6 | #define MAXMESSAGESIZE 400 7 | #define MAXCALLINGSIZE 450 8 | #define MAXBOARDSERVICES 9 9 | #define MAXLOCATIONSIZE 45 10 | 11 | #define OTHER 0 12 | #define TRAIN 1 13 | #define BUS 2 14 | 15 | #define UPD_SUCCESS 0 16 | #define UPD_INCOMPLETE 1 17 | #define UPD_UNAUTHORISED 2 18 | #define UPD_HTTP_ERROR 3 19 | #define UPD_TIMEOUT 4 20 | #define UPD_NO_RESPONSE 5 21 | #define UPD_DATA_ERROR 6 22 | #define UPD_NO_CHANGE 7 23 | 24 | struct stnMessages { 25 | int numMessages; 26 | char messages[MAXBOARDMESSAGES][MAXMESSAGESIZE]; 27 | }; 28 | 29 | struct rdService { 30 | char sTime[6]; 31 | char destination[MAXLOCATIONSIZE]; 32 | char via[MAXLOCATIONSIZE]; // also used for line name for TfL 33 | char etd[11]; 34 | char platform[4]; 35 | bool isCancelled; 36 | bool isDelayed; 37 | int trainLength; 38 | byte classesAvailable; 39 | char opco[50]; 40 | 41 | int serviceType; 42 | int timeToStation; // Only for TfL 43 | }; 44 | 45 | struct rdStation { 46 | char location[MAXLOCATIONSIZE]; 47 | bool platformAvailable; 48 | int numServices; 49 | bool boardChanged; // Only for TfL 50 | char calling[MAXCALLINGSIZE]; // Only store the calling stops for the first service returned 51 | char origin[MAXLOCATIONSIZE]; // Only store the origin for the first service returned 52 | char serviceMessage[MAXMESSAGESIZE]; // Only store the service message for the first service returned 53 | rdService service[MAXBOARDSERVICES]; 54 | }; 55 | -------------------------------------------------------------------------------- /lib/githubClient/githubClient.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * GitHub Client Library - enables checking for latest release and downloading assets to file system 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | 16 | #define MAX_RELEASE_ASSETS 16 // The maximum number of release asset details that will be read and stored 17 | 18 | class github: public JsonListener { 19 | 20 | private: 21 | const char* apiHost = "api.github.com"; 22 | const char* apiGetLatestRelease = "/repos/gadec-uk/departures-board/releases/latest"; 23 | String currentKey = ""; 24 | String currentArray = ""; 25 | String currentObject = ""; 26 | String previousObject = ""; 27 | 28 | String lastErrorMsg = ""; 29 | 30 | String assetURL; 31 | String assetName; 32 | md5Utils md5; 33 | 34 | public: 35 | String accessToken; 36 | String releaseId; 37 | String releaseDescription; 38 | int releaseAssets; 39 | String releaseAssetURL[MAX_RELEASE_ASSETS]; 40 | String releaseAssetName[MAX_RELEASE_ASSETS]; 41 | 42 | github(String token); 43 | 44 | bool getLatestRelease(); 45 | //bool downloadAssetToLittleFS(String url, String filename); 46 | 47 | String getLastError(); 48 | 49 | virtual void whitespace(char c); 50 | virtual void startDocument(); 51 | virtual void key(String key); 52 | virtual void value(String value); 53 | virtual void endArray(); 54 | virtual void endObject(); 55 | virtual void endDocument(); 56 | virtual void startArray(); 57 | virtual void startObject(); 58 | }; 59 | -------------------------------------------------------------------------------- /lib/TfLdataClient/TfLdataClient.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * TfL London Underground Client Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | 16 | typedef void (*tflClientCallback) (); 17 | 18 | #define MAXLINESIZE 20 19 | #define UGMAXREADSERVICES 20 20 | 21 | class TfLdataClient: public JsonListener { 22 | 23 | private: 24 | 25 | struct ugService { 26 | char destinationName[MAXLOCATIONSIZE]; 27 | char lineName[MAXLINESIZE]; 28 | int timeToStation; 29 | }; 30 | 31 | struct ugStation { 32 | int numServices; 33 | ugService service[UGMAXREADSERVICES]; 34 | }; 35 | 36 | const char* apiHost = "api.tfl.gov.uk"; 37 | String currentKey = ""; 38 | String currentObject = ""; 39 | 40 | int id=0; 41 | bool maxServicesRead = false; 42 | ugStation xStation; 43 | stnMessages xMessages; 44 | 45 | //tflClientCallback Xcb; 46 | bool pruneFromPhrase(char* input, const char* target); 47 | void replaceWord(char* input, const char* target, const char* replacement); 48 | static bool compareTimes(const ugService& a, const ugService& b); 49 | 50 | public: 51 | String lastErrorMsg = ""; 52 | 53 | TfLdataClient(); 54 | int updateArrivals(rdStation *station, stnMessages *messages, const char *locationId, String apiKey, tflClientCallback Xcb); 55 | 56 | virtual void whitespace(char c); 57 | virtual void startDocument(); 58 | virtual void key(String key); 59 | virtual void value(String value); 60 | virtual void endArray(); 61 | virtual void endObject(); 62 | virtual void endDocument(); 63 | virtual void startArray(); 64 | virtual void startObject(); 65 | }; -------------------------------------------------------------------------------- /lib/xmlStreamingParser/xmlStreamingParser.h: -------------------------------------------------------------------------------- 1 | /* 2 | * XML Streaming Parser Library 3 | * - based on the structure of samxl embedded XML parser by Zorxx Software at https://github.com/zorxx/saxml 4 | * 5 | * MIT License 6 | * 7 | * Copyright (c) 2025 Gadec Software 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | * 27 | */ 28 | #pragma once 29 | 30 | #include 31 | #include 32 | 33 | #define XML_BUFFER_MAX_LENGTH 450 34 | 35 | #define STATE_NULL 0 36 | #define STATE_BEGIN 1 37 | #define STATE_STARTTAG 2 38 | #define STATE_TAGNAME 3 39 | #define STATE_TAGCONTENTS 4 40 | #define STATE_ENDTAG 5 41 | #define STATE_EMPTYTAG 6 42 | #define STATE_ATTRIBUTE 7 43 | 44 | class xmlStreamingParser { 45 | private: 46 | 47 | int state; 48 | int nextState; 49 | xmlListener* myListener; 50 | 51 | char buffer[XML_BUFFER_MAX_LENGTH]; 52 | bool bInitialize; // True for the first call into a state 53 | bool inAttrQuote = false; // true if we're inside a quoted attribute string 54 | uint32_t length; 55 | 56 | void state_Begin(const char character); 57 | void state_StartTag(const char character); 58 | void state_TagName(const char character); 59 | void state_EmptyTag(const char character); 60 | void state_TagContents(const char character); 61 | void state_Attribute(const char character); 62 | void state_EndTag(const char character); 63 | void ContextBufferAddChar(const char character); 64 | void ChangeState(int newState); 65 | 66 | public: 67 | xmlStreamingParser(); 68 | void parse(const char character); 69 | void setListener(xmlListener* listener); 70 | void reset(); 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /lib/md5Utils/md5Utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * MD5 Utilities Library - calculate the MD5 hash of a file on the LittleFS file system. Convert a base64 MD5 hash to Hex. 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | md5Utils::md5Utils() {} 17 | 18 | String md5Utils::calculateFileMD5(const char* filePath) { 19 | File file = LittleFS.open(filePath, "r"); 20 | if (!file || file.isDirectory()) { 21 | return String(); // Return empty result if failure 22 | } 23 | 24 | // Initialize MD5 context 25 | mbedtls_md5_context ctx; 26 | mbedtls_md5_init(&ctx); 27 | mbedtls_md5_starts(&ctx); 28 | 29 | // Read file and update MD5 hash 30 | const size_t bufferSize = 512; 31 | uint8_t buffer[bufferSize]; 32 | while (file.available()) { 33 | size_t bytesRead = file.read(buffer, bufferSize); 34 | mbedtls_md5_update(&ctx, buffer, bytesRead); 35 | } 36 | 37 | // Finalize hash 38 | uint8_t hash[16]; 39 | mbedtls_md5_finish(&ctx, hash); 40 | mbedtls_md5_free(&ctx); 41 | file.close(); 42 | 43 | // Convert hash to 32-character hex string 44 | char hexString[33]; 45 | for (int i = 0; i < 16; i++) { 46 | sprintf(&hexString[i * 2], "%02x", hash[i]); 47 | } 48 | hexString[32] = '\0'; 49 | 50 | return String(hexString); 51 | } 52 | 53 | // 54 | // Converts a base64 encoded MD5 hash (as provided in the GitHub response headers) and converts to Hex 55 | // 56 | String md5Utils::base64ToHex(String base64Hash) { 57 | // Base64 decoding map 58 | const char base64Table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 59 | int base64Value[256]; 60 | for (int i = 0; i < 256; i++) base64Value[i] = -1; 61 | for (int i = 0; i < 64; i++) base64Value[(int)base64Table[i]] = i; 62 | 63 | // Decode Base64 into binary data 64 | int len = base64Hash.length(); 65 | int padding = (base64Hash[len - 1] == '=') + (base64Hash[len - 2] == '='); 66 | int binarySize = (len * 3) / 4 - padding; 67 | uint8_t binaryData[binarySize]; 68 | 69 | int index = 0, buffer = 0, bits = 0; 70 | for (int i = 0; i < len; i++) { 71 | int value = base64Value[(int)base64Hash[i]]; 72 | if (value == -1) continue; 73 | buffer = (buffer << 6) | value; 74 | bits += 6; 75 | if (bits >= 8) { 76 | bits -= 8; 77 | binaryData[index++] = (buffer >> bits) & 0xFF; 78 | } 79 | } 80 | 81 | // Convert binary data to hex string 82 | String hexHash = ""; 83 | for (int i = 0; i < binarySize; i++) { 84 | if (binaryData[i] < 16) hexHash += '0'; // Add leading zero for single-digit hex 85 | hexHash += String(binaryData[i], HEX); 86 | } 87 | 88 | return hexHash; 89 | } 90 | -------------------------------------------------------------------------------- /lib/raildataXmlClient/raildataXmlClient.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * raildataXmlClient Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #pragma once 13 | #include 14 | #include 15 | #include 16 | 17 | typedef void (*rdCallback) (int state, int id); 18 | 19 | #define MAXHOSTSIZE 48 20 | #define MAXAPIURLSIZE 48 21 | 22 | 23 | class raildataXmlClient: public xmlListener { 24 | 25 | private: 26 | 27 | struct rdiService { 28 | char sTime[6]; 29 | char destination[MAXLOCATIONSIZE]; 30 | char via[MAXLOCATIONSIZE]; 31 | char origin[MAXLOCATIONSIZE]; 32 | char etd[11]; 33 | char platform[4]; 34 | bool isCancelled; 35 | bool isDelayed; 36 | int trainLength; 37 | byte classesAvailable; 38 | char opco[50]; 39 | char calling[MAXCALLINGSIZE]; 40 | char serviceMessage[MAXMESSAGESIZE]; 41 | int serviceType; 42 | }; 43 | 44 | struct rdiStation { 45 | char location[MAXLOCATIONSIZE]; 46 | bool platformAvailable; 47 | int numServices; 48 | rdiService service[MAXBOARDSERVICES]; 49 | }; 50 | 51 | String grandParentTagName = ""; 52 | String parentTagName = ""; 53 | String tagName = ""; 54 | String tagPath = ""; 55 | int tagLevel = 0; 56 | bool loadingWDSL=false; 57 | String soapURL = ""; 58 | char soapHost[MAXHOSTSIZE]; 59 | char soapAPI[MAXAPIURLSIZE]; 60 | 61 | String currentPath = ""; 62 | rdiStation xStation; 63 | stnMessages xMessages; 64 | 65 | bool addedStopLocation = false; 66 | int id=0; 67 | int coaches=0; 68 | 69 | String lastErrorMessage = ""; 70 | bool firstDataLoad; 71 | bool endXml; 72 | 73 | char filterCrs[4]; 74 | bool filter = false; 75 | bool keepRoute = false; 76 | 77 | rdCallback Xcb; 78 | static bool compareTimes(const rdiService& a, const rdiService& b); 79 | void removeHtmlTags(char* input); 80 | void replaceWord(char* input, const char* target, const char* replacement); 81 | void pruneFromPhrase(char* input, const char* target); 82 | void fixFullStop(char* input); 83 | void sanitiseData(); 84 | void deleteService(int x); 85 | 86 | virtual void startTag(const char *tagName); 87 | virtual void endTag(const char *tagName); 88 | virtual void parameter(const char *param); 89 | virtual void value(const char *value); 90 | virtual void attribute(const char *attribute); 91 | 92 | public: 93 | raildataXmlClient(); 94 | int init(const char *wsdlHost, const char *wsdlAPI, rdCallback RDcb); 95 | int updateDepartures(rdStation *station, stnMessages *messages, const char *crsCode, const char *customToken, int numRows, bool includeBusServices, const char *callingCrsCode); 96 | String getLastError(); 97 | }; -------------------------------------------------------------------------------- /lib/weatherClient/weatherClient.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * OpenWeatherMap Weather Client Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | weatherClient::weatherClient() {} 17 | 18 | bool weatherClient::updateWeather(String apiKey, String lat, String lon) { 19 | 20 | lastErrorMsg = ""; 21 | 22 | JsonStreamingParser parser; 23 | parser.setListener(this); 24 | WiFiClient httpClient; 25 | 26 | int retryCounter=0; 27 | while (!httpClient.connect(apiHost, 80) && (retryCounter++ < 15)){ 28 | delay(200); 29 | } 30 | if (retryCounter>=15) { 31 | lastErrorMsg += F("Connection timeout"); 32 | return false; 33 | } 34 | 35 | String request = "GET /data/2.5/weather?units=metric&lang=en&lat=" + lat + F("&lon=") + lon + F("&appid=") + apiKey + F(" HTTP/1.0\r\nHost: ") + String(apiHost) + F("\r\nConnection: close\r\n\r\n"); 36 | httpClient.print(request); 37 | retryCounter=0; 38 | while(!httpClient.available() && retryCounter++ < 40) { 39 | delay(200); 40 | } 41 | 42 | if (!httpClient.available()) { 43 | // no response within 8 seconds so exit 44 | httpClient.stop(); 45 | lastErrorMsg += F("Response timeout"); 46 | return false; 47 | } 48 | 49 | // Parse status code 50 | String statusLine = httpClient.readStringUntil('\n'); 51 | if (!statusLine.startsWith(F("HTTP/")) || statusLine.indexOf(F("200 OK")) == -1) { 52 | httpClient.stop(); 53 | 54 | if (statusLine.indexOf(F("401")) > 0) { 55 | lastErrorMsg = F("Not Authorized"); 56 | } else if (statusLine.indexOf(F("500")) > 0) { 57 | lastErrorMsg = F("Server Error"); 58 | } else { 59 | lastErrorMsg = statusLine; 60 | } 61 | return false; 62 | } 63 | 64 | // Skip the remaining headers 65 | while (httpClient.connected() || httpClient.available()) { 66 | String line = httpClient.readStringUntil('\n'); 67 | if (line == "\r") break; 68 | } 69 | 70 | bool isBody = false; 71 | char c; 72 | weatherItem=0; 73 | 74 | unsigned long dataSendTimeout = millis() + 10000UL; 75 | while((httpClient.available() || httpClient.connected()) && (millis() < dataSendTimeout)) { 76 | while(httpClient.available()) { 77 | c = httpClient.read(); 78 | if (c == '{' || c == '[') isBody = true; 79 | if (isBody) parser.parse(c); 80 | } 81 | delay(5); 82 | } 83 | httpClient.stop(); 84 | if (millis() >= dataSendTimeout) { 85 | lastErrorMsg += F("Data timeout"); 86 | return false; 87 | } 88 | 89 | lastErrorMsg=""; 90 | 91 | currentWeather = description + " " + String((int)round(temperature)) + "\x80 Wind: " + String((int)round(windSpeed)) + "mph"; 92 | return true; 93 | } 94 | 95 | void weatherClient::whitespace(char c) {} 96 | 97 | void weatherClient::startDocument() {} 98 | 99 | void weatherClient::key(String key) { 100 | currentKey = key; 101 | } 102 | 103 | void weatherClient::value(String value) { 104 | if (currentObject == "weather" && weatherItem==0) { 105 | // Only read the first weather entry in the array 106 | if (currentKey == "description") description = value; 107 | } 108 | else if (currentKey == "temp") temperature = value.toFloat(); 109 | // Windspeed reported in mps, converting to mph 110 | else if (currentKey == "speed") windSpeed = value.toFloat() * 2.23694; 111 | } 112 | 113 | void weatherClient::endArray() {} 114 | 115 | void weatherClient::endObject() { 116 | if (currentObject == "weather") weatherItem++; 117 | currentObject = ""; 118 | } 119 | 120 | void weatherClient::endDocument() {} 121 | 122 | void weatherClient::startArray() {} 123 | 124 | void weatherClient::startObject() { 125 | currentObject = currentKey; 126 | } 127 | -------------------------------------------------------------------------------- /lib/HTTPUpdateGitHub/HTTPUpdateGitHub.h: -------------------------------------------------------------------------------- 1 | /* 2 | * HTTPUpdateGitHub Library (c) 2025 Gadec Software 3 | * - based on orignal HTTPUpdate for ESP32 by Markus Sattler 4 | * - Added ability to use GitHub token for updating from private repositories 5 | * - Added correct handling of redirects 6 | * - Added correct checking of MD5 hash from GitHub server 7 | * - Removed redundant code 8 | * 9 | * This library is free software; you can redistribute it and/or 10 | * modify it under the terms of the GNU Lesser General Public 11 | * License as published by the Free Software Foundation; either 12 | * version 2.1 of the License, or (at your option) any later version. 13 | * 14 | * This library is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | * Lesser General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU Lesser General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 22 | * 23 | */ 24 | 25 | #ifndef ___HTTP_UPDATE_H___ 26 | #define ___HTTP_UPDATE_H___ 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | /// note we use HTTP client errors too so we start at 100 37 | #define HTTP_UE_TOO_LESS_SPACE (-100) 38 | #define HTTP_UE_SERVER_NOT_REPORT_SIZE (-101) 39 | #define HTTP_UE_SERVER_FILE_NOT_FOUND (-102) 40 | #define HTTP_UE_SERVER_FORBIDDEN (-103) 41 | #define HTTP_UE_SERVER_WRONG_HTTP_CODE (-104) 42 | #define HTTP_UE_SERVER_FAULTY_MD5 (-105) 43 | #define HTTP_UE_BIN_VERIFY_HEADER_FAILED (-106) 44 | #define HTTP_UE_BIN_FOR_WRONG_FLASH (-107) 45 | #define HTTP_UE_NO_PARTITION (-108) 46 | 47 | enum HTTPUpdateResult { 48 | HTTP_UPDATE_FAILED, 49 | HTTP_UPDATE_NO_UPDATES, 50 | HTTP_UPDATE_OK 51 | }; 52 | 53 | typedef HTTPUpdateResult t_httpUpdate_return; // backward compatibility 54 | 55 | using HTTPUpdateStartCB = std::function; 56 | using HTTPUpdateRequestCB = std::function; 57 | using HTTPUpdateEndCB = std::function; 58 | using HTTPUpdateErrorCB = std::function; 59 | using HTTPUpdateProgressCB = std::function; 60 | 61 | class HTTPUpdate 62 | { 63 | public: 64 | HTTPUpdate(void); 65 | HTTPUpdate(int httpClientTimeout); 66 | ~HTTPUpdate(void); 67 | 68 | void rebootOnUpdate(bool reboot) 69 | { 70 | _rebootOnUpdate = reboot; 71 | } 72 | 73 | HTTPUpdateResult handleUpdate(WiFiClient& client, const String& uri, const String& token); 74 | 75 | // Notification callbacks 76 | void onStart(HTTPUpdateStartCB cbOnStart) { _cbStart = cbOnStart; } 77 | void onEnd(HTTPUpdateEndCB cbOnEnd) { _cbEnd = cbOnEnd; } 78 | void onError(HTTPUpdateErrorCB cbOnError) { _cbError = cbOnError; } 79 | void onProgress(HTTPUpdateProgressCB cbOnProgress) { _cbProgress = cbOnProgress; } 80 | 81 | int getLastError(void); 82 | String getLastErrorString(void); 83 | 84 | protected: 85 | 86 | bool runUpdate(Stream& in, uint32_t size, String md5, int command = U_FLASH); 87 | 88 | // Set the error and potentially use a CB to notify the application 89 | void _setLastError(int err) { 90 | _lastError = err; 91 | if (_cbError) { 92 | _cbError(err); 93 | } 94 | } 95 | int _lastError; 96 | bool _rebootOnUpdate = true; 97 | 98 | private: 99 | int _httpClientTimeout; 100 | md5Utils md5; 101 | 102 | // Callbacks 103 | HTTPUpdateStartCB _cbStart; 104 | HTTPUpdateEndCB _cbEnd; 105 | HTTPUpdateErrorCB _cbError; 106 | HTTPUpdateProgressCB _cbProgress; 107 | }; 108 | 109 | #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_HTTPUPDATE) 110 | extern HTTPUpdate httpUpdate; 111 | #endif 112 | 113 | #endif /* ___HTTP_UPDATE_H___ */ 114 | -------------------------------------------------------------------------------- /web/keys.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Departures Board 14 | 15 | 16 |
17 |
18 |
19 | Departures Board Setup 20 |
21 | 22 | 23 | You will need a National Rail Enquiries Darwin Lite Webservice token. Enter the token exactly as received (in the format nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn). You can register for a token free of charge here. 24 |
25 |
26 | 27 | 28 | Enter your openweathermap.org api key if you wish to display station weather information. You can register for a free api key here. 29 |
30 |
31 | 32 | 33 | Enter your Transport for London api key (app_key) if you wish to display London Underground station arrivals. You can register for a free api key here. 34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 | 103 | 104 | -------------------------------------------------------------------------------- /lib/githubClient/githubClient.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * GitHub Client Library - enables checking for latest release and downloading assets to file system 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | github::github(String token) { 20 | accessToken = token; // Initialise with a GitHub token if the repository is private 21 | } 22 | 23 | bool github::getLatestRelease() { 24 | 25 | lastErrorMsg = ""; 26 | JsonStreamingParser parser; 27 | parser.setListener(this); 28 | WiFiClientSecure httpsClient; 29 | 30 | httpsClient.setInsecure(); 31 | httpsClient.setTimeout(15000); 32 | 33 | int retryCounter=0; //retry counter 34 | while((!httpsClient.connect(apiHost, 443)) && (retryCounter < 30)){ 35 | delay(200); 36 | retryCounter++; 37 | } 38 | if(retryCounter>=30) { 39 | lastErrorMsg += F("Connection timeout"); 40 | return false; 41 | } 42 | 43 | String request = "GET "+ String(apiGetLatestRelease) + F(" HTTP/1.0\r\nHost: ") + String(apiHost) + F("\r\nuser-agent: esp32/1.0\r\nX-GitHub-Api-Version: 2022-11-28\r\nAccept: application/vnd.github+json\r\n"); 44 | if (accessToken.length()) request += "Authorization: Bearer " + String(accessToken) + F("\r\n"); 45 | request += F("Connection: close\r\n\r\n"); 46 | 47 | httpsClient.print(request); 48 | retryCounter=0; 49 | while(!httpsClient.available()) { 50 | delay(200); 51 | retryCounter++; 52 | if (retryCounter > 25) { 53 | // no response within 5 seconds so quit 54 | httpsClient.stop(); 55 | lastErrorMsg += F("Response timeout"); 56 | return false; 57 | } 58 | } 59 | 60 | while (httpsClient.connected()) { 61 | String line = httpsClient.readStringUntil('\n'); 62 | // check for success code... 63 | if (line.startsWith("HTTP")) { 64 | if (line.indexOf("200 OK") == -1) { 65 | httpsClient.stop(); 66 | lastErrorMsg += line; 67 | return false; 68 | } 69 | } 70 | if (line == "\r") { 71 | // Headers received 72 | break; 73 | } 74 | } 75 | 76 | bool isBody = false; 77 | char c; 78 | releaseId=""; 79 | releaseDescription=""; 80 | releaseAssets=0; 81 | unsigned long dataReceived = 0; 82 | 83 | unsigned long dataSendTimeout = millis() + 12000UL; 84 | while((httpsClient.available() || httpsClient.connected()) && (millis() < dataSendTimeout)) { 85 | while(httpsClient.available()) { 86 | c = httpsClient.read(); 87 | dataReceived++; 88 | if (c == '{' || c == '[') isBody = true; 89 | if (isBody) parser.parse(c); 90 | } 91 | delay(50); 92 | } 93 | httpsClient.stop(); 94 | if (millis() >= dataSendTimeout) { 95 | lastErrorMsg += "Data timeout (" + String(dataReceived) + F(" bytes)"); 96 | return false; 97 | } 98 | 99 | lastErrorMsg=F("SUCCESS"); 100 | 101 | return true; 102 | } 103 | 104 | /* 105 | bool github::downloadAssetToLittleFS(String url, String filename) { 106 | 107 | HTTPClient http; 108 | WiFiClientSecure client; 109 | bool result = true; 110 | int redirectCount = 0; 111 | const int maxRedirects = 5; 112 | 113 | lastErrorMsg = ""; 114 | LittleFS.remove(F("/tempfile")); // delete any leftover temp file 115 | 116 | client.setInsecure(); 117 | 118 | File f = LittleFS.open(F("/tempfile"), "w"); 119 | if (!f) { 120 | lastErrorMsg = F("Failed to create temp file"); 121 | return false; 122 | } 123 | 124 | while (redirectCount < maxRedirects) { 125 | http.begin(client, url); 126 | http.addHeader(F("Accept"), F("application/octet-stream")); 127 | if (accessToken.length()) http.addHeader(F("Authorization"), "Bearer " + accessToken); 128 | http.addHeader(F("X-GitHub-Api-Version"), F("2022-11-28")); 129 | http.addHeader(F("user-agent"), F("esp32/1.0")); 130 | const char * headerkeys[] = { "x-ms-blob-content-md5" }; // GitHub uses x-ms-blob-content-m5d, not x-md5 131 | size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*); 132 | // track the MD5 hash 133 | http.collectHeaders(headerkeys, headerkeyssize); 134 | 135 | int httpCode = http.GET(); 136 | if (httpCode == HTTP_CODE_OK) { 137 | int res = http.writeToStream(&f); 138 | if (res <= 0) { 139 | lastErrorMsg = "WriteToStream failed. Error: " + http.errorToString(res); 140 | result = false; 141 | } 142 | http.end(); 143 | break; 144 | } else if (httpCode == HTTP_CODE_MOVED_PERMANENTLY || 145 | httpCode == HTTP_CODE_FOUND || 146 | httpCode == HTTP_CODE_TEMPORARY_REDIRECT || 147 | httpCode == HTTP_CODE_PERMANENT_REDIRECT) { 148 | // Handle redirect 149 | String newUrl = http.getLocation(); 150 | http.end(); // End current request before retrying 151 | if (newUrl.length() == 0) { 152 | lastErrorMsg = F("HTTP Redirect without Location header!"); 153 | result = false; 154 | break; 155 | } 156 | url = newUrl; 157 | redirectCount++; 158 | } else { 159 | lastErrorMsg = "GET failed, error: " + String(httpCode) + " " + http.errorToString(httpCode); 160 | result = false; 161 | http.end(); 162 | break; 163 | } 164 | } 165 | 166 | f.close(); 167 | 168 | if (result) { 169 | // File downloaded, let's check the MD5 if provided 170 | if (http.hasHeader("x-ms-blob-content-md5")) { 171 | // Convert the base64 encoded MD5 back to a hex string 172 | String md5Server = md5.base64ToHex(String(http.header("x-ms-blob-content-md5"))); 173 | // Calculate the MD5 of the downloaded file 174 | String md5Download = md5.calculateFileMD5("/tempfile"); 175 | if (md5Download != md5Server) { 176 | lastErrorMsg = "\"" + filename.substring(1) + F("\" MD5 mismatch (corruption)"); 177 | return false; 178 | } 179 | } 180 | 181 | // File downloaded so delete the old one 182 | if (LittleFS.exists(filename)) { 183 | if (!LittleFS.remove(filename)) { 184 | lastErrorMsg = "Could not delete existing " + filename; 185 | return false; 186 | } 187 | } 188 | 189 | // Rename temp file 190 | if (!LittleFS.rename("/tempfile", filename)) { 191 | lastErrorMsg = F("Could not rename temp file"); 192 | return false; 193 | } 194 | LittleFS.remove(F("/tempfile")); 195 | lastErrorMsg = F("Success"); 196 | } 197 | 198 | return result; 199 | } 200 | */ 201 | 202 | String github::getLastError() { 203 | return lastErrorMsg; 204 | } 205 | 206 | void github::whitespace(char c) {} 207 | 208 | void github::startDocument() { 209 | currentArray = ""; 210 | currentObject = ""; 211 | } 212 | 213 | void github::key(String key) { 214 | currentKey = key; 215 | } 216 | 217 | void github::value(String value) { 218 | if (currentKey == "tag_name") releaseId = value; 219 | else if ((currentKey == "name") && (currentArray=="")) releaseDescription = value; 220 | else if ((currentKey == "url") && (currentArray=="assets") && (currentObject!="uploader")) assetURL = value; 221 | else if ((currentKey == "name") && (currentArray=="assets") && (currentObject!="uploader")) assetName = value; 222 | 223 | if (assetURL.length() && assetName.length() && releaseAssets 29 | 30 | #define fallthrough __attribute__((__fallthrough__)) 31 | 32 | xmlStreamingParser::xmlStreamingParser() { 33 | reset(); 34 | } 35 | 36 | void xmlStreamingParser::setListener(xmlListener* listener) { 37 | myListener = listener; 38 | } 39 | 40 | void xmlStreamingParser::reset() { 41 | inAttrQuote=false; 42 | ChangeState(STATE_BEGIN); 43 | } 44 | 45 | void xmlStreamingParser::parse(const char character) { 46 | switch (state) { 47 | case STATE_BEGIN: 48 | state_Begin(character); 49 | break; 50 | case STATE_STARTTAG: 51 | state_StartTag(character); 52 | break; 53 | case STATE_TAGNAME: 54 | state_TagName(character); 55 | break; 56 | case STATE_TAGCONTENTS: 57 | state_TagContents(character); 58 | break; 59 | case STATE_ENDTAG: 60 | state_EndTag(character); 61 | break; 62 | case STATE_EMPTYTAG: 63 | state_EmptyTag(character); 64 | break; 65 | case STATE_ATTRIBUTE: 66 | state_Attribute(character); 67 | break; 68 | default: 69 | break; 70 | } 71 | } 72 | 73 | /* Wait for a tag start character */ 74 | void xmlStreamingParser::state_Begin(const char character) { 75 | 76 | if (bInitialize) { 77 | length=0; 78 | buffer[length] = '\0'; 79 | bInitialize=false; 80 | } 81 | 82 | switch (character) 83 | { 84 | case '<': 85 | ChangeState(STATE_STARTTAG); 86 | break; 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | /* We've already found a tag start character, determine if this is start or end tag, 93 | * and parse the tag name */ 94 | void xmlStreamingParser::state_StartTag(const char character) { 95 | 96 | if (bInitialize) bInitialize=false; 97 | 98 | switch(character) 99 | { 100 | case '<': case '>': 101 | /* Syntax error! */ 102 | break; 103 | case ' ': case '\r': case '\n': case '\t': 104 | /* Ignore whitespace */ 105 | break; 106 | case '/': 107 | ChangeState(STATE_ENDTAG); 108 | break; 109 | default: 110 | buffer[0] = character; 111 | length = 1; 112 | buffer[length] = '\0'; 113 | ChangeState(STATE_TAGNAME); 114 | break; 115 | } 116 | } 117 | 118 | void xmlStreamingParser::state_TagName(const char character) { 119 | 120 | nextState = STATE_NULL; 121 | if(bInitialize) 122 | { 123 | /* Expect one character in the buffer; the start of the tag name from the previous state*/ 124 | bInitialize = false; 125 | } 126 | 127 | switch(character) 128 | { 129 | case ' ': case '\r': case '\n': case '\t': 130 | /* Tag name complete, whitespace indicates tag attribute */ 131 | nextState = STATE_ATTRIBUTE; 132 | break; 133 | case '/': 134 | nextState = STATE_EMPTYTAG; // workaround for urls 135 | break; 136 | case '>': 137 | nextState = STATE_TAGCONTENTS; 138 | /* Done with tag, contents may follow */ 139 | break; 140 | default: 141 | ContextBufferAddChar(character); 142 | break; 143 | } 144 | 145 | if(nextState != STATE_NULL) 146 | { 147 | if (length>0) { length++; myListener->startTag(buffer);} 148 | ChangeState(nextState); 149 | } 150 | } 151 | 152 | void xmlStreamingParser::state_EmptyTag(const char character) { 153 | nextState = STATE_NULL; 154 | 155 | if(bInitialize) 156 | { 157 | /* We need to keep the buffer as-is, since it contains the tag name */ 158 | bInitialize = false; 159 | } 160 | 161 | switch(character) 162 | { 163 | case '>': 164 | nextState = STATE_TAGCONTENTS; 165 | break; 166 | default: 167 | break; 168 | } 169 | 170 | if(nextState != STATE_NULL) 171 | { 172 | if (length>0) { length++; myListener->endTag(buffer); } 173 | ChangeState(nextState); 174 | } 175 | } 176 | 177 | void xmlStreamingParser::state_TagContents(const char character) { 178 | nextState = STATE_NULL; 179 | 180 | if(bInitialize) 181 | { 182 | length = 0; 183 | buffer[length] = '\0'; 184 | bInitialize = false; 185 | } 186 | 187 | switch(character) 188 | { 189 | case '<': 190 | nextState = STATE_STARTTAG; 191 | break; 192 | case ' ': case '\r': case '\n': case '\t': 193 | if(length == 0) 194 | break; /* Ignore leading whitespace */ 195 | else 196 | { 197 | // Fallthrough 198 | fallthrough; 199 | } 200 | default: 201 | ContextBufferAddChar(character); 202 | break; 203 | } 204 | 205 | if(nextState != STATE_NULL) 206 | { 207 | if (length>0) { length++; myListener->value(buffer); } 208 | ChangeState(nextState); 209 | } 210 | } 211 | 212 | void xmlStreamingParser::state_Attribute(const char character) { 213 | nextState = STATE_NULL; 214 | 215 | if(bInitialize) 216 | { 217 | length = 0; 218 | buffer[length] = '\0'; 219 | bInitialize = false; 220 | } 221 | 222 | switch(character) 223 | { 224 | case ' ': case '\r': case '\n': case '\t': 225 | if(length == 0) 226 | break; 227 | else 228 | nextState = STATE_ATTRIBUTE; 229 | break; 230 | case '\"': 231 | inAttrQuote = !inAttrQuote; 232 | ContextBufferAddChar('\"'); 233 | break; 234 | case '/': 235 | if (inAttrQuote) { 236 | ContextBufferAddChar('/'); 237 | } else { 238 | /* Handle the case where an attribute is included in an empty tag, 239 | and the attribute name/value has no trailing whitespace 240 | prior to the empty tag terminator. */ 241 | if (length > 0) { 242 | myListener->attribute(buffer); 243 | length = 0; 244 | buffer[length] = '\0'; 245 | } 246 | /* We've found an empty tag that contains at least one attribute. 247 | Since the buffer containing the tag name is long-gone (the attribute 248 | is now in the parser's string buffer), we don't have a way to get it 249 | back. In order to generate a "tagEnd" event, store a dummy string 250 | containing a single space character (which isn't a valid tag name), 251 | which will be provided to the tagEndHandler callback. */ 252 | ContextBufferAddChar(' '); 253 | nextState = STATE_EMPTYTAG; 254 | } 255 | break; 256 | case '>': 257 | if (inAttrQuote) ContextBufferAddChar(character); 258 | else nextState = STATE_TAGCONTENTS; /* Done with tag, contents may follow */ 259 | break; 260 | default: 261 | ContextBufferAddChar(character); 262 | break; 263 | } 264 | 265 | if(nextState != STATE_NULL) 266 | { 267 | if(nextState != STATE_EMPTYTAG) 268 | { 269 | if (length>0) { inAttrQuote=false; length++; myListener->attribute(buffer); } 270 | } 271 | ChangeState(nextState); 272 | } 273 | } 274 | 275 | void xmlStreamingParser::state_EndTag(const char character) { 276 | 277 | nextState=STATE_NULL; 278 | 279 | if(bInitialize) 280 | { 281 | length = 0; 282 | buffer[length] = '\0'; 283 | bInitialize = false; 284 | } 285 | 286 | switch(character) 287 | { 288 | case '<': 289 | /* Syntax error! */ 290 | break; 291 | case ' ': case '\r': case '\n': case '\t': 292 | /* Ignore whitespace */ 293 | break; 294 | case '>': 295 | nextState = STATE_TAGCONTENTS; 296 | break; 297 | default: 298 | ContextBufferAddChar(character); 299 | break; 300 | } 301 | 302 | if(nextState != STATE_NULL) 303 | { 304 | if (length>0) { length++; myListener->endTag(buffer); } 305 | ChangeState(nextState); 306 | } 307 | } 308 | 309 | void xmlStreamingParser::ContextBufferAddChar(const char character) { 310 | if (length < sizeof(buffer)-2) { 311 | buffer[length] = character; 312 | length++; 313 | buffer[length] = '\0'; 314 | } 315 | } 316 | 317 | void xmlStreamingParser::ChangeState(int newState) { 318 | state = newState; 319 | bInitialize=true; 320 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # departures-board [![License Badge](https://img.shields.io/badge/BY--NC--SA%204.0%20License-grey?style=flat&logo=creativecommons&logoColor=white)](https://creativecommons.org/licenses/by-nc-sa/4.0/) 2 | 3 | This is an ESP32 based mini Departures Board replicating those at many UK railway stations using data provided by National Rail's public API and, new in v1.2, replicating London Underground Arrivals boards using data provided by TfL . This implementation uses a 3.12" OLED display panel with SSD1322 display controller onboard. STL files are also provided for 3D printing the custom desktop case. 4 | 5 | ## Features 6 | * All processing is done onboard by the ESP32 processor 7 | * Smooth animation matching the real departures and arrivals boards 8 | * Displays up to the next 9 departures with scheduled time, platform number, destination, calling stations and expected departure time 9 | * Optionally only show services calling at a selected station 10 | * Optionally display an alternate station between specific hours of the day 11 | * Network Rail service messages 12 | * Train information (operator, class, number of coaches etc.) 13 | * Displays up to the next 9 arrivals with time to station (London Underground mode) 14 | * TfL station and network service messages (London Underground mode) 15 | * Fully-featured Web UI - choose any station on the UK network / London Tube & DLR network 16 | * Automatic firmware updates (optional) 17 | * Displays the weather at the selected location (optional) 18 | * STL files provided for custom 3D printed case (**updated: now USB-C compatible**) 19 | 20 | This short video demonstrates the Departures Board in action... 21 | [![Departures Board Demo Video](https://github.com/user-attachments/assets/409b9a82-33a9-4351-ac87-f7e44ac56795)](https://youtu.be/N3pHk6yqwvo) 22 | 23 | ## Quick Start 24 | 25 | ### What you'll need 26 | 27 | 1. An ESP32 D1 Mini board (or clone) - either USB-C or Micro-USB version with CH9102 recommended. Cost around £3 from [AliExpress](https://www.aliexpress.com/item/1005005972627549.html). 28 | 2. A 3.12" 256x64 OLED Display Panel with SSD1322 display controller onboard. Cost around £12 from [AliExpress](https://www.aliexpress.com/item/1005005985371717.html). 29 | 3. A 3D printed case using the [STL](https://github.com/gadec-uk/departures-board/tree/main/stl) files provided. If you don't have a 3D printer, there are several services you can use to get one printed. 30 | 4. A National Rail Darwin Lite API token (these are free of charge - request one [here](https://realtime.nationalrail.co.uk/OpenLDBWSRegistration)). 31 | 5. Optionally, an OpenWeather Map API token to display weather conditions at the selected station (these are also free, sign-up for a free developer account [here](https://home.openweathermap.org/users/sign_up)). 32 | 6. Optionally, a TfL Open Data API token to display the London Underground/DLR Arrivals Board. These are free, sign-up for a free developer account [here](https://api-portal.tfl.gov.uk/signup) 33 | 7. Some intermediate soldering skills. 34 | 35 | 36 | 37 | ### Preparing the OLED display for 4-Wire SPI Mode 38 | 39 | 40 | As supplied, the display is usually shipped with 8-bit 80XX mode enabled. This needs to be changed to 4-Wire SPI mode by removing one link and adding another (the image shows where to make these changes on the rear of the circuit board). 41 | 42 | ### Wiring Guide 43 | 44 | Solder the 4 SPI connections, plus power and ground. The wires **MUST** be soldered to the **BACK** of the ESP32 Mini board (the side without the components) to enable it to sit in place in the case. You can solder directly to the pins on the OLED screen or for the best fit (if you are a more experienced solderer) de-solder and remove the header pins and solder directly to the board. You cannot use Dupont connectors, they will not fit the custom case design. 45 | 46 | | OLED Pin | ESP32 Mini Pin | 47 | |:---------|:-------------:| 48 | | 1 VSS | GND | 49 | | 2 VCC_IN | 3.3v | 50 | | 4 D0/CLK | IO18 | 51 | | 5 D1/DIN | IO23 | 52 | | 14 D/C# | IO5 | 53 | | 16 CS# | IO26 | 54 | 55 | 56 | 57 | ### Installing the firmware 58 | 59 | The project uses the Arduino framework and the ESP32 v3.2.0 core. If you want to build from source, you'll need [PlatformIO](https://platformio.org). 60 | 61 | Alternatively, you can download pre-compiled firmware images from the [releases](https://github.com/gadec-uk/departures-board/releases). These can be installed over the USB serial connection using [esptool](https://github.com/espressif/esptool). If you have python installed, install with *pip install esptool*. For convenience, a pre-compiled executable version for Windows is included [here](https://github.com/gadec-uk/departures-board/tree/main/esptool). 62 | 63 | If the board is not recognised you are probably using a version with the CP2104 USB-to-Serial chip. Drivers for the CP2104 are [here](https://www.silabs.com/developer-tools/usb-to-uart-bridge-vcp-drivers?tab=downloads) 64 | 65 | Attach the ESP32 Mini board via it's USB port and use the following command to flash the firmware: 66 | 67 | ``` 68 | esptool.py --chip esp32 --baud 460800 write_flash -z \ 69 | 0x1000 bootloader.bin \ 70 | 0xe000 boot_app0.bin \ 71 | 0x8000 partitions.bin \ 72 | 0x10000 firmware.bin 73 | ``` 74 | 75 | The tool should automatically find the correct serial port. If it fails to, you can manually specify the correct port by adding *--port COMx* (replace *COMx* with your actual port, e.g. COM3, /dev/ttyUSB0, etc.). 76 | 77 | If using the pre-compiled esptool.exe version on Windows, save the esptool.exe and the four firmware (.bin) files to the same directory. Open a command prompt (Windows Key + R, type cmd and press enter) and change to the directory you saved the files into. Now type the following command on one line and press enter: 78 | ``` 79 | .\esptool --chip esp32 --baud 460800 write_flash -z 0x1000 bootloader.bin 0xe000 boot_app0.bin 0x8000 partitions.bin 0x10000 firmware.bin 80 | ``` 81 | 82 | Subsequent updates can be carried out automatically over-the-air or you can manually update from the Web GUI. 83 | 84 | ### First time configuration 85 | 86 | WiFiManager is used to setup the initial WiFi connection on first boot. The ESP32 will broadcast a temporary WiFi network named "Departures Board", connect to the network and follow the on-screen instuctions. You can also watch a video walkthrough of the entire process below. 87 | [![Departures Board Setup Video](https://github.com/user-attachments/assets/176f0489-d846-42de-913f-eb838d9ab941)](https://youtu.be/bMyI56zwHyc) 88 | 89 | Once the ESP32 has established an Internet connection, the next step is to enter your National Rail API token (and optionally, your OpenWeather Map and Transport for London API tokens). Finally, select a station location. Start typing the location name and valid choices will be displayed as you type. 90 | 91 | ### Web GUI 92 | 93 | At start-up, the ESP32's IP address is displayed. To change the station or to configure other miscellaneous settings, open the web page at that address. The settings available are: 94 | - **Station** - start typing a few characters of a station name and select from the drop-down station picker displayed (National Rail mode). 95 | - **Only show services calling at** - filter services based on *calling at* location (National Rail mode - if you want to see the next trains *to* a particular station). 96 | - **Alternate station** - automatically switch to displaying an alternate station between the hours set here (National Rail mode). 97 | - **Underground Station** - start typing a few characters of an Underground or DLR station name and select from the drop-down station picker displayed (London Underground mode). 98 | - **Brightness** - adjusts the brightness of the OLED screen. 99 | - **London Underground Arrivals Board Mode** - switch between National Rail Departures Board and London Underground Arrivals Board modes. 100 | - **Show the date on screen** - displays the date in the upper-right corner (useful if you're also using this as a desk clock!) 101 | - **Include Bus services** - optionally include bus replacement services (shown with a small bus icon in place of platform number). 102 | - **Include current weather at station location** - this option requires a valid OpenWeather Map API key (see above). 103 | - **Enable automatic firmware updates at startup** - automatically checks for AND installs the latest firmware/Web GUI from this repository. 104 | - **Enable overnight sleep mode (screensaver)** - if you're running the board 24/7, you can help prevent screen burn-in by enabling this option overnight. 105 | 106 | A drop-down menu (top-right) adds the following options: 107 | - **Check for Updates** - manually checks for and installs any updates to the firmware/Web GUI. 108 | - **Edit API Keys** - view/edit your National Rail / OpenWeather Map API keys. 109 | - **Clear WiFi Settings** - deletes the stored WiFi credentials and restarts in WiFiManager mode (useful to change WiFi network). 110 | - **Restart System** - restarts the ESP32. 111 | 112 | #### Other Web GUI Endpoints 113 | 114 | A few other urls have been implemented, primarily for debugging/developer use: 115 | - **/factoryreset** - deletes all configuration information, api keys and WiFi credentials. The entire setup process will need to be repeated. 116 | - **/update** - for manual firmware updates. Download the latest binary from the [releases](https://github.com/gadec-uk/departures-board/releases). Only the **firmware.bin** file should be uploaded via */update*. The other .bin files are not used for upgrades. This method is *not* recommended for normal use. 117 | - **/info** - displays some basic information about the current running state. 118 | - **/formatffs** - formats the filing system, erasing the configuration & Web GUI (but not the WiFi credentials). 119 | - **/dir** - displays a (basic) directory listing of the file system with the ability to view/delete files. 120 | - **/upload** - upload a file to the file system. 121 | 122 | ### Donating 123 | 124 | 125 | This software is completely free to use without obligation. If you would like to support me and encourage ongoing updates, you can [buy me a coffee!](https://buymeacoffee.com/gadec.uk) 126 | 127 | ### License 128 | This work is licensed under **Creative Commons Attribution-NonCommercial-ShareAlike 4.0**. To view a copy of this license, visit [https://creativecommons.org/licenses/by-nc-sa/4.0/](https://creativecommons.org/licenses/by-nc-sa/4.0/) 129 | -------------------------------------------------------------------------------- /lib/HTTPUpdateGitHub/HTTPUpdateGitHub.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * HTTPUpdateGitHub Library (c) 2025 Gadec Software 3 | * - based on orignal HTTPUpdate for ESP32 by Markus Sattler 4 | * - Added ability to use GitHub token for updating from private repositories 5 | * - Added correct handling of redirects 6 | * - Added correct checking of MD5 hash from GitHub server 7 | * - Removed redundant code 8 | * 9 | * This library is free software; you can redistribute it and/or 10 | * modify it under the terms of the GNU Lesser General Public 11 | * License as published by the Free Software Foundation; either 12 | * version 2.1 of the License, or (at your option) any later version. 13 | * 14 | * This library is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | * Lesser General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU Lesser General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 22 | * 23 | */ 24 | 25 | #include 26 | #include 27 | 28 | #include 29 | #include // get running partition 30 | #include 31 | 32 | HTTPUpdate::HTTPUpdate(void) 33 | : _httpClientTimeout(8000) 34 | { 35 | } 36 | 37 | HTTPUpdate::HTTPUpdate(int httpClientTimeout) 38 | : _httpClientTimeout(httpClientTimeout) 39 | { 40 | } 41 | 42 | HTTPUpdate::~HTTPUpdate(void) 43 | { 44 | } 45 | 46 | /** 47 | * return error code as int 48 | */ 49 | int HTTPUpdate::getLastError(void) 50 | { 51 | return _lastError; 52 | } 53 | 54 | /** 55 | * return error code as String 56 | */ 57 | String HTTPUpdate::getLastErrorString(void) 58 | { 59 | 60 | if(_lastError == 0) { 61 | return String(); // no error 62 | } 63 | 64 | // error from Update class 65 | if(_lastError > 0) { 66 | StreamString error; 67 | Update.printError(error); 68 | error.trim(); // remove line ending 69 | return String("Update error: ") + error; 70 | } 71 | 72 | // error from http client 73 | if(_lastError > -100) { 74 | return String("HTTP error: ") + HTTPClient::errorToString(_lastError); 75 | } 76 | 77 | switch(_lastError) { 78 | case HTTP_UE_TOO_LESS_SPACE: 79 | return "Not Enough space"; 80 | case HTTP_UE_SERVER_NOT_REPORT_SIZE: 81 | return "Server Did Not Report Size"; 82 | case HTTP_UE_SERVER_FILE_NOT_FOUND: 83 | return "File Not Found (404)"; 84 | case HTTP_UE_SERVER_FORBIDDEN: 85 | return "Forbidden (403)"; 86 | case HTTP_UE_SERVER_WRONG_HTTP_CODE: 87 | return "Wrong HTTP Code"; 88 | case HTTP_UE_SERVER_FAULTY_MD5: 89 | return "Wrong MD5"; 90 | case HTTP_UE_BIN_VERIFY_HEADER_FAILED: 91 | return "Verify Bin Header Failed"; 92 | case HTTP_UE_BIN_FOR_WRONG_FLASH: 93 | return "New Binary Does Not Fit Flash Size"; 94 | case HTTP_UE_NO_PARTITION: 95 | return "Partition Could Not be Found"; 96 | } 97 | 98 | return String(); 99 | } 100 | 101 | /* 102 | * HTTPUpdate is now exposed directly from the library. 103 | * - optionally pass GitHub token for downloads from private repository. 104 | * - handles redirects correctly. 105 | */ 106 | HTTPUpdateResult HTTPUpdate::handleUpdate(WiFiClient& client, const String& uri = "/", const String& token = "") { 107 | 108 | HTTPUpdateResult ret = HTTP_UPDATE_FAILED; 109 | 110 | HTTPClient http; 111 | int redirectCount = 0; 112 | const int maxRedirects = 5; 113 | String url = uri; 114 | String md5Hex = ""; 115 | 116 | while (redirectCount < maxRedirects) { 117 | 118 | log_d("URL: %s\n",url.c_str()); 119 | if(!http.begin(client, url)) 120 | { 121 | return HTTP_UPDATE_FAILED; 122 | } 123 | 124 | // use HTTP/1.0 for update since the update handler does not support any transfer Encoding 125 | http.useHTTP10(true); 126 | http.setTimeout(_httpClientTimeout); 127 | http.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS); // Disable redirects, they don't work anyway 128 | http.setUserAgent("ESP32-http-Update"); 129 | http.addHeader("Cache-Control", "no-cache"); 130 | // Additional headers for GitHub 131 | http.addHeader("Accept", "application/octet-stream"); 132 | if (token.length()) http.addHeader("Authorization", "Bearer " + token); 133 | http.addHeader("X-GitHub-Api-Version", "2022-11-28"); 134 | 135 | const char * headerkeys[] = { "x-ms-blob-content-md5" }; // GitHub uses x-ms-blob-content-m5d, not x-md5 136 | size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*); 137 | 138 | // track the MD5 hash 139 | http.collectHeaders(headerkeys, headerkeyssize); 140 | 141 | int code = http.GET(); 142 | int len = http.getSize(); 143 | log_d("HTTP GET result %d\n",code); 144 | 145 | if (code == HTTP_CODE_MOVED_PERMANENTLY || 146 | code == HTTP_CODE_FOUND || 147 | code == HTTP_CODE_TEMPORARY_REDIRECT || 148 | code == HTTP_CODE_PERMANENT_REDIRECT) { 149 | // Handle redirect 150 | String newUrl = http.getLocation(); 151 | log_d("[HTTP] Redirected to: %s\n", newUrl.c_str()); 152 | http.end(); // End current request before retrying 153 | if (newUrl.length() == 0) { 154 | log_e("[HTTP] Redirect without Location header!"); 155 | return HTTP_UPDATE_FAILED; 156 | } 157 | url = newUrl; 158 | redirectCount++; 159 | } else { 160 | if(code <= 0) { 161 | log_e("HTTP error: %s\n", http.errorToString(code).c_str()); 162 | _lastError = code; 163 | http.end(); 164 | return HTTP_UPDATE_FAILED; 165 | } 166 | 167 | log_d("Header read fin.\n"); 168 | log_d("Server header:\n"); 169 | log_d(" - code: %d\n", code); 170 | log_d(" - len: %d\n", len); 171 | 172 | if(http.hasHeader("x-ms-blob-content-md5")) { 173 | log_d(" - MD5: %s\n", http.header("x-ms-blob-content-md5").c_str()); 174 | // Convert the base64 encoded MD5 back to a hex string 175 | md5Hex = md5.base64ToHex(String(http.header("x-ms-blob-content-md5"))); 176 | log_d(" - MD5 Hex: %s\n", md5Hex.c_str()); 177 | } 178 | 179 | log_d("ESP32 info:\n"); 180 | log_d(" - free Space: %d\n", ESP.getFreeSketchSpace()); 181 | log_d(" - current Sketch Size: %d\n", ESP.getSketchSize()); 182 | 183 | switch(code) { 184 | case HTTP_CODE_OK: ///< OK (Start Update) 185 | if(len > 0) { 186 | bool startUpdate = true; 187 | int sketchFreeSpace = ESP.getFreeSketchSpace(); 188 | if(!sketchFreeSpace){ 189 | _lastError = HTTP_UE_NO_PARTITION; 190 | return HTTP_UPDATE_FAILED; 191 | } 192 | 193 | if(len > sketchFreeSpace) { 194 | log_e("FreeSketchSpace to low (%d) needed: %d\n", sketchFreeSpace, len); 195 | startUpdate = false; 196 | } 197 | 198 | if(!startUpdate) { 199 | _lastError = HTTP_UE_TOO_LESS_SPACE; 200 | ret = HTTP_UPDATE_FAILED; 201 | } else { 202 | // Warn main app we're starting up... 203 | if (_cbStart) { 204 | _cbStart(); 205 | } 206 | 207 | WiFiClient * tcp = http.getStreamPtr(); 208 | delay(200); 209 | 210 | int command; 211 | command = U_FLASH; 212 | log_d("runUpdate flash...\n"); 213 | 214 | // check for valid first magic byte 215 | if(tcp->peek() != 0xE9) { 216 | log_e("Magic header does not start with 0xE9, starts with %d\n",tcp->peek()); 217 | _lastError = HTTP_UE_BIN_VERIFY_HEADER_FAILED; 218 | http.end(); 219 | return HTTP_UPDATE_FAILED; 220 | } 221 | 222 | if(runUpdate(*tcp, len, md5Hex, command)) { 223 | ret = HTTP_UPDATE_OK; 224 | log_d("Update ok\n"); 225 | http.end(); 226 | // Warn main app we're all done 227 | if (_cbEnd) { 228 | _cbEnd(); 229 | } 230 | 231 | if(_rebootOnUpdate) { 232 | ESP.restart(); 233 | } 234 | 235 | } else { 236 | ret = HTTP_UPDATE_FAILED; 237 | log_e("Update failed\n"); 238 | } 239 | } 240 | } else { 241 | _lastError = HTTP_UE_SERVER_NOT_REPORT_SIZE; 242 | ret = HTTP_UPDATE_FAILED; 243 | log_e("Content-Length was 0 or wasn't set by Server?!\n"); 244 | } 245 | break; 246 | case HTTP_CODE_NOT_MODIFIED: 247 | ///< Not Modified (No updates) 248 | ret = HTTP_UPDATE_NO_UPDATES; 249 | break; 250 | case HTTP_CODE_NOT_FOUND: 251 | _lastError = HTTP_UE_SERVER_FILE_NOT_FOUND; 252 | ret = HTTP_UPDATE_FAILED; 253 | break; 254 | case HTTP_CODE_FORBIDDEN: 255 | _lastError = HTTP_UE_SERVER_FORBIDDEN; 256 | ret = HTTP_UPDATE_FAILED; 257 | break; 258 | default: 259 | _lastError = HTTP_UE_SERVER_WRONG_HTTP_CODE; 260 | ret = HTTP_UPDATE_FAILED; 261 | log_e("HTTP Code is (%d)\n", code); 262 | break; 263 | } 264 | 265 | http.end(); 266 | return ret; 267 | } 268 | } 269 | log_e("Too many redirects!"); 270 | return HTTP_UPDATE_FAILED; 271 | } 272 | 273 | /** 274 | * write Update to flash 275 | * @param in Stream& 276 | * @param size uint32_t 277 | * @param md5 String 278 | * @return true if Update ok 279 | */ 280 | bool HTTPUpdate::runUpdate(Stream& in, uint32_t size, String md5, int command) 281 | { 282 | StreamString error; 283 | 284 | if (_cbProgress) { 285 | Update.onProgress(_cbProgress); 286 | } 287 | 288 | if(!Update.begin(size, command)) { 289 | _lastError = Update.getError(); 290 | Update.printError(error); 291 | error.trim(); // remove line ending 292 | log_e("Update.begin failed! (%s)\n", error.c_str()); 293 | return false; 294 | } 295 | 296 | if (_cbProgress) { 297 | _cbProgress(0, size); 298 | } 299 | 300 | if(md5.length()) { 301 | if(!Update.setMD5(md5.c_str())) { 302 | _lastError = HTTP_UE_SERVER_FAULTY_MD5; 303 | log_e("Update.setMD5 failed! (%s)\n", md5.c_str()); 304 | return false; 305 | } 306 | } 307 | 308 | if(Update.writeStream(in) != size) { 309 | _lastError = Update.getError(); 310 | Update.printError(error); 311 | error.trim(); // remove line ending 312 | log_e("Update.writeStream failed! (%s)\n", error.c_str()); 313 | return false; 314 | } 315 | 316 | if (_cbProgress) { 317 | _cbProgress(size, size); 318 | } 319 | 320 | if(!Update.end()) { 321 | _lastError = Update.getError(); 322 | Update.printError(error); 323 | error.trim(); // remove line ending 324 | log_e("Update.end failed! (%s)\n", error.c_str()); 325 | return false; 326 | } 327 | 328 | return true; 329 | } 330 | 331 | #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_HTTPUPDATE) 332 | HTTPUpdate httpUpdate; 333 | #endif 334 | -------------------------------------------------------------------------------- /lib/TfLdataClient/TfLdataClient.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Departures Board (c) 2025 Gadec Software 3 | * 4 | * TfL London Underground Client Library 5 | * 6 | * https://github.com/gadec-uk/departures-board 7 | * 8 | * This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. 9 | * To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | TfLdataClient::TfLdataClient() {} 18 | 19 | int TfLdataClient::updateArrivals(rdStation *station, stnMessages *messages, const char *locationId, String apiKey, tflClientCallback Xcb) { 20 | 21 | unsigned long perfTimer=millis(); 22 | long dataReceived = 0; 23 | bool bChunked = false; 24 | lastErrorMsg = ""; 25 | 26 | JsonStreamingParser parser; 27 | parser.setListener(this); 28 | WiFiClientSecure httpsClient; 29 | httpsClient.setInsecure(); 30 | httpsClient.setTimeout(15000); 31 | 32 | station->boardChanged=false; 33 | 34 | int retryCounter=0; 35 | while (!httpsClient.connect(apiHost,443) && (retryCounter++ < 15)){ 36 | delay(200); 37 | } 38 | if (retryCounter>=15) { 39 | lastErrorMsg = F("Connection timeout"); 40 | return UPD_NO_RESPONSE; 41 | } 42 | String request = "GET /StopPoint/" + String(locationId) + F("/Arrivals?app_key=") + apiKey + F(" HTTP/1.0\r\nHost: ") + String(apiHost) + F("\r\nConnection: close\r\n\r\n"); 43 | httpsClient.print(request); 44 | Xcb(); 45 | unsigned long ticker = millis()+800; 46 | retryCounter=0; 47 | while(!httpsClient.available() && retryCounter++ < 40) { 48 | delay(200); 49 | } 50 | 51 | if (!httpsClient.available()) { 52 | // no response within 8 seconds so exit 53 | httpsClient.stop(); 54 | lastErrorMsg = F("Response timeout"); 55 | return UPD_TIMEOUT; 56 | } 57 | 58 | // Parse status code 59 | String statusLine = httpsClient.readStringUntil('\n'); 60 | if (!statusLine.startsWith(F("HTTP/")) || statusLine.indexOf(F("200 OK")) == -1) { 61 | httpsClient.stop(); 62 | 63 | if (statusLine.indexOf(F("401")) > 0 || statusLine.indexOf(F("429")) > 0) { 64 | lastErrorMsg = F("Not Authorized"); 65 | return UPD_UNAUTHORISED; 66 | } else if (statusLine.indexOf(F("500")) > 0) { 67 | lastErrorMsg = statusLine; 68 | return UPD_DATA_ERROR; 69 | } else { 70 | lastErrorMsg = statusLine; 71 | return UPD_HTTP_ERROR; 72 | } 73 | } 74 | 75 | // Skip the remaining headers 76 | while (httpsClient.connected() || httpsClient.available()) { 77 | String line = httpsClient.readStringUntil('\n'); 78 | if (line == F("\r")) break; 79 | if (line.startsWith(F("Transfer-Encoding:")) && line.indexOf(F("chunked")) >= 0) bChunked=true; 80 | } 81 | 82 | bool isBody = false; 83 | char c; 84 | id=0; 85 | maxServicesRead = false; 86 | xStation.numServices = 0; 87 | xMessages.numMessages = 0; 88 | for (int i=0;iticker) { 99 | Xcb(); 100 | ticker = millis()+800; 101 | } 102 | } 103 | delay(25); 104 | } 105 | httpsClient.stop(); 106 | if (millis() >= dataSendTimeout) { 107 | lastErrorMsg = F("Timed out during data receive operation - "); 108 | lastErrorMsg += String(dataReceived) + F(" bytes received"); 109 | return UPD_TIMEOUT; 110 | } 111 | 112 | // Update the distruption messages 113 | while (!httpsClient.connect(apiHost, 443) && (retryCounter++ < 15)){ 114 | delay(200); 115 | } 116 | if (retryCounter>=15) { 117 | lastErrorMsg = F("Connection timeout [msgs]"); 118 | return UPD_NO_RESPONSE; 119 | } 120 | request = "GET /StopPoint/" + String(locationId) + F("/Disruption?getFamily=true&flattenResponse=true&app_key=") + apiKey + F(" HTTP/1.0\r\nHost: ") + String(apiHost) + F("\r\nConnection: close\r\n\r\n"); 121 | httpsClient.print(request); 122 | retryCounter=0; 123 | while(!httpsClient.available() && retryCounter++ < 40) { 124 | delay(200); 125 | } 126 | 127 | if (!httpsClient.available()) { 128 | // no response within 8 seconds so exit 129 | httpsClient.stop(); 130 | lastErrorMsg = F("Response timeout [msgs]"); 131 | return UPD_TIMEOUT; 132 | } 133 | 134 | // Parse status code 135 | statusLine = httpsClient.readStringUntil('\n'); 136 | if (!statusLine.startsWith(F("HTTP/")) || statusLine.indexOf(F("200 OK")) == -1) { 137 | httpsClient.stop(); 138 | 139 | if (statusLine.indexOf(F("401")) > 0) { 140 | lastErrorMsg = F("Not Authorized [msgs]"); 141 | return UPD_UNAUTHORISED; 142 | } else if (statusLine.indexOf(F("500")) > 0) { 143 | lastErrorMsg = F("Server Error [msgs]"); 144 | return UPD_DATA_ERROR; 145 | } else { 146 | lastErrorMsg = statusLine; 147 | return UPD_HTTP_ERROR; 148 | } 149 | } 150 | 151 | // Skip the remaining headers 152 | while (httpsClient.connected() || httpsClient.available()) { 153 | String line = httpsClient.readStringUntil('\n'); 154 | if (line == F("\r")) break; 155 | if (line.startsWith(F("Transfer-Encoding:")) && line.indexOf(F("chunked")) >= 0) bChunked=true; 156 | } 157 | 158 | isBody = false; 159 | id=0; 160 | maxServicesRead = false; 161 | parser.reset(); 162 | 163 | dataSendTimeout = millis() + 10000UL; 164 | while((httpsClient.available() || httpsClient.connected()) && (millis() < dataSendTimeout) && (!maxServicesRead)) { 165 | while(httpsClient.available() && !maxServicesRead) { 166 | c = httpsClient.read(); 167 | dataReceived++; 168 | if (c == '{' || c == '[') isBody = true; 169 | if (isBody) parser.parse(c); 170 | if (millis()>ticker) { 171 | Xcb(); 172 | ticker = millis()+800; 173 | } 174 | } 175 | delay(25); 176 | } 177 | httpsClient.stop(); 178 | if (millis() >= dataSendTimeout) { 179 | lastErrorMsg = F("Timed out during msgs data receive operation - "); 180 | lastErrorMsg += String(dataReceived) + F(" bytes received"); 181 | return UPD_TIMEOUT; 182 | } 183 | 184 | // Sort the services by arrival time 185 | size_t arraySize = xStation.numServices; 186 | std::sort(xStation.service, xStation.service+arraySize,compareTimes); 187 | 188 | // Limit results to the nearest UGMAXSERVICES services 189 | if (xStation.numServices > MAXBOARDSERVICES) xStation.numServices = MAXBOARDSERVICES; 190 | 191 | // Check if any of the services have changed 192 | if (xStation.numServices != station->numServices) station->boardChanged=true; 193 | else { 194 | for (int i=0;i1) break; // Only check first two services 196 | if (strcmp(xStation.service[i].destinationName,station->service[i].destination)) { 197 | station->boardChanged=true; 198 | break; 199 | } 200 | } 201 | } 202 | 203 | // Remove line break formatting from messages 204 | for (int i=0;inumServices = xStation.numServices; 210 | for (int i=0;iservice[i].destination,xStation.service[i].destinationName); 212 | strcpy(station->service[i].via,xStation.service[i].lineName); 213 | station->service[i].timeToStation = xStation.service[i].timeToStation; 214 | } 215 | messages->numMessages = xMessages.numMessages; 216 | for (int i=0;imessages[i],xMessages.messages[i]); 217 | 218 | if (bChunked) lastErrorMsg = "WARNING: Chunked response! "; 219 | if (station->boardChanged) { 220 | lastErrorMsg += F("SUCCESS [Primary Service Changed] Update took: "); 221 | lastErrorMsg += String(millis() - perfTimer) + F("ms [") + String(dataReceived) + F("]"); 222 | return UPD_SUCCESS; 223 | } else { 224 | lastErrorMsg += F("SUCCESS Update took: "); 225 | lastErrorMsg += String(millis() - perfTimer) + F("ms [") + String(dataReceived) + F("]"); 226 | return UPD_NO_CHANGE; 227 | } 228 | } 229 | 230 | // 231 | // Function to prune messages from the point at which a word or phrase is found 232 | // 233 | bool TfLdataClient::pruneFromPhrase(char* input, const char* target) { 234 | // Find the first occurance of the target word or phrase 235 | char* pos = strstr(input,target); 236 | // If found, prune from here 237 | if (pos) { 238 | input[pos - input] = '\0'; 239 | return true; 240 | } 241 | return false; 242 | } 243 | 244 | // 245 | // Function to replace occurrences of a word or phrase in a character array 246 | // 247 | void TfLdataClient::replaceWord(char* input, const char* target, const char* replacement) { 248 | // Find the first occurrence of the target word 249 | char* pos = strstr(input, target); 250 | while (pos) { 251 | // Calculate the length of the target word 252 | size_t targetLen = strlen(target); 253 | // Calculate the length difference between target and replacement 254 | int diff = strlen(replacement) - targetLen; 255 | 256 | // Shift the remaining characters to accommodate the replacement 257 | memmove(pos + strlen(replacement), pos + targetLen, strlen(pos + targetLen) + 1); 258 | 259 | // Copy the replacement word into the position 260 | memcpy(pos, replacement, strlen(replacement)); 261 | 262 | // Find the next occurrence of the target word 263 | pos = strstr(pos + strlen(replacement), target); 264 | } 265 | } 266 | 267 | // Custom comparator function to compare time to station 268 | bool TfLdataClient::compareTimes(const ugService& a, const ugService& b) { 269 | return a.timeToStation < b.timeToStation; 270 | } 271 | 272 | void TfLdataClient::whitespace(char c) {} 273 | 274 | void TfLdataClient::startDocument() {} 275 | 276 | void TfLdataClient::key(String key) { 277 | currentKey = key; 278 | if (currentKey == "id") { 279 | // Next entry 280 | if (xStation.numServices 13 | #include 14 | #include 15 | #include 16 | 17 | raildataXmlClient::raildataXmlClient() { 18 | firstDataLoad=true; 19 | } 20 | 21 | // Custom comparator function to compare time strings 22 | bool raildataXmlClient::compareTimes(const rdiService& a, const rdiService& b) { 23 | // Convert time strings to integers for comparison 24 | int hour1, minute1, hour2, minute2; 25 | sscanf(a.sTime, "%d:%d", &hour1, &minute1); 26 | sscanf(b.sTime, "%d:%d", &hour2, &minute2); 27 | 28 | // Compare hours first 29 | if (hour1 != hour2) { 30 | // Fudge for rollover at midnight 31 | if (hour1 < 2 && hour2 > 20) return false; 32 | if (hour2 < 2 && hour1 > 20) return true; 33 | else return hour1 < hour2; 34 | } 35 | // If hours are equal, compare minutes 36 | return minute1 < minute2; 37 | } 38 | 39 | // 40 | // This function obtains the SOAP host and api url from the given wsdlHost and wsdlAPI 41 | // 42 | int raildataXmlClient::init(const char *wsdlHost, const char *wsdlAPI, rdCallback RDcb) 43 | { 44 | Xcb = RDcb; 45 | 46 | WiFiClientSecure httpsClient; 47 | httpsClient.setInsecure(); 48 | httpsClient.setTimeout(15000); 49 | 50 | int retryCounter=0; //retry counter 51 | while((!httpsClient.connect(wsdlHost, 443)) && (retryCounter < 30)){ 52 | delay(100); 53 | retryCounter++; 54 | } 55 | if(retryCounter==30) { 56 | return UPD_NO_RESPONSE; // No response within 3s 57 | } 58 | 59 | httpsClient.print("GET " + String(wsdlAPI) + F(" HTTP/1.0\r\n") + 60 | F("Host: ") + String(wsdlHost) + F("\r\n") + 61 | F("Connection: close\r\n\r\n")); 62 | 63 | retryCounter = 0; 64 | while(!httpsClient.available()) { 65 | delay(100); 66 | retryCounter++; 67 | if (retryCounter > 100) { 68 | return UPD_TIMEOUT; // Timeout after 10s 69 | } 70 | } 71 | 72 | while (httpsClient.connected() || httpsClient.available()) { 73 | String line = httpsClient.readStringUntil('\n'); 74 | // check for success code... 75 | if (line.startsWith(F("HTTP"))) { 76 | if (line.indexOf(F("200 OK")) == -1) { 77 | httpsClient.stop(); 78 | if (line.indexOf(F("401")) > 0) { 79 | return UPD_UNAUTHORISED; 80 | } else if (line.indexOf(F("500")) > 0) { 81 | return UPD_DATA_ERROR; 82 | } else { 83 | return UPD_HTTP_ERROR; 84 | } 85 | } 86 | } 87 | if (line == F("\r")) { 88 | // Headers received 89 | break; 90 | } 91 | } 92 | 93 | char c; 94 | unsigned long dataSendTimeout = millis() + 8000UL; 95 | loadingWDSL = true; 96 | xmlStreamingParser parser; 97 | parser.setListener(this); 98 | parser.reset(); 99 | grandParentTagName = ""; 100 | parentTagName = ""; 101 | tagName = ""; 102 | tagLevel = 0; 103 | 104 | while((httpsClient.available() || httpsClient.connected()) && (millis() < dataSendTimeout)) { 105 | while (httpsClient.available()) { 106 | c = httpsClient.read(); 107 | parser.parse(c); 108 | } 109 | } 110 | 111 | httpsClient.stop(); 112 | loadingWDSL = false; 113 | 114 | if (soapURL.startsWith(F("https://"))) { 115 | int delim = soapURL.indexOf(F("/"),8); 116 | if (delim>0) { 117 | soapURL.substring(8,delim).toCharArray(soapHost,sizeof(soapHost)); 118 | soapURL.substring(delim).toCharArray(soapAPI,sizeof(soapAPI)); 119 | return UPD_SUCCESS; 120 | } 121 | } 122 | return UPD_DATA_ERROR; 123 | } 124 | 125 | // 126 | // Function to remove HTML tags from a character array 127 | // 128 | void raildataXmlClient::removeHtmlTags(char* input) { 129 | bool inTag = false; 130 | char* output = input; // Output pointer 131 | 132 | for (char* p = input; *p; ++p) { 133 | if (*p == '<') { 134 | inTag = true; 135 | } else if (*p == '>') { 136 | inTag = false; 137 | } else if (!inTag) { 138 | *output++ = *p; // Copy non-tag characters 139 | } 140 | } 141 | 142 | *output = '\0'; // Null-terminate the output 143 | } 144 | 145 | // 146 | // Function to replace occurrences of a word or phrase in a character array 147 | // 148 | void raildataXmlClient::replaceWord(char* input, const char* target, const char* replacement) { 149 | // Find the first occurrence of the target word 150 | char* pos = strstr(input, target); 151 | while (pos) { 152 | // Calculate the length of the target word 153 | size_t targetLen = strlen(target); 154 | // Calculate the length difference between target and replacement 155 | int diff = strlen(replacement) - targetLen; 156 | 157 | // Shift the remaining characters to accommodate the replacement 158 | memmove(pos + strlen(replacement), pos + targetLen, strlen(pos + targetLen) + 1); 159 | 160 | // Copy the replacement word into the position 161 | memcpy(pos, replacement, strlen(replacement)); 162 | 163 | // Find the next occurrence of the target word 164 | pos = strstr(pos + strlen(replacement), target); 165 | } 166 | } 167 | 168 | // 169 | // Function to prune messages from the point at which a word or phrase is found 170 | // 171 | void raildataXmlClient::pruneFromPhrase(char* input, const char* target) { 172 | // Find the first occurance of the target word or phrase 173 | char* pos = strstr(input,target); 174 | // If found, prune from here 175 | if (pos) input[pos - input] = '\0'; 176 | } 177 | 178 | // 179 | // Function to ensure there's one and only one fullstop at the end of messages. 180 | // 181 | void raildataXmlClient::fixFullStop(char *input) { 182 | if (input[0]) { 183 | while (input[0] && (input[strlen(input)-1] == '.' || input[strlen(input)-1] == ' ')) input[strlen(input)-1] = '\0'; // Remove all trailing full stops 184 | if (strlen(input) < MAXMESSAGESIZE-1) strcat(input,"."); // Add a single fullstop 185 | } 186 | } 187 | 188 | // 189 | // Updates the Departure Board data from the SOAP API 190 | // 191 | int raildataXmlClient::updateDepartures(rdStation *station, stnMessages *messages, const char *crsCode, const char *customToken, int numRows, bool includeBusServices, const char *callingCrsCode) { 192 | 193 | unsigned long perfTimer=millis(); 194 | bool bChunked = false; 195 | lastErrorMessage = ""; 196 | 197 | // Reset the counters 198 | xStation.numServices=0; 199 | xMessages.numMessages=0; 200 | xStation.platformAvailable=false; 201 | addedStopLocation = false; 202 | strcpy(xStation.location,""); 203 | 204 | for (int i=0;i=30) { 235 | lastErrorMessage = F("Timed out, no response from connect"); // No response within 3s 236 | return UPD_NO_RESPONSE; 237 | } 238 | 239 | int reqRows = MAXBOARDSERVICES; 240 | if (callingCrsCode[0]) reqRows = 10; // Request maximum services if we're filtering 241 | String data = F(""); 242 | data += String(customToken) + F("") + String(reqRows) + F(""); 243 | data += String(crsCode) + F(""); 244 | 245 | httpsClient.print("POST " + String(soapAPI) + F(" HTTP/1.1\r\n") + 246 | F("Host: ") + String(soapHost) + F("\r\n") + 247 | F("Content-Type: text/xml;charset=UTF-8\r\n") + 248 | F("Connection: close\r\n") + 249 | F("Content-Length: ") + String(data.length()) + F("\r\n\r\n") + 250 | data + F("\r\n\r\n")); 251 | 252 | Xcb(1,0); // progress callback 253 | unsigned long ticker = millis()+800; 254 | retryCounter = 0; 255 | while(!httpsClient.available()) { 256 | delay(100); 257 | retryCounter++; 258 | if (retryCounter >= 30) { 259 | lastErrorMessage = F("Timed out (GET)"); 260 | return UPD_TIMEOUT; // No response within 3s 261 | } 262 | } 263 | 264 | unsigned long dataSendTimeout = millis() + 1000UL; 265 | while((httpsClient.available() || httpsClient.connected()) && (millis() < dataSendTimeout)) { 266 | String line = httpsClient.readStringUntil('\n'); 267 | // check for success code... 268 | if (line.startsWith(F("HTTP"))) { 269 | if (line.indexOf(F("200 OK")) == -1) { 270 | httpsClient.stop(); 271 | if (line.indexOf(F("401")) > 0) { 272 | lastErrorMessage = line; 273 | return UPD_UNAUTHORISED; 274 | } else if (line.indexOf(F("500")) > 0) { 275 | lastErrorMessage = line; 276 | return UPD_DATA_ERROR; 277 | } else { 278 | lastErrorMessage = line; 279 | return UPD_HTTP_ERROR; 280 | } 281 | } 282 | } else if (line.startsWith(F("Transfer-Encoding:")) && line.indexOf(F("chunked")) >= 0) bChunked=true; 283 | if (line == F("\r")) { 284 | // Headers received 285 | break; 286 | } 287 | yield(); 288 | } 289 | 290 | xmlStreamingParser parser; 291 | parser.setListener(this); 292 | parser.reset(); 293 | grandParentTagName = ""; 294 | parentTagName = ""; 295 | tagName = ""; 296 | tagLevel = 0; 297 | loadingWDSL=false; 298 | long dataReceived = 0; 299 | if (callingCrsCode[0]) { 300 | strcpy(filterCrs,callingCrsCode); 301 | filter=true; 302 | } else { 303 | strcpy(filterCrs,""); 304 | filter=false; 305 | } 306 | keepRoute=false; 307 | 308 | char c; 309 | dataSendTimeout = millis() + 12000UL; 310 | perfTimer=millis(); // Reset the data load timer 311 | while((httpsClient.available() || httpsClient.connected()) && (millis() < dataSendTimeout)) { 312 | while (httpsClient.available()) { 313 | c = httpsClient.read(); 314 | parser.parse(c); 315 | dataReceived++; 316 | if (millis()>ticker) { 317 | Xcb(2,xStation.numServices); // Callback progress 318 | ticker = millis()+800; 319 | } 320 | // yield(); 321 | } 322 | if (millis()>ticker) { 323 | Xcb(2,id); // Callback with progress 324 | ticker = millis()+800; 325 | } 326 | delay(50); 327 | } 328 | 329 | httpsClient.stop(); 330 | if (bChunked) lastErrorMessage = "WARNING: Chunked response! "; 331 | if (millis() >= dataSendTimeout) { 332 | lastErrorMessage += F("Timed out during data receive operation - "); 333 | lastErrorMessage += String(dataReceived) + F(" bytes received"); 334 | return UPD_TIMEOUT; 335 | } 336 | 337 | if (!xStation.location[0]) { 338 | // We didn't get a location back so probably failed 339 | lastErrorMessage += F("Data incomplete - no location in response"); 340 | return UPD_DATA_ERROR; 341 | } 342 | if (filter && !keepRoute && xStation.numServices) xStation.numServices--; // Last route added needs filtering out 343 | 344 | sanitiseData(); 345 | if (includeBusServices) { 346 | // Look for any included bus services, and sort if found 347 | for (int i=0;inumMessages != xMessages.numMessages || station->numServices != xStation.numServices || station->platformAvailable != xStation.platformAvailable || strcmp(station->location,xStation.location)) noUpdate=false; 368 | else { 369 | for (int i=0;imessages[i],xMessages.messages[i])) { 371 | noUpdate=false; 372 | break; 373 | } 374 | } 375 | if (noUpdate) { 376 | for (int i=0;iservice[i].sTime, xStation.service[i].sTime) || strcmp(station->service[i].destination, xStation.service[i].destination) || strcmp(station->service[i].via, xStation.service[i].via) || strcmp(station->service[i].etd, xStation.service[i].etd) || strcmp(station->service[i].platform, xStation.service[i].platform)) { 378 | noUpdate=false; 379 | break; 380 | } 381 | if (station->service[i].isCancelled != xStation.service[i].isCancelled || station->service[i].isDelayed != xStation.service[i].isDelayed || station->service[i].trainLength != xStation.service[i].trainLength || station->service[i].classesAvailable != xStation.service[i].classesAvailable || station->service[i].serviceType != xStation.service[i].serviceType) { 382 | noUpdate=false; 383 | break; 384 | } 385 | } 386 | } 387 | } 388 | } else { 389 | firstDataLoad=false; 390 | noUpdate=false; 391 | } 392 | 393 | if (!noUpdate) { 394 | // copy everything back to the caller's structure 395 | messages->numMessages = xMessages.numMessages; 396 | station->numServices = xStation.numServices; 397 | strcpy(station->location,xStation.location); 398 | station->platformAvailable = xStation.platformAvailable; 399 | for (int i=0;imessages[i],xMessages.messages[i]); 400 | for (int i=0;iservice[i].sTime, xStation.service[i].sTime); 402 | strcpy(station->service[i].destination, xStation.service[i].destination); 403 | strcpy(station->service[i].via, xStation.service[i].via); 404 | strcpy(station->service[i].etd, xStation.service[i].etd); 405 | strcpy(station->service[i].platform, xStation.service[i].platform); 406 | station->service[i].isCancelled = xStation.service[i].isCancelled; 407 | station->service[i].isDelayed = xStation.service[i].isDelayed; 408 | station->service[i].trainLength = xStation.service[i].trainLength; 409 | station->service[i].classesAvailable = xStation.service[i].classesAvailable; 410 | strcpy(station->service[i].opco, xStation.service[i].opco); 411 | station->service[i].serviceType = xStation.service[i].serviceType; 412 | } 413 | if (xStation.numServices) { 414 | strcpy(station->calling,xStation.service[0].calling); 415 | strcpy(station->origin,xStation.service[0].origin); 416 | strcpy(station->serviceMessage,xStation.service[0].serviceMessage); 417 | } 418 | } 419 | 420 | Xcb(3,xStation.numServices); 421 | if (noUpdate) { 422 | lastErrorMessage += "Success (No Changes) - data [" + String(dataReceived) + F("] load took ") + String(millis()-perfTimer) + F("ms"); 423 | return UPD_NO_CHANGE; 424 | } else { 425 | lastErrorMessage += "Success - data [" + String(dataReceived) + F("] took ") + String(millis()-perfTimer) + F("ms"); 426 | return UPD_SUCCESS; 427 | } 428 | } 429 | 430 | String raildataXmlClient::getLastError() { 431 | return lastErrorMessage; 432 | } 433 | 434 | void raildataXmlClient::deleteService(int x) { 435 | 436 | if (x==xStation.numServices-1) { 437 | // it's the last one so just reduce the count by one 438 | xStation.numServices--; 439 | return; 440 | } 441 | // shuffle the other services down 442 | for (int i=x;i"); 487 | replaceWord(xMessages.messages[i],"

",""); 488 | replaceWord(xMessages.messages[i],"

",""); 489 | replaceWord(xMessages.messages[i],"
"," "); 490 | 491 | removeHtmlTags(xMessages.messages[i]); 492 | replaceWord(xMessages.messages[i],"&","&"); 493 | replaceWord(xMessages.messages[i],""","\""); 494 | // Remove unwanted text at the end of service messages... 495 | pruneFromPhrase(xMessages.messages[i]," More details "); 496 | pruneFromPhrase(xMessages.messages[i]," Latest information "); 497 | pruneFromPhrase(xMessages.messages[i]," Further information "); 498 | 499 | fixFullStop(xMessages.messages[i]); 500 | } 501 | } 502 | 503 | void raildataXmlClient::startTag(const char *tag) 504 | { 505 | tagLevel++; 506 | grandParentTagName = parentTagName; 507 | parentTagName = tagName; 508 | tagName = String(tag); 509 | tagPath = grandParentTagName + "/" + parentTagName + "/" + tagName; 510 | } 511 | 512 | void raildataXmlClient::endTag(const char *tag) 513 | { 514 | tagLevel--; 515 | tagName = parentTagName; 516 | parentTagName=grandParentTagName; 517 | grandParentTagName="??"; 518 | } 519 | 520 | void raildataXmlClient::parameter(const char *param) 521 | { 522 | } 523 | 524 | void raildataXmlClient::value(const char *value) 525 | { 526 | if (loadingWDSL) return; 527 | 528 | if (tagLevel<6 || tagLevel==9 || tagLevel>11) return; 529 | 530 | if (tagLevel == 11 && tagPath.endsWith(F("callingPoint/lt8:locationName"))) { 531 | if ((strlen(xStation.service[id].calling) + strlen(value) + 13) < sizeof(xStation.service[0].calling)) { 532 | // Add the calling point, add a comma prefix if this isn't the first one 533 | if (xStation.service[id].calling[0]) strcat(xStation.service[id].calling,", "); 534 | strcat(xStation.service[id].calling,value); 535 | addedStopLocation = true; 536 | } 537 | return; 538 | } else if (filter && tagLevel == 11 && tagPath.endsWith(F("callingPoint/lt8:crs")) && addedStopLocation) { 539 | // check if we should keep this route? 540 | if (strcmp(filterCrs,value)==0) keepRoute = true; 541 | return; 542 | } else if (tagLevel == 11 && tagPath.endsWith(F("callingPoint/lt8:st")) && addedStopLocation) { 543 | // check there's still room to add the eta of the calling point 544 | if ((strlen(xStation.service[id].calling) + strlen(value) + 4) < sizeof(xStation.service[0].calling)) { 545 | strcat(xStation.service[id].calling," ("); 546 | strcat(xStation.service[id].calling,value); 547 | strcat(xStation.service[id].calling,")"); 548 | } 549 | addedStopLocation = false; 550 | return; 551 | } else if (tagLevel == 11 && tagName == F("lt7:coachClass")) { 552 | if (strcmp(value,"First")==0) xStation.service[id].classesAvailable = xStation.service[id].classesAvailable | 1; 553 | else if (strcmp(value,"Standard")==0) xStation.service[id].classesAvailable = xStation.service[id].classesAvailable | 2; 554 | coaches++; 555 | return; 556 | } else if (tagLevel == 8 && tagName == F("lt4:length")) { 557 | xStation.service[id].trainLength = String(value).toInt(); 558 | return; 559 | } else if (tagLevel == 8 && tagName == F("lt4:operator")) { 560 | strncpy(xStation.service[id].opco,value,sizeof(xStation.service[0].opco)-1); 561 | xStation.service[id].opco[sizeof(xStation.service[0].opco)-1] = '\0'; 562 | return; 563 | } else if (tagLevel == 10 && tagPath.startsWith(F("lt5:origin/lt4:location/lt4:loc"))) { 564 | strncpy(xStation.service[id].origin,value,sizeof(xStation.service[0].origin)-1); 565 | xStation.service[id].origin[sizeof(xStation.service[0].origin)-1] = '\0'; 566 | return; 567 | } else if (tagLevel == 8 && tagName == F("lt4:serviceType")) { 568 | if (strcmp(value,"train")==0) xStation.service[id].serviceType = TRAIN; 569 | else if (strcmp(value,"bus")==0) xStation.service[id].serviceType = BUS; 570 | return; 571 | } else if (tagLevel == 8 && tagName == F("lt4:std")) { 572 | // Starting a new service 573 | // If we're filtering on calling point, check if we need to keep the previous service (if there was one) 574 | if (filter && !keepRoute && id>=0) { 575 | // We don't want this service, so clear it 576 | strcpy(xStation.service[id].sTime,""); 577 | strcpy(xStation.service[id].destination,""); 578 | strcpy(xStation.service[id].via,""); 579 | strcpy(xStation.service[id].origin,""); 580 | strcpy(xStation.service[id].etd,""); 581 | strcpy(xStation.service[id].platform,""); 582 | strcpy(xStation.service[id].opco,""); 583 | strcpy(xStation.service[id].calling,""); 584 | strcpy(xStation.service[id].serviceMessage,""); 585 | xStation.service[id].trainLength=0; 586 | xStation.service[id].classesAvailable=0; 587 | xStation.service[id].serviceType=0; 588 | xStation.service[id].isCancelled=false; 589 | xStation.service[id].isDelayed=false; 590 | xStation.numServices--; 591 | id--; 592 | } 593 | keepRoute = false; // reset for next route 594 | if (id>=0) { 595 | if (xStation.service[id].trainLength == 0) xStation.service[id].trainLength = coaches; 596 | } 597 | coaches=0; 598 | if (id < MAXBOARDSERVICES-1) { 599 | id++; 600 | xStation.numServices++; 601 | } 602 | strncpy(xStation.service[id].sTime,value,sizeof(xStation.service[0].sTime)); 603 | xStation.service[id].sTime[sizeof(xStation.service[0].sTime)-1] = '\0'; 604 | return; 605 | } else if (tagLevel == 8 && tagName == F("lt4:etd")) { 606 | strncpy(xStation.service[id].etd,value,sizeof(xStation.service[0].etd)); 607 | xStation.service[id].etd[sizeof(xStation.service[0].etd)-1] = '\0'; 608 | return; 609 | } else if (tagLevel == 10 && tagPath.startsWith(F("lt5:destination/lt4:location/lt4:lo"))) { 610 | strncpy(xStation.service[id].destination,value,sizeof(xStation.service[0].destination)-1); 611 | xStation.service[id].destination[sizeof(xStation.service[0].destination)-1] = '\0'; 612 | return; 613 | } else if (tagLevel == 10 && tagPath == F("lt5:destination/lt4:location/lt4:via")) { 614 | strncpy(xStation.service[id].via,value,sizeof(xStation.service[0].via)-1); 615 | xStation.service[id].via[sizeof(xStation.service[0].via)-1] = '\0'; 616 | return; 617 | } else if (tagLevel == 8 && tagName == F("lt4:delayReason")) { 618 | strncpy(xStation.service[id].serviceMessage,value,sizeof(xStation.service[0].serviceMessage)-1); 619 | xStation.service[id].serviceMessage[sizeof(xStation.service[0].serviceMessage)-1] = '\0'; 620 | xStation.service[id].isDelayed = true; 621 | return; 622 | } else if (tagLevel == 8 && tagName == F("lt4:cancelReason")) { 623 | strncpy(xStation.service[id].serviceMessage,value,sizeof(xStation.service[0].serviceMessage)-1); 624 | xStation.service[id].serviceMessage[sizeof(xStation.service[0].serviceMessage)-1] = '\0'; 625 | xStation.service[id].isCancelled = true; 626 | return; 627 | } else if (tagLevel == 8 && tagName == (F("lt4:platform"))) { 628 | strncpy(xStation.service[id].platform,value,sizeof(xStation.service[0].platform)-1); 629 | xStation.service[id].platform[sizeof(xStation.service[0].platform)-1] = '\0'; 630 | return; 631 | } else if (tagLevel == 6 && tagName == F("lt4:locationName")) { 632 | strncpy(xStation.location,value,sizeof(xStation.location)-1); 633 | xStation.location[sizeof(xStation.location)-1] = '\0'; 634 | return; 635 | } else if (tagLevel == 6 && tagName == F("lt4:platformAvailable")) { 636 | if (strcmp(value,"true")==0) xStation.platformAvailable = true; 637 | return; 638 | } else if (tagPath.endsWith(F("nrccMessages/lt:message"))) { // tagLevel 7 639 | if (xMessages.numMessages < MAXBOARDMESSAGES) { 640 | xMessages.numMessages++; 641 | strncpy(xMessages.messages[xMessages.numMessages-1],value,sizeof(xMessages.messages[0])-1); 642 | xMessages.messages[xMessages.numMessages-1][sizeof(xMessages.messages[0])-1] = '\0'; 643 | } 644 | return; 645 | } 646 | } 647 | 648 | void raildataXmlClient::attribute(const char *attr) 649 | { 650 | if (loadingWDSL) { 651 | if (tagName == "soap:address") { 652 | String myURL = String(attr); 653 | if (myURL.startsWith("location=\"") && myURL.endsWith("\"")) { 654 | soapURL = myURL.substring(10,myURL.length()-1); 655 | } 656 | } 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /web/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Departures Board 15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 34 | 35 | 36 | 40 | 41 | 45 | 46 | 57 | 58 | 131 | 132 |
133 | 134 | 135 | Lowering the brightness will help prevent burn-in. 136 |
137 | 138 |
139 | 140 |
141 | 142 |
143 |
144 | 145 |
146 |
147 | 148 |
149 |
150 | 151 |
152 |
153 | 154 |
155 |
156 | 157 |
158 |
159 | 160 |
161 |
162 |
163 | 164 | 190 |
191 |
192 | 193 | 219 |
220 |
221 |
222 | 223 |
224 | 225 |
226 | 227 |
228 |
229 |
230 | 231 |
232 | 235 | 238 |
239 |
240 | 241 |
242 |
243 |
244 | 245 | 736 | 737 | -------------------------------------------------------------------------------- /include/webgui/keys.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const uint8_t keyshtm[] PROGMEM={ 4 | 0x3C, 0x21, 0x44, 0x4F, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20, 0x68, 0x74, 0x6D, 0x6C, 0x3E, 0x0D, 0x0A, 0x3C, 0x68, 0x74, 0x6D, 5 | 0x6C, 0x20, 0x6C, 0x61, 0x6E, 0x67, 0x3D, 0x22, 0x65, 0x6E, 0x22, 0x3E, 0x0D, 0x0A, 0x3C, 0x68, 0x65, 0x61, 0x64, 0x3E, 0x0D, 6 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x69, 0x6E, 0x6B, 0x20, 0x72, 0x65, 0x6C, 0x3D, 0x22, 0x73, 0x74, 0x79, 0x6C, 0x65, 7 | 0x73, 0x68, 0x65, 0x65, 0x74, 0x22, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 8 | 0x63, 0x64, 0x6E, 0x2E, 0x6A, 0x73, 0x64, 0x65, 0x6C, 0x69, 0x76, 0x72, 0x2E, 0x6E, 0x65, 0x74, 0x2F, 0x6E, 0x70, 0x6D, 0x2F, 9 | 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x40, 0x33, 0x2E, 0x34, 0x2E, 0x31, 0x2F, 0x64, 0x69, 0x73, 0x74, 0x2F, 10 | 0x63, 0x73, 0x73, 0x2F, 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x73, 0x73, 11 | 0x22, 0x20, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x22, 0x73, 0x68, 0x61, 0x33, 0x38, 0x34, 0x2D, 0x48, 12 | 0x53, 0x4D, 0x78, 0x63, 0x52, 0x54, 0x52, 0x78, 0x6E, 0x4E, 0x2B, 0x42, 0x64, 0x67, 0x30, 0x4A, 0x64, 0x62, 0x78, 0x59, 0x4B, 13 | 0x72, 0x54, 0x68, 0x65, 0x63, 0x4F, 0x4B, 0x75, 0x48, 0x35, 0x7A, 0x43, 0x59, 0x6F, 0x74, 0x6C, 0x53, 0x41, 0x63, 0x70, 0x31, 14 | 0x2B, 0x63, 0x38, 0x78, 0x6D, 0x79, 0x54, 0x65, 0x39, 0x47, 0x59, 0x67, 0x31, 0x6C, 0x39, 0x61, 0x36, 0x39, 0x70, 0x73, 0x75, 15 | 0x22, 0x20, 0x63, 0x72, 0x6F, 0x73, 0x73, 0x6F, 0x72, 0x69, 0x67, 0x69, 0x6E, 0x3D, 0x22, 0x61, 0x6E, 0x6F, 0x6E, 0x79, 0x6D, 16 | 0x6F, 0x75, 0x73, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x20, 0x73, 0x72, 17 | 0x63, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x61, 0x6A, 0x61, 0x78, 0x2E, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 18 | 0x65, 0x61, 0x70, 0x69, 0x73, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6A, 0x61, 0x78, 0x2F, 0x6C, 0x69, 0x62, 0x73, 0x2F, 0x6A, 19 | 0x71, 0x75, 0x65, 0x72, 0x79, 0x2F, 0x33, 0x2E, 0x37, 0x2E, 0x31, 0x2F, 0x6A, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2E, 0x6D, 0x69, 20 | 0x6E, 0x2E, 0x6A, 0x73, 0x22, 0x3E, 0x3C, 0x2F, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 21 | 0x3C, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x20, 0x73, 0x72, 0x63, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 22 | 0x63, 0x64, 0x6E, 0x2E, 0x6A, 0x73, 0x64, 0x65, 0x6C, 0x69, 0x76, 0x72, 0x2E, 0x6E, 0x65, 0x74, 0x2F, 0x6E, 0x70, 0x6D, 0x2F, 23 | 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x40, 0x33, 0x2E, 0x34, 0x2E, 0x31, 0x2F, 0x64, 0x69, 0x73, 0x74, 0x2F, 24 | 0x6A, 0x73, 0x2F, 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x6A, 0x73, 0x22, 0x20, 25 | 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x22, 0x73, 0x68, 0x61, 0x33, 0x38, 0x34, 0x2D, 0x61, 0x4A, 0x32, 26 | 0x31, 0x4F, 0x6A, 0x6C, 0x4D, 0x58, 0x4E, 0x4C, 0x35, 0x55, 0x79, 0x49, 0x6C, 0x2F, 0x58, 0x4E, 0x77, 0x54, 0x4D, 0x71, 0x76, 27 | 0x7A, 0x65, 0x52, 0x4D, 0x5A, 0x48, 0x32, 0x77, 0x38, 0x63, 0x35, 0x63, 0x52, 0x56, 0x70, 0x7A, 0x70, 0x55, 0x38, 0x59, 0x35, 28 | 0x62, 0x41, 0x70, 0x54, 0x70, 0x70, 0x53, 0x75, 0x55, 0x6B, 0x68, 0x5A, 0x58, 0x4E, 0x30, 0x56, 0x78, 0x48, 0x64, 0x22, 0x20, 29 | 0x63, 0x72, 0x6F, 0x73, 0x73, 0x6F, 0x72, 0x69, 0x67, 0x69, 0x6E, 0x3D, 0x22, 0x61, 0x6E, 0x6F, 0x6E, 0x79, 0x6D, 0x6F, 0x75, 30 | 0x73, 0x22, 0x3E, 0x3C, 0x2F, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6D, 0x65, 31 | 0x74, 0x61, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3D, 0x22, 0x55, 0x54, 0x46, 0x2D, 0x38, 0x22, 0x3E, 0x0D, 0x0A, 32 | 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6D, 0x65, 0x74, 0x61, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x3D, 0x22, 0x76, 0x69, 0x65, 0x77, 0x70, 33 | 0x6F, 0x72, 0x74, 0x22, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x3D, 0x22, 0x75, 0x73, 0x65, 0x72, 0x2D, 0x73, 0x63, 34 | 0x61, 0x6C, 0x61, 0x62, 0x6C, 0x65, 0x3D, 0x6E, 0x6F, 0x2C, 0x20, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x61, 0x6C, 0x2D, 0x73, 0x63, 35 | 0x61, 0x6C, 0x65, 0x3D, 0x31, 0x2C, 0x20, 0x6D, 0x61, 0x78, 0x69, 0x6D, 0x75, 0x6D, 0x2D, 0x73, 0x63, 0x61, 0x6C, 0x65, 0x3D, 36 | 0x31, 0x2C, 0x20, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x2D, 0x73, 0x63, 0x61, 0x6C, 0x65, 0x3D, 0x31, 0x2C, 0x20, 0x77, 37 | 0x69, 0x64, 0x74, 0x68, 0x3D, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2D, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x3E, 0x0D, 0x0A, 38 | 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6D, 0x65, 0x74, 0x61, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x3D, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x65, 39 | 0x2D, 0x6D, 0x6F, 0x62, 0x69, 0x6C, 0x65, 0x2D, 0x77, 0x65, 0x62, 0x2D, 0x61, 0x70, 0x70, 0x2D, 0x63, 0x61, 0x70, 0x61, 0x62, 40 | 0x6C, 0x65, 0x22, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x3D, 0x22, 0x79, 0x65, 0x73, 0x22, 0x20, 0x2F, 0x3E, 0x0D, 41 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6D, 0x65, 0x74, 0x61, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x3D, 0x22, 0x61, 0x70, 0x70, 0x6C, 42 | 0x65, 0x2D, 0x6D, 0x6F, 0x62, 0x69, 0x6C, 0x65, 0x2D, 0x77, 0x65, 0x62, 0x2D, 0x61, 0x70, 0x70, 0x2D, 0x73, 0x74, 0x61, 0x74, 43 | 0x75, 0x73, 0x2D, 0x62, 0x61, 0x72, 0x2D, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x22, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 44 | 0x3D, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x20, 0x2F, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 45 | 0x69, 0x6E, 0x6B, 0x20, 0x72, 0x65, 0x6C, 0x3D, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x65, 0x2D, 0x74, 0x6F, 0x75, 0x63, 0x68, 0x2D, 46 | 0x69, 0x63, 0x6F, 0x6E, 0x22, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 0x22, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6F, 0x6E, 0x2E, 0x73, 47 | 0x76, 0x67, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x69, 0x6E, 0x6B, 0x20, 0x72, 0x65, 0x6C, 0x3D, 0x22, 48 | 0x69, 0x63, 0x6F, 0x6E, 0x22, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 0x22, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6F, 0x6E, 0x2E, 0x73, 49 | 0x76, 0x67, 0x22, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3D, 0x22, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x73, 0x76, 0x67, 0x2B, 0x78, 50 | 0x6D, 0x6C, 0x22, 0x20, 0x2F, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x74, 0x69, 0x74, 0x6C, 0x65, 0x3E, 0x44, 0x65, 51 | 0x70, 0x61, 0x72, 0x74, 0x75, 0x72, 0x65, 0x73, 0x20, 0x42, 0x6F, 0x61, 0x72, 0x64, 0x3C, 0x2F, 0x74, 0x69, 0x74, 0x6C, 0x65, 52 | 0x3E, 0x0D, 0x0A, 0x3C, 0x2F, 0x68, 0x65, 0x61, 0x64, 0x3E, 0x0D, 0x0A, 0x3C, 0x62, 0x6F, 0x64, 0x79, 0x3E, 0x0D, 0x0A, 0x20, 53 | 0x20, 0x20, 0x20, 0x3C, 0x64, 0x69, 0x76, 0x20, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x3D, 0x22, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3A, 54 | 0x20, 0x6D, 0x69, 0x6E, 0x28, 0x39, 0x30, 0x25, 0x2C, 0x20, 0x35, 0x30, 0x30, 0x70, 0x78, 0x29, 0x3B, 0x20, 0x6D, 0x61, 0x72, 55 | 0x67, 0x69, 0x6E, 0x3A, 0x20, 0x30, 0x20, 0x61, 0x75, 0x74, 0x6F, 0x3B, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6E, 0x67, 0x3A, 56 | 0x20, 0x30, 0x20, 0x31, 0x30, 0x70, 0x78, 0x3B, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 57 | 0x66, 0x6F, 0x72, 0x6D, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6E, 0x67, 0x73, 58 | 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x65, 0x74, 59 | 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x65, 0x67, 0x65, 0x6E, 60 | 0x64, 0x20, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x3D, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2D, 0x61, 0x6C, 0x69, 0x67, 0x6E, 0x3A, 0x20, 61 | 0x63, 0x65, 0x6E, 0x74, 0x65, 0x72, 0x3B, 0x22, 0x3E, 0x44, 0x65, 0x70, 0x61, 0x72, 0x74, 0x75, 0x72, 0x65, 0x73, 0x20, 0x42, 62 | 0x6F, 0x61, 0x72, 0x64, 0x20, 0x53, 0x65, 0x74, 0x75, 0x70, 0x3C, 0x2F, 0x6C, 0x65, 0x67, 0x65, 0x6E, 0x64, 0x3E, 0x0D, 0x0A, 63 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x64, 0x69, 0x76, 0x20, 0x63, 0x6C, 0x61, 0x73, 64 | 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 65 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 66 | 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x2D, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x22, 0x20, 0x66, 0x6F, 0x72, 0x3D, 0x22, 67 | 0x6E, 0x72, 0x4B, 0x65, 0x79, 0x22, 0x3E, 0x4E, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x52, 0x61, 0x69, 0x6C, 0x20, 68 | 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x3C, 0x2F, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 69 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x69, 0x6E, 0x70, 0x75, 0x74, 0x20, 0x69, 0x64, 0x3D, 0x22, 70 | 0x6E, 0x72, 0x4B, 0x65, 0x79, 0x22, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3D, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x70, 0x6C, 71 | 0x61, 0x63, 0x65, 0x68, 0x6F, 0x6C, 0x64, 0x65, 0x72, 0x3D, 0x22, 0x45, 0x6E, 0x74, 0x65, 0x72, 0x20, 0x79, 0x6F, 0x75, 0x72, 72 | 0x20, 0x33, 0x36, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x20, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x20, 73 | 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x20, 0x69, 74 | 0x6E, 0x70, 0x75, 0x74, 0x2D, 0x6D, 0x64, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 75 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x73, 0x70, 0x61, 0x6E, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x68, 0x65, 76 | 0x6C, 0x70, 0x2D, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x3E, 0x59, 0x6F, 0x75, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x6E, 0x65, 77 | 0x65, 0x64, 0x20, 0x61, 0x20, 0x4E, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x52, 0x61, 0x69, 0x6C, 0x20, 0x45, 0x6E, 78 | 0x71, 0x75, 0x69, 0x72, 0x69, 0x65, 0x73, 0x20, 0x44, 0x61, 0x72, 0x77, 0x69, 0x6E, 0x20, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x57, 79 | 0x65, 0x62, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x2E, 0x20, 0x45, 0x6E, 0x74, 0x65, 80 | 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x20, 0x65, 0x78, 0x61, 0x63, 0x74, 0x6C, 0x79, 0x20, 0x61, 81 | 0x73, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x20, 0x28, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x6F, 82 | 0x72, 0x6D, 0x61, 0x74, 0x20, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x2D, 0x6E, 0x6E, 0x6E, 0x6E, 0x2D, 0x6E, 0x6E, 83 | 0x6E, 0x6E, 0x2D, 0x6E, 0x6E, 0x6E, 0x6E, 0x2D, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x29, 84 | 0x2E, 0x20, 0x59, 0x6F, 0x75, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x20, 0x66, 0x6F, 85 | 0x72, 0x20, 0x61, 0x20, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x20, 0x66, 0x72, 0x65, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x63, 0x68, 0x61, 86 | 0x72, 0x67, 0x65, 0x20, 0x3C, 0x61, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 87 | 0x72, 0x65, 0x61, 0x6C, 0x74, 0x69, 0x6D, 0x65, 0x2E, 0x6E, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x72, 0x61, 0x69, 0x6C, 88 | 0x2E, 0x63, 0x6F, 0x2E, 0x75, 0x6B, 0x2F, 0x4F, 0x70, 0x65, 0x6E, 0x4C, 0x44, 0x42, 0x57, 0x53, 0x52, 0x65, 0x67, 0x69, 0x73, 89 | 0x74, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x20, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x3D, 0x22, 0x5F, 0x62, 0x6C, 0x61, 90 | 0x6E, 0x6B, 0x22, 0x3E, 0x68, 0x65, 0x72, 0x65, 0x3C, 0x2F, 0x61, 0x3E, 0x2E, 0x3C, 0x2F, 0x73, 0x70, 0x61, 0x6E, 0x3E, 0x0D, 91 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x64, 0x69, 0x76, 0x3E, 0x0D, 0x0A, 92 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x64, 0x69, 0x76, 0x20, 0x63, 0x6C, 0x61, 0x73, 93 | 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 94 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x20, 0x63, 0x6C, 95 | 0x61, 0x73, 0x73, 0x3D, 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x2D, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x22, 0x20, 0x66, 96 | 0x6F, 0x72, 0x3D, 0x22, 0x6F, 0x77, 0x6D, 0x4B, 0x65, 0x79, 0x22, 0x3E, 0x4F, 0x70, 0x65, 0x6E, 0x57, 0x65, 0x61, 0x74, 0x68, 97 | 0x65, 0x72, 0x20, 0x4D, 0x61, 0x70, 0x20, 0x41, 0x50, 0x49, 0x20, 0x4B, 0x65, 0x79, 0x3C, 0x2F, 0x6C, 0x61, 0x62, 0x65, 0x6C, 98 | 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x69, 99 | 0x6E, 0x70, 0x75, 0x74, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x6F, 0x77, 0x6D, 0x4B, 0x65, 0x79, 0x22, 0x20, 0x74, 0x79, 0x70, 0x65, 100 | 0x3D, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x70, 0x6C, 0x61, 0x63, 0x65, 0x68, 0x6F, 0x6C, 0x64, 0x65, 0x72, 0x3D, 0x22, 101 | 0x4F, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x6C, 0x79, 0x2C, 0x20, 0x65, 0x6E, 0x74, 0x65, 0x72, 0x20, 0x79, 0x6F, 0x75, 102 | 0x72, 0x20, 0x41, 0x50, 0x49, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 103 | 0x6D, 0x2D, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x20, 0x69, 0x6E, 0x70, 0x75, 0x74, 0x2D, 0x6D, 0x64, 0x22, 0x3E, 0x0D, 104 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x73, 0x70, 0x61, 105 | 0x6E, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x68, 0x65, 0x6C, 0x70, 0x2D, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x3E, 106 | 0x45, 0x6E, 0x74, 0x65, 0x72, 0x20, 0x79, 0x6F, 0x75, 0x72, 0x20, 0x6F, 0x70, 0x65, 0x6E, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 107 | 0x72, 0x6D, 0x61, 0x70, 0x2E, 0x6F, 0x72, 0x67, 0x20, 0x61, 0x70, 0x69, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x69, 0x66, 0x20, 0x79, 108 | 0x6F, 0x75, 0x20, 0x77, 0x69, 0x73, 0x68, 0x20, 0x74, 0x6F, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x20, 0x73, 0x74, 109 | 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 110 | 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x20, 0x59, 0x6F, 0x75, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 111 | 0x72, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x66, 0x72, 0x65, 0x65, 0x20, 0x61, 0x70, 0x69, 0x20, 0x6B, 0x65, 0x79, 0x20, 112 | 0x3C, 0x61, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x68, 0x6F, 0x6D, 0x65, 113 | 0x2E, 0x6F, 0x70, 0x65, 0x6E, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x6D, 0x61, 0x70, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x75, 114 | 0x73, 0x65, 0x72, 0x73, 0x2F, 0x73, 0x69, 0x67, 0x6E, 0x5F, 0x75, 0x70, 0x22, 0x20, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x3D, 115 | 0x22, 0x5F, 0x62, 0x6C, 0x61, 0x6E, 0x6B, 0x22, 0x3E, 0x68, 0x65, 0x72, 0x65, 0x3C, 0x2F, 0x61, 0x3E, 0x2E, 0x3C, 0x2F, 0x73, 116 | 0x70, 0x61, 0x6E, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x64, 117 | 0x69, 0x76, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x64, 0x69, 0x76, 118 | 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x22, 0x3E, 0x0D, 119 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x61, 0x62, 120 | 0x65, 0x6C, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x2D, 0x6C, 0x61, 0x62, 121 | 0x65, 0x6C, 0x22, 0x20, 0x66, 0x6F, 0x72, 0x3D, 0x22, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x22, 0x3E, 0x54, 0x72, 0x61, 0x6E, 122 | 0x73, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x4C, 0x6F, 0x6E, 0x64, 0x6F, 0x6E, 0x20, 0x41, 0x50, 0x49, 0x20, 123 | 0x4B, 0x65, 0x79, 0x3C, 0x2F, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 124 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x69, 0x6E, 0x70, 0x75, 0x74, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x61, 0x70, 125 | 0x70, 0x4B, 0x65, 0x79, 0x22, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3D, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x70, 0x6C, 0x61, 126 | 0x63, 0x65, 0x68, 0x6F, 0x6C, 0x64, 0x65, 0x72, 0x3D, 0x22, 0x4F, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x6C, 0x79, 0x2C, 127 | 0x20, 0x65, 0x6E, 0x74, 0x65, 0x72, 0x20, 0x79, 0x6F, 0x75, 0x72, 0x20, 0x54, 0x66, 0x4C, 0x20, 0x41, 0x70, 0x70, 0x20, 0x6B, 128 | 0x65, 0x79, 0x22, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x63, 0x6F, 0x6E, 0x74, 0x72, 129 | 0x6F, 0x6C, 0x20, 0x69, 0x6E, 0x70, 0x75, 0x74, 0x2D, 0x6D, 0x64, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 130 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x73, 0x70, 0x61, 0x6E, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 131 | 0x3D, 0x22, 0x68, 0x65, 0x6C, 0x70, 0x2D, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x3E, 0x45, 0x6E, 0x74, 0x65, 0x72, 0x20, 0x79, 132 | 0x6F, 0x75, 0x72, 0x20, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x4C, 0x6F, 0x6E, 133 | 0x64, 0x6F, 0x6E, 0x20, 0x61, 0x70, 0x69, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x28, 0x61, 0x70, 0x70, 0x5F, 0x6B, 0x65, 0x79, 0x29, 134 | 0x20, 0x69, 0x66, 0x20, 0x79, 0x6F, 0x75, 0x20, 0x77, 0x69, 0x73, 0x68, 0x20, 0x74, 0x6F, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6C, 135 | 0x61, 0x79, 0x20, 0x4C, 0x6F, 0x6E, 0x64, 0x6F, 0x6E, 0x20, 0x55, 0x6E, 0x64, 0x65, 0x72, 0x67, 0x72, 0x6F, 0x75, 0x6E, 0x64, 136 | 0x20, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x72, 0x72, 0x69, 0x76, 0x61, 0x6C, 0x73, 0x2E, 0x20, 0x59, 0x6F, 137 | 0x75, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 138 | 0x66, 0x72, 0x65, 0x65, 0x20, 0x61, 0x70, 0x69, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x3C, 0x61, 0x20, 0x68, 0x72, 0x65, 0x66, 0x3D, 139 | 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x61, 0x70, 0x69, 0x2D, 0x70, 0x6F, 0x72, 0x74, 0x61, 0x6C, 0x2E, 0x74, 140 | 0x66, 0x6C, 0x2E, 0x67, 0x6F, 0x76, 0x2E, 0x75, 0x6B, 0x2F, 0x73, 0x69, 0x67, 0x6E, 0x75, 0x70, 0x22, 0x20, 0x74, 0x61, 0x72, 141 | 0x67, 0x65, 0x74, 0x3D, 0x22, 0x5F, 0x62, 0x6C, 0x61, 0x6E, 0x6B, 0x22, 0x3E, 0x68, 0x65, 0x72, 0x65, 0x3C, 0x2F, 0x61, 0x3E, 142 | 0x2E, 0x3C, 0x2F, 0x73, 0x70, 0x61, 0x6E, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 143 | 0x20, 0x3C, 0x2F, 0x64, 0x69, 0x76, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 144 | 0x3C, 0x64, 0x69, 0x76, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x2D, 0x67, 0x72, 0x6F, 0x75, 145 | 0x70, 0x22, 0x20, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x3D, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2D, 0x61, 0x6C, 0x69, 0x67, 0x6E, 0x3A, 146 | 0x20, 0x63, 0x65, 0x6E, 0x74, 0x65, 0x72, 0x3B, 0x22, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 147 | 0x20, 0x20, 0x20, 0x3C, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 0x22, 0x63, 0x6F, 0x6E, 0x74, 148 | 0x72, 0x6F, 0x6C, 0x2D, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x22, 0x20, 0x66, 0x6F, 0x72, 0x3D, 0x22, 0x73, 0x61, 0x76, 0x65, 0x22, 149 | 0x3E, 0x3C, 0x2F, 0x6C, 0x61, 0x62, 0x65, 0x6C, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 150 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x62, 0x75, 0x74, 0x74, 0x6F, 0x6E, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x73, 0x61, 0x76, 151 | 0x65, 0x22, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x3D, 0x22, 0x73, 0x61, 0x76, 0x65, 0x22, 0x20, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x3D, 152 | 0x22, 0x62, 0x74, 0x6E, 0x20, 0x62, 0x74, 0x6E, 0x2D, 0x70, 0x72, 0x69, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x20, 0x74, 0x79, 0x70, 153 | 0x65, 0x3D, 0x22, 0x62, 0x75, 0x74, 0x74, 0x6F, 0x6E, 0x22, 0x20, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x3E, 0x53, 154 | 0x61, 0x76, 0x65, 0x3C, 0x2F, 0x62, 0x75, 0x74, 0x74, 0x6F, 0x6E, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 155 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x64, 0x69, 0x76, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 156 | 0x3C, 0x2F, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x65, 0x74, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x66, 0x6F, 157 | 0x72, 0x6D, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x64, 0x69, 0x76, 0x3E, 0x0D, 0x0A, 0x0D, 0x0A, 0x20, 0x20, 158 | 0x20, 0x20, 0x3C, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3E, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 159 | 0x28, 0x64, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x29, 0x2E, 0x72, 0x65, 0x61, 0x64, 0x79, 0x28, 0x66, 0x75, 0x6E, 0x63, 160 | 0x74, 0x69, 0x6F, 0x6E, 0x28, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 161 | 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x6E, 0x72, 0x4B, 0x65, 0x79, 0x50, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x20, 0x3D, 162 | 0x20, 0x2F, 0x5E, 0x5B, 0x61, 0x2D, 0x66, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x38, 0x7D, 0x2D, 0x5B, 0x61, 0x2D, 0x66, 0x30, 0x2D, 163 | 0x39, 0x5D, 0x7B, 0x34, 0x7D, 0x2D, 0x5B, 0x61, 0x2D, 0x66, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x34, 0x7D, 0x2D, 0x5B, 0x61, 0x2D, 164 | 0x66, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x34, 0x7D, 0x2D, 0x5B, 0x61, 0x2D, 0x66, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x31, 0x32, 0x7D, 165 | 0x24, 0x2F, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x75, 166 | 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4E, 0x72, 0x4B, 0x65, 0x79, 0x28, 167 | 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 168 | 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x76, 0x61, 0x6C, 0x20, 0x3D, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6E, 0x72, 0x4B, 0x65, 0x79, 169 | 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 0x28, 0x29, 0x2E, 0x74, 0x72, 0x69, 0x6D, 0x28, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 170 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x69, 0x73, 171 | 0x56, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x3D, 0x20, 0x6E, 0x72, 0x4B, 0x65, 0x79, 0x50, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x2E, 172 | 0x74, 0x65, 0x73, 0x74, 0x28, 0x76, 0x61, 0x6C, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 173 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x73, 0x61, 0x76, 0x65, 0x27, 0x29, 0x2E, 0x70, 0x72, 0x6F, 174 | 0x70, 0x28, 0x27, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x27, 0x2C, 0x20, 0x21, 0x69, 0x73, 0x56, 0x61, 0x6C, 0x69, 175 | 0x64, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0D, 0x0A, 0x0D, 176 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2F, 0x2F, 0x20, 0x49, 0x6E, 0x69, 0x74, 0x69, 177 | 0x61, 0x6C, 0x20, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 178 | 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x73, 0x61, 0x76, 0x65, 0x27, 0x29, 0x2E, 0x70, 0x72, 0x6F, 0x70, 0x28, 0x27, 0x64, 179 | 0x69, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x27, 0x2C, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x20, 180 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2F, 0x2F, 0x20, 0x4C, 0x6F, 0x61, 0x64, 0x20, 0x73, 0x61, 181 | 0x76, 0x65, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x6B, 0x65, 0x79, 0x73, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 182 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x2E, 0x67, 0x65, 0x74, 0x4A, 0x53, 0x4F, 0x4E, 0x28, 0x27, 0x2F, 0x61, 0x70, 0x69, 0x6B, 183 | 0x65, 0x79, 0x73, 0x2E, 0x6A, 0x73, 0x6F, 0x6E, 0x27, 0x2C, 0x20, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 184 | 0x64, 0x61, 0x74, 0x61, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 185 | 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x6E, 0x72, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x29, 186 | 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 187 | 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6E, 0x72, 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 0x28, 0x64, 0x61, 188 | 0x74, 0x61, 0x2E, 0x6E, 0x72, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 189 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 190 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x6F, 0x77, 0x6D, 0x54, 0x6F, 191 | 0x6B, 0x65, 0x6E, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 192 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6F, 0x77, 0x6D, 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x76, 193 | 0x61, 0x6C, 0x28, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x6F, 0x77, 0x6D, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 194 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0D, 0x0A, 0x20, 0x20, 0x20, 195 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x64, 0x61, 0x74, 0x61, 196 | 0x2E, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 197 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 198 | 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 0x28, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x29, 0x3B, 0x0D, 199 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0D, 0x0A, 0x20, 200 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2F, 0x2F, 0x20, 0x56, 0x61, 0x6C, 201 | 0x69, 0x64, 0x61, 0x74, 0x65, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x6C, 0x6F, 0x61, 0x64, 0x69, 0x6E, 0x67, 0x0D, 0x0A, 202 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 203 | 0x61, 0x74, 0x65, 0x4E, 0x72, 0x4B, 0x65, 0x79, 0x28, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 204 | 0x20, 0x20, 0x20, 0x20, 0x7D, 0x29, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 205 | 0x20, 0x20, 0x2F, 0x2F, 0x20, 0x56, 0x61, 0x6C, 0x69, 0x64, 0x61, 0x74, 0x65, 0x20, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x70, 0x75, 206 | 0x74, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6E, 0x72, 207 | 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x6F, 0x6E, 0x28, 0x27, 0x69, 0x6E, 0x70, 0x75, 0x74, 0x27, 0x2C, 0x20, 0x66, 0x75, 0x6E, 208 | 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 209 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4E, 0x72, 0x4B, 0x65, 0x79, 0x28, 210 | 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x29, 0x3B, 0x0D, 0x0A, 211 | 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x27, 0x23, 0x73, 0x61, 0x76, 212 | 0x65, 0x27, 0x29, 0x2E, 0x63, 0x6C, 0x69, 0x63, 0x6B, 0x28, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x29, 213 | 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 214 | 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x6E, 0x72, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x20, 0x3D, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6E, 0x72, 215 | 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 0x28, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 216 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x6F, 0x77, 0x6D, 0x54, 0x6F, 0x6B, 217 | 0x65, 0x6E, 0x20, 0x3D, 0x20, 0x24, 0x28, 0x27, 0x23, 0x6F, 0x77, 0x6D, 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 218 | 0x28, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 219 | 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x20, 0x3D, 0x20, 0x24, 0x28, 0x27, 0x23, 0x61, 0x70, 220 | 0x70, 0x4B, 0x65, 0x79, 0x27, 0x29, 0x2E, 0x76, 0x61, 0x6C, 0x28, 0x29, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 221 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x20, 0x70, 0x61, 0x79, 222 | 0x6C, 0x6F, 0x61, 0x64, 0x20, 0x3D, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 223 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6E, 0x72, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x3A, 0x20, 0x6E, 0x72, 0x54, 224 | 0x6F, 0x6B, 0x65, 0x6E, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 225 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6F, 0x77, 0x6D, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x3A, 0x20, 0x6F, 0x77, 0x6D, 0x54, 0x6F, 226 | 0x6B, 0x65, 0x6E, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 227 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x3A, 0x20, 0x61, 0x70, 0x70, 0x4B, 0x65, 0x79, 0x0D, 0x0A, 228 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x3B, 0x0D, 0x0A, 0x0D, 229 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x2E, 0x61, 0x6A, 230 | 0x61, 0x78, 0x28, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 231 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x72, 0x6C, 0x3A, 0x20, 0x27, 0x2F, 0x73, 0x61, 0x76, 0x65, 0x6B, 0x65, 0x79, 0x73, 0x27, 232 | 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 233 | 0x20, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3A, 0x20, 0x27, 0x50, 0x4F, 0x53, 0x54, 0x27, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 234 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x65, 235 | 0x6E, 0x74, 0x54, 0x79, 0x70, 0x65, 0x3A, 0x20, 0x27, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 236 | 0x6A, 0x73, 0x6F, 0x6E, 0x27, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 237 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64, 0x61, 0x74, 0x61, 0x3A, 0x20, 0x4A, 0x53, 0x4F, 0x4E, 0x2E, 0x73, 0x74, 0x72, 238 | 0x69, 0x6E, 0x67, 0x69, 0x66, 0x79, 0x28, 0x70, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x29, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 239 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x75, 0x63, 0x63, 240 | 0x65, 0x73, 0x73, 0x3A, 0x20, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 241 | 0x73, 0x65, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 242 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x6C, 0x65, 0x72, 0x74, 0x28, 0x72, 0x65, 0x73, 0x70, 0x6F, 243 | 0x6E, 0x73, 0x65, 0x29, 0x3B, 0x20, 0x2F, 0x2F, 0x20, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x20, 0x73, 0x65, 0x72, 0x76, 244 | 0x65, 0x72, 0x27, 0x73, 0x20, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 245 | 0x20, 0x74, 0x65, 0x78, 0x74, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 246 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6C, 0x6F, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x72, 0x65, 247 | 0x70, 0x6C, 0x61, 0x63, 0x65, 0x28, 0x22, 0x2F, 0x22, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 248 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 249 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x3A, 250 | 0x20, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x78, 0x68, 0x72, 0x2C, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 251 | 0x73, 0x2C, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x29, 0x20, 0x7B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 252 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x6C, 0x65, 0x72, 0x74, 253 | 0x28, 0x78, 0x68, 0x72, 0x2E, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x54, 0x65, 0x78, 0x74, 0x29, 0x3B, 0x20, 0x2F, 254 | 0x2F, 0x20, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x27, 0x73, 0x20, 0x65, 0x72, 255 | 0x72, 0x6F, 0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x0D, 0x0A, 0x20, 0x20, 256 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0D, 0x0A, 257 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x29, 0x3B, 0x0D, 0x0A, 258 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 259 | 0x20, 0x20, 0x20, 0x20, 0x7D, 0x29, 0x3B, 0x0D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 260 | 0x3E, 0x0D, 0x0A, 0x3C, 0x2F, 0x62, 0x6F, 0x64, 0x79, 0x3E, 0x0D, 0x0A, 0x3C, 0x2F, 0x68, 0x74, 0x6D, 0x6C, 0x3E 261 | }; 262 | -------------------------------------------------------------------------------- /include/webgui/webgraphics.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const uint8_t nrelogo[] PROGMEM = { 4 | 0x52, 0x49, 0x46, 0x46, 0xFC, 0x06, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 0x0A, 0x00, 0x00, 0x00, 0x10, 5 | 0x00, 0x00, 0x00, 0xEF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x41, 0x4C, 0x50, 0x48, 0x54, 0x05, 0x00, 0x00, 0x11, 0x67, 0xA0, 0xA0, 6 | 0x6D, 0x1B, 0x16, 0x8F, 0x42, 0xFF, 0x22, 0x22, 0x8E, 0xF9, 0xEC, 0x4B, 0xFF, 0x00, 0xCA, 0xFB, 0xFF, 0xAF, 0x6D, 0x9B, 0x8F, 7 | 0xF4, 0xB3, 0x92, 0x18, 0x0E, 0x2A, 0x93, 0x0F, 0xAA, 0x0B, 0x23, 0x1D, 0x9C, 0xF2, 0xF3, 0x30, 0x8C, 0xE7, 0x83, 0xC7, 0x54, 8 | 0x06, 0x8D, 0x0A, 0x81, 0x2D, 0x6E, 0xD7, 0x31, 0x83, 0x0F, 0x1E, 0x93, 0x0E, 0x63, 0xD4, 0xC1, 0x1D, 0x6F, 0xF9, 0x1D, 0x14, 9 | 0x4B, 0x26, 0xE9, 0xF3, 0x5F, 0x09, 0xE2, 0xF4, 0xF1, 0xF0, 0x14, 0xD1, 0xFF, 0x09, 0xC0, 0x95, 0x68, 0x3C, 0x88, 0xB2, 0x89, 10 | 0x61, 0xAF, 0xEB, 0x02, 0xE5, 0xA5, 0xBA, 0x6A, 0x60, 0xB1, 0xB1, 0x14, 0x4A, 0xC9, 0xC8, 0xA0, 0x7C, 0x60, 0x17, 0x26, 0x8A, 11 | 0xC2, 0x50, 0x74, 0x75, 0x98, 0x35, 0xA4, 0x8D, 0xBE, 0xDC, 0x5F, 0xF0, 0xC7, 0x0B, 0x6D, 0xA9, 0xCC, 0x9A, 0x6D, 0xA9, 0x74, 12 | 0x9D, 0x3C, 0x1A, 0x8E, 0xFD, 0x5C, 0xC1, 0x1D, 0x19, 0x66, 0xEE, 0xCE, 0xEE, 0xA4, 0xD9, 0xDA, 0x60, 0xF6, 0x7F, 0x5C, 0x71, 13 | 0xA8, 0xA5, 0x39, 0xD7, 0x57, 0x5A, 0x5B, 0x2B, 0x5E, 0x0E, 0xCD, 0xDB, 0x3A, 0xB7, 0x17, 0xBA, 0x18, 0xE6, 0x9E, 0xF0, 0xFD, 14 | 0x11, 0x77, 0x59, 0xFD, 0xF5, 0xFE, 0x84, 0xFD, 0xED, 0xF5, 0x85, 0x86, 0xFD, 0xED, 0x97, 0x39, 0x77, 0x14, 0x2D, 0xE1, 0x8D, 15 | 0xEF, 0xAA, 0x0C, 0xB5, 0xB6, 0xF2, 0x43, 0x2B, 0xD3, 0x59, 0xC4, 0xE9, 0xA7, 0xDC, 0xD5, 0xA7, 0x1A, 0x79, 0x77, 0xD5, 0xA9, 16 | 0xB9, 0x31, 0x3F, 0x03, 0x5F, 0x09, 0xCA, 0x32, 0x37, 0xD4, 0xBA, 0x7F, 0x9A, 0xAE, 0xF3, 0x94, 0x21, 0xDD, 0x62, 0xA3, 0x5E, 17 | 0x77, 0xB3, 0x0D, 0xBB, 0xFE, 0xA9, 0x43, 0x05, 0xBD, 0xD9, 0x5D, 0x3A, 0xD5, 0x21, 0xB4, 0xFF, 0xD5, 0xEF, 0x5E, 0xDF, 0x16, 18 | 0xF3, 0xC9, 0x2B, 0xC2, 0xF8, 0x20, 0xA3, 0x6F, 0xAD, 0x66, 0xF0, 0xBE, 0x56, 0x15, 0x78, 0x5F, 0xD3, 0x6B, 0x77, 0x01, 0xD7, 19 | 0xBF, 0x22, 0x60, 0x63, 0xF8, 0x4E, 0x85, 0x24, 0xF9, 0xA8, 0x02, 0x18, 0x67, 0x30, 0xF8, 0x2F, 0x8A, 0xC3, 0xE7, 0x18, 0xE3, 20 | 0x6F, 0x03, 0x8C, 0xAB, 0x16, 0xE0, 0x5D, 0x31, 0x74, 0xF4, 0x7E, 0x02, 0x47, 0xF1, 0xD7, 0x7A, 0x99, 0xC9, 0xF5, 0xBF, 0x98, 21 | 0x20, 0x05, 0x47, 0xFF, 0x52, 0x26, 0x98, 0xB6, 0x12, 0x33, 0xBE, 0x19, 0x80, 0xBE, 0x59, 0x2C, 0xD0, 0xA6, 0x75, 0x91, 0x4D, 22 | 0x9B, 0x37, 0xAB, 0x69, 0xC6, 0x37, 0x47, 0x0E, 0x5E, 0x7F, 0xFD, 0xF5, 0xE2, 0x4A, 0xF0, 0xC8, 0x4B, 0x33, 0xAF, 0xC5, 0x31, 23 | 0xC6, 0x63, 0x16, 0xA8, 0xB2, 0x98, 0xC6, 0x50, 0x06, 0xD0, 0xA9, 0x03, 0x28, 0x93, 0x94, 0x3B, 0x12, 0x0C, 0xF8, 0x12, 0x80, 24 | 0x4E, 0x92, 0x85, 0x2B, 0x21, 0x20, 0xDF, 0x14, 0x98, 0x8A, 0x1B, 0x89, 0x3B, 0x34, 0xAF, 0xCE, 0xAE, 0x41, 0x3C, 0x95, 0x40, 25 | 0x6A, 0x31, 0x46, 0x30, 0xE6, 0x39, 0x31, 0x17, 0x6B, 0x35, 0x6D, 0x00, 0x7A, 0x8C, 0x9E, 0xC2, 0x20, 0x49, 0x67, 0x39, 0xA6, 26 | 0x62, 0xF2, 0x71, 0x54, 0x51, 0xE5, 0x9A, 0x34, 0xB4, 0x07, 0xF7, 0xD4, 0x5D, 0x3D, 0x16, 0x63, 0x8E, 0xB1, 0xD0, 0x8A, 0x09, 27 | 0x05, 0x06, 0xDA, 0x19, 0x89, 0xF8, 0xD9, 0xA4, 0x89, 0x08, 0xE5, 0x72, 0x1C, 0x0F, 0x49, 0x56, 0x12, 0x72, 0x69, 0x7E, 0xBE, 28 | 0x04, 0x74, 0x0F, 0x0D, 0x6E, 0x17, 0x26, 0x58, 0x89, 0x31, 0x98, 0xF3, 0xE2, 0x32, 0x83, 0xE9, 0x8D, 0x46, 0xDA, 0xF9, 0xA4, 29 | 0x4F, 0xB9, 0xA1, 0x54, 0x2A, 0x6D, 0x5A, 0x0A, 0xAC, 0x2D, 0x95, 0x3A, 0x4F, 0xC5, 0x48, 0x9E, 0x49, 0xE3, 0x3F, 0x05, 0xE8, 30 | 0xCA, 0xE0, 0x2A, 0x28, 0xF3, 0x50, 0x0C, 0x0C, 0x2C, 0xCC, 0xFC, 0x9B, 0x54, 0x91, 0xD2, 0xB7, 0x63, 0xEC, 0x2E, 0xD5, 0x88, 31 | 0xF1, 0xC4, 0x85, 0x6D, 0xD5, 0x9E, 0x7D, 0xB7, 0xD8, 0xBC, 0x0E, 0xFB, 0x3E, 0x78, 0x58, 0x35, 0xEE, 0x16, 0x8F, 0xDF, 0x07, 32 | 0xE0, 0xE4, 0xAB, 0xF7, 0xAA, 0x09, 0xBB, 0xF0, 0x16, 0x77, 0xA1, 0xFC, 0xC4, 0x85, 0xAD, 0xFA, 0x96, 0x4C, 0x82, 0x00, 0x70, 33 | 0xBB, 0x38, 0x79, 0x8F, 0x02, 0xEC, 0x9B, 0xB9, 0x47, 0x54, 0x97, 0x01, 0x77, 0x29, 0x9B, 0xD4, 0x3B, 0x31, 0xA3, 0x8D, 0x0B, 34 | 0x94, 0xAD, 0x0D, 0x30, 0x5E, 0xD9, 0xD0, 0xE2, 0xC5, 0x5A, 0xAD, 0xF6, 0xE1, 0x55, 0x30, 0x66, 0x6A, 0xB5, 0xC0, 0x89, 0x91, 35 | 0x0D, 0xE6, 0x22, 0x3D, 0x52, 0xCE, 0x91, 0xCC, 0xF6, 0xCF, 0xA0, 0x47, 0x1E, 0x32, 0xF8, 0x09, 0x39, 0x86, 0xB7, 0x49, 0x66, 36 | 0x13, 0xEC, 0x57, 0x43, 0x66, 0xF5, 0x3E, 0xE9, 0x94, 0x59, 0x48, 0x78, 0xD8, 0xB2, 0x94, 0x56, 0x9D, 0xBC, 0x0E, 0xFB, 0x42, 37 | 0xB2, 0xE8, 0x9A, 0x00, 0xD5, 0xEE, 0x68, 0xFB, 0x0F, 0x16, 0x5A, 0x39, 0xF4, 0x28, 0xD5, 0x1F, 0xE9, 0x78, 0x8C, 0xD6, 0x45, 38 | 0xB9, 0x47, 0x92, 0x32, 0x86, 0x3A, 0x9F, 0xDA, 0xC7, 0x7C, 0x83, 0x92, 0xCE, 0xF3, 0x81, 0xFC, 0x48, 0xEB, 0x1D, 0x3A, 0x46, 39 | 0x49, 0x8A, 0x90, 0x92, 0x15, 0x9D, 0x0C, 0x39, 0x96, 0x40, 0xF2, 0x12, 0x2E, 0x53, 0xD2, 0x36, 0x98, 0x4B, 0x20, 0x29, 0x9A, 40 | 0xDC, 0xF0, 0x85, 0x83, 0xF6, 0x53, 0x5B, 0x7E, 0x89, 0x09, 0xD4, 0xCE, 0x88, 0x57, 0xDF, 0x3C, 0xE2, 0x65, 0x3F, 0xB5, 0xD7, 41 | 0x77, 0x46, 0x83, 0xA5, 0x07, 0x63, 0x1E, 0x53, 0x8C, 0x2E, 0xA3, 0x4E, 0x9C, 0xDA, 0xE6, 0x5A, 0xE6, 0x3D, 0x2E, 0xBB, 0x2C, 42 | 0xE1, 0x3F, 0x05, 0xF4, 0x2A, 0x2E, 0x47, 0xDE, 0x62, 0x36, 0x90, 0x8B, 0x83, 0xA7, 0x9E, 0xE3, 0xCE, 0xFD, 0xA9, 0x76, 0x2A, 43 | 0xE8, 0xC8, 0xA5, 0x5E, 0xAA, 0x7B, 0xEF, 0xBE, 0x5B, 0x69, 0x56, 0xA0, 0x53, 0xA7, 0x86, 0x4F, 0xD3, 0xE4, 0x00, 0x2F, 0xEB, 44 | 0x67, 0xD1, 0x1A, 0x23, 0x30, 0xC7, 0x0D, 0xA5, 0x92, 0x02, 0x63, 0x4B, 0xA9, 0x54, 0x2A, 0x75, 0x92, 0x1A, 0xBC, 0x9F, 0xF9, 45 | 0xDE, 0x19, 0x7C, 0x2A, 0xD1, 0x8E, 0xEC, 0x6A, 0x39, 0xD0, 0x99, 0xEF, 0x1F, 0x42, 0xE7, 0x8C, 0x4B, 0x61, 0xA4, 0x70, 0x78, 46 | 0x08, 0x08, 0x2A, 0x70, 0x53, 0x09, 0x00, 0xCD, 0x22, 0x10, 0xEE, 0xB3, 0x81, 0x66, 0x9A, 0x6C, 0xA4, 0x2F, 0xC6, 0x03, 0xAD, 47 | 0x77, 0x1B, 0x9A, 0x54, 0x91, 0xD2, 0xAF, 0x27, 0xE8, 0x94, 0xCC, 0xF7, 0x4D, 0x34, 0x93, 0x3C, 0x1B, 0x08, 0x8B, 0x7D, 0x13, 48 | 0x9D, 0x43, 0xAE, 0x83, 0x34, 0x66, 0x87, 0x19, 0x9D, 0x45, 0xCC, 0xA5, 0xCA, 0x24, 0x1D, 0x3F, 0x04, 0xB8, 0xF3, 0x0A, 0x48, 49 | 0x1B, 0x13, 0x1C, 0x6D, 0xF0, 0x62, 0xAD, 0xB6, 0x1C, 0xE5, 0x0F, 0x2E, 0xD4, 0x6A, 0xB5, 0xE0, 0x4C, 0x02, 0x7C, 0xC6, 0xB4, 50 | 0x92, 0x5A, 0x0E, 0x74, 0xAE, 0x89, 0x99, 0x73, 0x50, 0x4D, 0xB1, 0xAB, 0xC9, 0x5D, 0x3A, 0x0B, 0xF0, 0xE7, 0x67, 0x02, 0xE1, 51 | 0x64, 0x05, 0xF0, 0x8B, 0x6E, 0x11, 0x08, 0xD3, 0x7C, 0xF7, 0x88, 0x00, 0xBE, 0x78, 0xAA, 0x4A, 0x92, 0x72, 0x39, 0x5E, 0x64, 52 | 0xD4, 0x4C, 0x7A, 0x8E, 0xCC, 0xF7, 0xEA, 0x6B, 0x03, 0x89, 0x76, 0x1D, 0xE8, 0x55, 0x5C, 0x6A, 0xCF, 0x31, 0x1B, 0x98, 0xE8, 53 | 0x9C, 0x71, 0xB9, 0xDC, 0x4F, 0x71, 0xC8, 0xA0, 0x44, 0xF8, 0xD4, 0x78, 0x98, 0x4A, 0x24, 0x55, 0xEB, 0xD0, 0xC3, 0x62, 0x73, 54 | 0x17, 0xC0, 0x14, 0x7D, 0x05, 0x51, 0xEA, 0x11, 0xCA, 0xE5, 0xD8, 0xD7, 0x27, 0x99, 0x4B, 0x42, 0xC8, 0xBC, 0xCF, 0x90, 0x52, 55 | 0xF1, 0x39, 0x93, 0xED, 0x1E, 0x9A, 0xA0, 0x13, 0x50, 0x44, 0x9E, 0x6A, 0x50, 0x92, 0xD9, 0x84, 0x0A, 0xDA, 0xCC, 0x76, 0x18, 56 | 0x32, 0xD5, 0x8D, 0xD7, 0x5F, 0xBF, 0x25, 0xC6, 0xE0, 0x86, 0x9F, 0x69, 0x3E, 0xE7, 0xA8, 0x9F, 0xA5, 0xF1, 0x57, 0xC3, 0x50, 57 | 0x33, 0x20, 0xBA, 0x11, 0xCA, 0xED, 0xA5, 0x97, 0x48, 0x8A, 0x88, 0x1F, 0xF1, 0x58, 0x6C, 0x90, 0xDF, 0x53, 0xBD, 0x4C, 0xE6, 58 | 0xFB, 0x67, 0xD0, 0x27, 0xCF, 0x80, 0x26, 0xBA, 0x75, 0x3D, 0xA4, 0x1D, 0xE6, 0x3B, 0x36, 0x80, 0x7D, 0x3C, 0x84, 0xE7, 0x68, 59 | 0x7F, 0x4A, 0x7E, 0x47, 0x83, 0x05, 0x8F, 0x11, 0x92, 0x94, 0xAE, 0x09, 0x50, 0xF4, 0xC8, 0xAF, 0x4C, 0x83, 0x94, 0x61, 0xA6, 60 | 0x3B, 0xEA, 0xE5, 0x00, 0x3F, 0x3F, 0xC1, 0x1F, 0x42, 0x95, 0xE1, 0x19, 0xCC, 0xC5, 0x90, 0x94, 0xA4, 0x8D, 0xE8, 0x09, 0x4B, 61 | 0x01, 0xAA, 0xD6, 0x72, 0xBC, 0xFF, 0xC8, 0x5A, 0x4B, 0xD1, 0xBF, 0x79, 0x43, 0x9C, 0xDA, 0x86, 0xEA, 0x77, 0x6F, 0x64, 0x70, 62 | 0x7A, 0x39, 0x4E, 0xDE, 0x88, 0xA9, 0x8F, 0xB4, 0x53, 0xDA, 0xC9, 0x1B, 0x01, 0x18, 0xD6, 0x46, 0xC0, 0x7A, 0x45, 0x79, 0xFF, 63 | 0x91, 0xB2, 0xA5, 0x5B, 0xDA, 0x89, 0x7B, 0x00, 0xC0, 0xB2, 0x2C, 0xEB, 0xC6, 0x63, 0x2B, 0x80, 0x19, 0xB5, 0xFA, 0xDD, 0xF6, 64 | 0x4F, 0x4D, 0x4C, 0x7D, 0x34, 0x32, 0x2D, 0xA6, 0xC5, 0x94, 0x06, 0x4C, 0x69, 0x78, 0x37, 0xBC, 0x05, 0x2F, 0xFE, 0xA0, 0xA1, 65 | 0x9C, 0x44, 0xB2, 0x18, 0x33, 0x64, 0x9B, 0x26, 0x06, 0xDE, 0x49, 0x23, 0xFE, 0x5A, 0x8E, 0xA5, 0xA8, 0xE0, 0xAF, 0x05, 0xED, 66 | 0x04, 0x29, 0x86, 0x92, 0xBB, 0x10, 0x46, 0x2F, 0x6E, 0x05, 0x86, 0xF2, 0xA7, 0xD7, 0x2D, 0x00, 0xCA, 0xBF, 0x91, 0x74, 0x96, 67 | 0x63, 0x38, 0xEB, 0xCA, 0x42, 0x00, 0x07, 0x4E, 0x6F, 0x55, 0xF1, 0xFF, 0x53, 0xFD, 0xE0, 0xDD, 0x96, 0x65, 0xDD, 0x73, 0xFD, 68 | 0xE2, 0xBF, 0x18, 0xE3, 0xE5, 0x3F, 0x19, 0x2F, 0xDF, 0x58, 0xFF, 0x57, 0x52, 0xED, 0x33, 0xF5, 0x8E, 0xBF, 0x90, 0x1E, 0xE7, 69 | 0xA9, 0xFD, 0x75, 0x70, 0xBE, 0xD9, 0xBF, 0x8E, 0x2F, 0xE6, 0x51, 0x57, 0xFE, 0x3A, 0xF0, 0x52, 0x90, 0xE6, 0x49, 0x81, 0xBF, 70 | 0xD2, 0xF2, 0xA9, 0x0F, 0xBF, 0x9F, 0x9D, 0x9D, 0xFD, 0xE1, 0xE3, 0x7B, 0xD7, 0xE3, 0xAF, 0x14, 0x56, 0x50, 0x38, 0x20, 0x82, 71 | 0x01, 0x00, 0x00, 0x30, 0x0D, 0x00, 0x9D, 0x01, 0x2A, 0xF0, 0x00, 0x31, 0x00, 0x3F, 0x71, 0xAE, 0xCA, 0x5C, 0xBB, 0xBF, 0xA8, 72 | 0xA4, 0xAA, 0xB5, 0x9B, 0x33, 0xF0, 0x2E, 0x09, 0x64, 0x6E, 0xDF, 0xFA, 0xDF, 0x77, 0x15, 0x99, 0xFE, 0x00, 0x72, 0x80, 0x3E, 73 | 0xD0, 0x2D, 0x72, 0xF4, 0xF5, 0x4C, 0x9D, 0xDC, 0xB9, 0xE9, 0x83, 0x70, 0x58, 0x0E, 0xAC, 0x0E, 0xD6, 0x78, 0x17, 0xE2, 0x8F, 74 | 0xC5, 0x7D, 0x99, 0x70, 0x2F, 0xC5, 0x18, 0xAF, 0x0B, 0x12, 0x1C, 0x60, 0xAB, 0xD7, 0x2F, 0xE1, 0x28, 0x08, 0x3D, 0x52, 0x5F, 75 | 0x2B, 0x65, 0x42, 0x2A, 0x42, 0x7E, 0xF5, 0xFA, 0xDD, 0xCF, 0xEF, 0x16, 0x8B, 0xEE, 0x4F, 0xF0, 0x90, 0xBD, 0x63, 0x2B, 0x6A, 76 | 0x46, 0x9A, 0x4E, 0x47, 0x1A, 0x48, 0x37, 0xEE, 0xA0, 0x8C, 0x2F, 0x00, 0x00, 0xFE, 0xEA, 0xFB, 0x97, 0xC4, 0x32, 0x73, 0x62, 77 | 0x1E, 0x63, 0xED, 0x86, 0xFD, 0x79, 0xA2, 0x37, 0x99, 0xA3, 0x2C, 0xAF, 0xF8, 0x44, 0x98, 0xC2, 0xDE, 0xE0, 0xD8, 0x31, 0x12, 78 | 0x2E, 0x87, 0x86, 0xEC, 0xCB, 0x4C, 0xA3, 0xE6, 0xC4, 0xED, 0x12, 0x7F, 0x24, 0x00, 0xE7, 0x52, 0xD8, 0x35, 0x52, 0xFC, 0xD5, 79 | 0x8E, 0x0F, 0xA7, 0xBD, 0xEA, 0xC9, 0x03, 0x51, 0x60, 0x13, 0x4D, 0xE5, 0xE1, 0x7E, 0x15, 0x8F, 0x22, 0xFD, 0x8B, 0x3B, 0x0D, 80 | 0xDD, 0x9C, 0x41, 0x86, 0x4F, 0xF5, 0x82, 0xD2, 0x98, 0xB3, 0xE8, 0x1D, 0x05, 0x60, 0x30, 0x54, 0x08, 0xF3, 0xCC, 0xDC, 0x4D, 81 | 0xF7, 0x6B, 0x43, 0xDE, 0x80, 0x7D, 0x6F, 0x8F, 0xC9, 0x4C, 0x47, 0x87, 0x3D, 0x8E, 0x20, 0xF8, 0xBC, 0x09, 0x17, 0x2A, 0xEA, 82 | 0xA3, 0x37, 0x6D, 0xCB, 0x19, 0x5E, 0x78, 0xE3, 0x51, 0xD8, 0x91, 0x30, 0xE5, 0x68, 0xE0, 0x55, 0x28, 0xE0, 0x24, 0xFA, 0x66, 83 | 0x62, 0xD7, 0xBD, 0x88, 0x56, 0x96, 0x7F, 0x9D, 0x6B, 0x11, 0x76, 0x9E, 0xDC, 0x5B, 0x4E, 0xEF, 0x79, 0x81, 0x7E, 0x47, 0x34, 84 | 0x50, 0x9D, 0x53, 0xB5, 0x2E, 0x5F, 0x9B, 0xE5, 0x2D, 0xDD, 0xF2, 0x97, 0xAE, 0x91, 0xFE, 0x40, 0x61, 0x7E, 0x1F, 0xFF, 0x8C, 85 | 0x04, 0x51, 0x20, 0x62, 0x4F, 0x90, 0x1A, 0x24, 0x37, 0x07, 0xC2, 0xD0, 0x4D, 0x6C, 0xFC, 0x78, 0x87, 0x70, 0x00, 0xA5, 0xCA, 86 | 0x70, 0x15, 0x9C, 0xD0, 0xC1, 0x61, 0x16, 0xEF, 0xA2, 0x1A, 0x5D, 0x90, 0x6A, 0xB7, 0xDE, 0xC9, 0xFF, 0xED, 0x79, 0xF9, 0x59, 87 | 0x35, 0x3E, 0xCD, 0xDF, 0x99, 0xF2, 0xCC, 0x3F, 0x79, 0x89, 0x02, 0xF6, 0x25, 0xC2, 0x42, 0x81, 0x1E, 0xCC, 0xCA, 0xEF, 0xD9, 88 | 0x36, 0x2E, 0xAC, 0x0D, 0x48, 0xF6, 0x88, 0xF7, 0xF9, 0x7B, 0xE5, 0xF9, 0x66, 0x81, 0x26, 0x90, 0x60, 0x03, 0x39, 0x49, 0x5C, 89 | 0xD2, 0x4B, 0x42, 0x03, 0x66, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00 90 | }; 91 | 92 | static const uint8_t tfllogo[] PROGMEM = { 93 | 0x52, 0x49, 0x46, 0x46, 0xE0, 0x06, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x20, 0xD4, 0x06, 0x00, 0x00, 0xB0, 94 | 0x20, 0x00, 0x9D, 0x01, 0x2A, 0xF0, 0x00, 0x31, 0x00, 0x3E, 0xDD, 0x64, 0xAB, 0x4F, 0xA8, 0xA5, 0xA4, 0x22, 0x2B, 0xD7, 0x49, 95 | 0x11, 0x10, 0x1B, 0x89, 0x62, 0x44, 0x77, 0x20, 0xCC, 0xC2, 0x74, 0x43, 0x3D, 0x08, 0xCD, 0xDB, 0x73, 0xD1, 0x06, 0xD9, 0x6F, 96 | 0x31, 0x5E, 0x77, 0x5E, 0x8E, 0xBC, 0xE6, 0x7A, 0x8A, 0x37, 0xA0, 0x7F, 0xC5, 0x60, 0x97, 0xF9, 0x63, 0xEF, 0xB2, 0x92, 0xFC, 97 | 0x3D, 0x9A, 0x69, 0xFA, 0x1F, 0x2A, 0xFB, 0xD5, 0xF5, 0x65, 0xE1, 0xC3, 0x79, 0x67, 0x3C, 0xFF, 0x33, 0xE8, 0x05, 0xDD, 0x3E, 98 | 0xF3, 0xDF, 0xEF, 0x7D, 0x0D, 0xFB, 0x01, 0xCB, 0x01, 0xE0, 0x99, 0x40, 0x4F, 0xCE, 0x9E, 0x87, 0x99, 0xD7, 0x7A, 0xBB, 0xD8, 99 | 0x2B, 0xF5, 0xF3, 0xAD, 0x39, 0x7E, 0x91, 0x40, 0x0F, 0x11, 0xE7, 0x4F, 0x2B, 0x5B, 0x4F, 0x1B, 0x31, 0x77, 0x16, 0x3B, 0xE6, 100 | 0xC0, 0x57, 0xFE, 0xF1, 0x3C, 0x3E, 0x7C, 0x95, 0xFB, 0xF1, 0x42, 0x0D, 0x90, 0x5C, 0x9E, 0x64, 0x20, 0x90, 0xD8, 0x83, 0x8F, 101 | 0x21, 0x77, 0x4D, 0x46, 0x99, 0x0C, 0x6F, 0x3D, 0xA6, 0x70, 0xF8, 0x5D, 0xC7, 0xEC, 0xAD, 0x8D, 0x6C, 0x1D, 0x50, 0x88, 0x80, 102 | 0xCB, 0x4E, 0x19, 0xEB, 0x88, 0xA8, 0x1C, 0xC1, 0x8A, 0x83, 0xCB, 0x60, 0x3C, 0xBE, 0x4A, 0x6C, 0x69, 0xDD, 0x71, 0x38, 0x50, 103 | 0xB5, 0xA0, 0x61, 0x93, 0xB0, 0xC6, 0x34, 0x8F, 0xA9, 0x07, 0xBB, 0x0B, 0xA0, 0xAC, 0x61, 0x98, 0x8A, 0x9F, 0x0D, 0x3C, 0xD2, 104 | 0xB3, 0x86, 0x11, 0x9E, 0x12, 0x2F, 0xCB, 0xDF, 0x88, 0x23, 0x41, 0x3F, 0xCA, 0x20, 0x02, 0x8F, 0x4F, 0x61, 0x38, 0x7D, 0xF5, 105 | 0xB6, 0xD1, 0x47, 0x4B, 0x55, 0x32, 0xCE, 0x7A, 0x6D, 0x7E, 0xE6, 0xD1, 0x2E, 0x9C, 0xED, 0xD4, 0x64, 0xEC, 0xDE, 0x2A, 0x8D, 106 | 0x17, 0xB2, 0x0A, 0xC5, 0xC1, 0x2A, 0x06, 0x20, 0xCF, 0xDC, 0xDA, 0x5D, 0x4D, 0x0E, 0xA4, 0x73, 0xE8, 0x00, 0xFE, 0xE0, 0x81, 107 | 0x9D, 0x6C, 0x69, 0xC0, 0xCB, 0x9B, 0x9E, 0x13, 0xC5, 0x55, 0x5E, 0x09, 0x92, 0xF7, 0xEA, 0x38, 0x1C, 0x16, 0xFF, 0x59, 0x6C, 108 | 0x32, 0x54, 0x21, 0x43, 0x00, 0x46, 0x22, 0x12, 0x17, 0xA3, 0x88, 0xB7, 0x9A, 0xBD, 0x55, 0xE8, 0x21, 0xE5, 0x31, 0xD5, 0xBB, 109 | 0xAB, 0x75, 0x57, 0x50, 0x56, 0xCE, 0x4A, 0xBD, 0x08, 0x1D, 0xB8, 0x3E, 0xF0, 0xC5, 0x73, 0x47, 0xD7, 0x53, 0xF8, 0x16, 0xFA, 110 | 0xAD, 0x6E, 0xDB, 0x58, 0xA4, 0xAC, 0x8C, 0x83, 0x8E, 0x2C, 0x25, 0x6C, 0x8F, 0x87, 0xDA, 0xB6, 0x8D, 0x7D, 0xE4, 0x6B, 0x74, 111 | 0xB4, 0x39, 0x04, 0xF9, 0x57, 0x1C, 0xBF, 0x73, 0x4F, 0x68, 0xB5, 0x3B, 0xED, 0x0C, 0xA7, 0x3A, 0x38, 0x78, 0x5E, 0xDA, 0xD8, 112 | 0x06, 0x3A, 0x57, 0xF5, 0xF9, 0x8F, 0x0F, 0xC4, 0x69, 0x57, 0x4E, 0xBE, 0x42, 0x2F, 0x56, 0x51, 0xF9, 0x6C, 0x2D, 0xFF, 0x96, 113 | 0x4B, 0x69, 0x57, 0x76, 0xA7, 0xF7, 0xA4, 0xF9, 0xF3, 0xF8, 0x43, 0x69, 0x47, 0x43, 0xC0, 0x4D, 0x21, 0xF2, 0x16, 0x48, 0x0E, 114 | 0xA2, 0xAB, 0x03, 0x9B, 0x6E, 0xF5, 0x92, 0xF3, 0xE5, 0x12, 0x66, 0xB8, 0x8B, 0x67, 0xFD, 0xAA, 0x48, 0x99, 0x6B, 0x29, 0xEF, 115 | 0x6E, 0x99, 0xFA, 0x90, 0x93, 0xC8, 0x53, 0x46, 0x8A, 0x38, 0x3B, 0x87, 0x1A, 0xC4, 0x58, 0xAE, 0xB3, 0xA1, 0x7F, 0xE4, 0xE5, 116 | 0x47, 0x4F, 0xE4, 0x62, 0xE0, 0x7D, 0xE2, 0x5D, 0x60, 0x55, 0x79, 0x57, 0x95, 0xB1, 0xA2, 0x95, 0x8D, 0xBB, 0x07, 0xD6, 0xDE, 117 | 0x7B, 0x9C, 0x06, 0x18, 0x4A, 0x78, 0x43, 0xE9, 0xC8, 0xD5, 0xBE, 0xB9, 0xDC, 0xBD, 0x9D, 0x3C, 0x54, 0xF0, 0x54, 0x5F, 0x56, 118 | 0x2F, 0xE7, 0xE4, 0xB0, 0xE2, 0x3C, 0x64, 0x32, 0x38, 0xDE, 0xBF, 0x7D, 0xB3, 0x90, 0xCD, 0x21, 0x0A, 0x7E, 0xB7, 0x9D, 0xEB, 119 | 0x02, 0xA8, 0x27, 0x7B, 0x46, 0x46, 0xD7, 0xED, 0xED, 0x9B, 0xC6, 0x6B, 0x2F, 0x89, 0xF8, 0x99, 0x86, 0x8C, 0x82, 0x74, 0xE6, 120 | 0x47, 0x46, 0xB7, 0x95, 0xAF, 0x1A, 0x34, 0xC6, 0xBF, 0x6D, 0x25, 0x11, 0x4D, 0x42, 0xFD, 0x8A, 0xA5, 0x13, 0xFB, 0x7E, 0x05, 121 | 0xD5, 0xF7, 0x46, 0xD1, 0xC6, 0xCF, 0xBA, 0xBF, 0xD9, 0x7A, 0x78, 0x3E, 0x4B, 0x83, 0x05, 0xA1, 0xCC, 0xB2, 0xC7, 0x30, 0x02, 122 | 0x08, 0xED, 0x38, 0xC1, 0x0F, 0x1C, 0x19, 0x84, 0x7E, 0x7E, 0xFF, 0x8D, 0x62, 0x4C, 0x0A, 0x25, 0xF7, 0xDC, 0x21, 0x8A, 0xCD, 123 | 0xEC, 0x2B, 0x11, 0x7C, 0x9B, 0x83, 0x68, 0x34, 0x6E, 0x79, 0xD1, 0xD0, 0xFA, 0xC6, 0xD3, 0xD4, 0x1E, 0x96, 0xC6, 0x04, 0xFB, 124 | 0x57, 0xE3, 0x36, 0x6A, 0x13, 0x7D, 0x43, 0xEF, 0x1B, 0x3B, 0x64, 0x1E, 0x88, 0xF3, 0xC5, 0xB1, 0xF5, 0x48, 0x9C, 0x33, 0x92, 125 | 0x08, 0xBB, 0x51, 0x99, 0x80, 0xAE, 0x2A, 0x85, 0xCE, 0x78, 0xB3, 0xBE, 0x32, 0x5E, 0x00, 0x77, 0x57, 0xC5, 0xDE, 0x12, 0x09, 126 | 0x17, 0x6D, 0xF0, 0x26, 0xBE, 0x78, 0x62, 0x38, 0xED, 0xE2, 0xAD, 0xD0, 0xEE, 0x2F, 0x38, 0x7C, 0xCA, 0x77, 0x4A, 0xBC, 0x65, 127 | 0x2B, 0xB9, 0x5C, 0x22, 0x27, 0x71, 0xDA, 0xB2, 0x95, 0x34, 0xC0, 0xDD, 0xE4, 0x98, 0xA2, 0xEB, 0x8B, 0x53, 0xF0, 0x4A, 0x11, 128 | 0xBB, 0x1D, 0x6B, 0x4C, 0x3B, 0xA8, 0xF6, 0x39, 0x74, 0x80, 0x26, 0x1D, 0x57, 0xC3, 0x83, 0x07, 0x13, 0x4D, 0x4B, 0x28, 0x80, 129 | 0x30, 0x30, 0xDA, 0xD1, 0xE7, 0x2B, 0xAC, 0x41, 0x33, 0xEA, 0x46, 0xA6, 0x3D, 0xEF, 0xCE, 0xB9, 0x40, 0x73, 0x3F, 0x40, 0xC8, 130 | 0x4C, 0x44, 0xE8, 0x02, 0x7E, 0x7B, 0xEA, 0xA1, 0xF3, 0xAA, 0xE5, 0xDA, 0x3A, 0xBB, 0x9C, 0xE0, 0xD3, 0x22, 0x1F, 0x4E, 0x6C, 131 | 0xA1, 0x5B, 0x79, 0xF8, 0x98, 0x5F, 0xEB, 0xB9, 0x37, 0xD3, 0x3A, 0x74, 0xDA, 0x16, 0xE5, 0x38, 0x5B, 0x78, 0xA3, 0xE6, 0x95, 132 | 0xD4, 0xBF, 0x7F, 0x5C, 0x50, 0xF4, 0xCD, 0x23, 0x87, 0x54, 0xB7, 0x44, 0x82, 0x38, 0x6B, 0xCC, 0xEF, 0x80, 0x05, 0xF9, 0x7E, 133 | 0xEF, 0xCB, 0x1C, 0x2E, 0xBC, 0x6E, 0xD7, 0x99, 0x85, 0x03, 0xA6, 0xE6, 0x75, 0xF8, 0x33, 0xB7, 0x6A, 0xED, 0xFB, 0x70, 0x72, 134 | 0xB8, 0xF5, 0x57, 0xFC, 0xE0, 0x75, 0xAD, 0xB3, 0xB4, 0xBF, 0xD5, 0x96, 0x8D, 0x11, 0xA5, 0x33, 0xA5, 0xA8, 0xE4, 0xCB, 0x0B, 135 | 0x87, 0xF7, 0xCD, 0x7E, 0xA8, 0x52, 0x25, 0xDB, 0x27, 0x56, 0x17, 0x44, 0x6B, 0x99, 0x30, 0x12, 0x49, 0x25, 0x1D, 0xA0, 0x11, 136 | 0x9E, 0x9C, 0x11, 0xE5, 0x39, 0xD1, 0x6B, 0xBD, 0xD1, 0x5C, 0x1A, 0x43, 0x48, 0x68, 0x7E, 0x84, 0x7D, 0xE1, 0xD0, 0x41, 0xAD, 137 | 0xD1, 0x04, 0xBF, 0x91, 0x35, 0x4B, 0xE0, 0x6B, 0x6F, 0x38, 0x6F, 0x4C, 0x1F, 0x23, 0xF8, 0xFA, 0x2F, 0x6C, 0x1F, 0xEA, 0x33, 138 | 0x1D, 0xB0, 0xD0, 0x2A, 0x32, 0x43, 0xDE, 0x59, 0xB3, 0xE4, 0x2B, 0x4D, 0x99, 0x1B, 0xF9, 0xAB, 0xB0, 0xA8, 0x59, 0x00, 0x0E, 139 | 0xA0, 0x0B, 0x75, 0xA7, 0xD9, 0xDF, 0x56, 0x48, 0xED, 0x90, 0xCC, 0x6C, 0x85, 0xC9, 0xDF, 0xFA, 0x73, 0x37, 0x1E, 0xB9, 0x43, 140 | 0xE8, 0x93, 0xEB, 0x04, 0x37, 0x92, 0xF3, 0x49, 0x29, 0x6B, 0x2B, 0xD7, 0xED, 0xD7, 0x3D, 0xB4, 0xD3, 0x60, 0x64, 0x4F, 0x17, 141 | 0x5E, 0x5E, 0xB8, 0x98, 0x5C, 0x22, 0x22, 0xCE, 0xBD, 0x93, 0x01, 0x47, 0xCB, 0x13, 0x4E, 0x40, 0xF9, 0x8C, 0x97, 0x31, 0xF7, 142 | 0x8E, 0xA9, 0x57, 0xC7, 0x55, 0x32, 0x26, 0xE7, 0xCA, 0xE9, 0xE5, 0x46, 0xFA, 0xEA, 0x11, 0xF9, 0xC3, 0xB1, 0xA9, 0x57, 0x94, 143 | 0x8D, 0xDB, 0x96, 0xF3, 0x13, 0x65, 0x7B, 0xBB, 0xF8, 0x53, 0x13, 0x7D, 0xEA, 0xF9, 0xB0, 0x39, 0xEB, 0x7C, 0x41, 0xBA, 0x3C, 144 | 0x02, 0xC9, 0xA0, 0x86, 0xDC, 0xD7, 0xA6, 0x59, 0x55, 0x8B, 0xAB, 0xA7, 0x28, 0xAA, 0xE6, 0x4A, 0x51, 0xAF, 0x9A, 0xC7, 0xD9, 145 | 0x89, 0xB2, 0x5B, 0xBB, 0x92, 0x91, 0x31, 0x63, 0x91, 0x1E, 0xA3, 0x70, 0x3F, 0xA7, 0xCB, 0x9F, 0x59, 0xBE, 0x2E, 0x54, 0xE0, 146 | 0x88, 0x8F, 0x31, 0x46, 0xF8, 0x55, 0xCC, 0xF2, 0xA3, 0x7D, 0xB1, 0x8B, 0x1B, 0xBF, 0xBB, 0x18, 0x01, 0x2C, 0xEC, 0x58, 0x33, 147 | 0xA2, 0x2D, 0xA4, 0xE6, 0xE7, 0x2F, 0xA7, 0xE0, 0xFD, 0x33, 0x11, 0x64, 0xEF, 0x07, 0x80, 0xBC, 0x7E, 0x04, 0x0C, 0xB6, 0xE1, 148 | 0x06, 0x04, 0x73, 0xC6, 0x89, 0xFB, 0xF1, 0x50, 0xC3, 0x4A, 0xA1, 0x36, 0x1D, 0xEF, 0x27, 0x67, 0xBB, 0xAF, 0xD0, 0x61, 0x1A, 149 | 0x71, 0x04, 0x28, 0x3C, 0xBF, 0x82, 0x7B, 0x05, 0x84, 0xDC, 0xF6, 0xF2, 0x4F, 0x9C, 0x6E, 0xC3, 0x01, 0x17, 0xB3, 0xEE, 0x3F, 150 | 0xB5, 0x33, 0xFF, 0x65, 0x2A, 0x4C, 0xB7, 0xE4, 0x7C, 0x42, 0x98, 0x0E, 0x61, 0x62, 0xD5, 0xAF, 0xBC, 0xFE, 0x0D, 0x24, 0x20, 151 | 0x80, 0x78, 0x85, 0x7B, 0xAA, 0x4A, 0x23, 0xE9, 0x47, 0x3D, 0x3A, 0x0C, 0x98, 0xA7, 0xBD, 0x37, 0xE4, 0x19, 0xB5, 0x3E, 0xB3, 152 | 0x65, 0x00, 0x20, 0xBA, 0xF2, 0x38, 0x0D, 0x3C, 0xEC, 0xFA, 0x6E, 0x39, 0x0F, 0x60, 0xE1, 0xE5, 0x05, 0x08, 0x92, 0xA7, 0x69, 153 | 0x97, 0x02, 0x9E, 0x12, 0xB9, 0xD9, 0x2D, 0x3F, 0xFF, 0x4E, 0x63, 0xBC, 0xDB, 0xD7, 0x42, 0x58, 0x05, 0x96, 0x15, 0x53, 0xC9, 154 | 0xE3, 0x33, 0x2C, 0x62, 0x71, 0xBA, 0x33, 0xDD, 0xC9, 0xF6, 0x33, 0xD9, 0x6D, 0x7E, 0x32, 0xD5, 0xB4, 0x7B, 0x47, 0xD1, 0xBC, 155 | 0x08, 0x72, 0xFA, 0xE5, 0x76, 0x72, 0xB1, 0x15, 0xB8, 0xA5, 0x87, 0x8F, 0x8D, 0x7F, 0x36, 0xCC, 0x10, 0x75, 0xB6, 0xF9, 0x9E, 156 | 0x47, 0x48, 0xA9, 0x43, 0xDB, 0xB5, 0xC2, 0x78, 0xD8, 0xA2, 0x3B, 0xB6, 0x89, 0x4D, 0xCC, 0x92, 0x9E, 0x3D, 0x12, 0x81, 0x12, 157 | 0x94, 0xA8, 0x42, 0x87, 0x73, 0x20, 0x3F, 0xA2, 0x8A, 0xAC, 0x09, 0x9C, 0xF3, 0xA4, 0x95, 0x5B, 0xC8, 0x98, 0x93, 0x8B, 0xDE, 158 | 0x67, 0x00, 0x8A, 0x34, 0x77, 0xEE, 0x7E, 0xA1, 0x11, 0x25, 0xF8, 0x90, 0x41, 0x9F, 0x9F, 0x2B, 0x95, 0x98, 0x05, 0x2E, 0x7D, 159 | 0x4A, 0x66, 0x49, 0x67, 0x21, 0x16, 0x9A, 0x29, 0x79, 0x09, 0x6C, 0xB4, 0x51, 0x4B, 0xC3, 0xE3, 0xCB, 0xCD, 0x06, 0x4E, 0xFE, 160 | 0xA3, 0xFE, 0x7B, 0x01, 0x50, 0x16, 0xB0, 0xC0, 0x98, 0xD3, 0x76, 0x16, 0x1B, 0x96, 0xDA, 0xC3, 0x11, 0xDE, 0xFC, 0x98, 0x10, 161 | 0xA9, 0x83, 0xD0, 0x64, 0x5D, 0x18, 0xF3, 0xE8, 0xE6, 0x82, 0xBD, 0xDE, 0x57, 0xE9, 0xAC, 0x4C, 0x83, 0x73, 0xA8, 0xF3, 0xCE, 162 | 0xCE, 0x59, 0x88, 0x38, 0xA8, 0xD1, 0x8E, 0x4B, 0xCB, 0xFA, 0xF8, 0xCB, 0xC4, 0xB1, 0x16, 0xB4, 0x28, 0x18, 0xBB, 0xA8, 0xE4, 163 | 0xCB, 0xE9, 0x2C, 0xE1, 0x10, 0x06, 0x49, 0xF5, 0x41, 0xEE, 0xBF, 0x2C, 0x93, 0xFD, 0x13, 0x7F, 0xC2, 0x6F, 0xB9, 0xB4, 0x20, 164 | 0x3A, 0x04, 0xF1, 0x1A, 0x29, 0x14, 0x59, 0x45, 0x67, 0xB4, 0xA9, 0xD4, 0xE5, 0x5B, 0x4B, 0x75, 0x2A, 0x54, 0x0D, 0x56, 0xF5, 165 | 0x94, 0x62, 0xC7, 0xCC, 0x9D, 0x5F, 0xA7, 0x8C, 0x9D, 0x85, 0x9C, 0xC2, 0x3E, 0x88, 0x87, 0xA2, 0xBB, 0xB5, 0x45, 0xFD, 0x82, 166 | 0xA9, 0x63, 0x4C, 0x93, 0x15, 0x9D, 0xF6, 0x96, 0x2B, 0x4E, 0xF6, 0x3E, 0x54, 0x2C, 0x51, 0x99, 0xE3, 0xBE, 0x87, 0x74, 0x3D, 167 | 0x6D, 0xC4, 0x51, 0x04, 0xC4, 0x69, 0xBE, 0x1F, 0x44, 0xE5, 0x92, 0xAF, 0x13, 0x51, 0x81, 0x84, 0xFC, 0x96, 0xA3, 0xF4, 0x7D, 168 | 0x0F, 0xFE, 0x76, 0xA3, 0x67, 0x40, 0xFE, 0x62, 0x12, 0x8C, 0x14, 0x67, 0x9D, 0x43, 0xE5, 0x18, 0x2D, 0x85, 0xB9, 0x21, 0x1A, 169 | 0x92, 0x51, 0xEE, 0xC2, 0x6E, 0xB1, 0xC5, 0xD6, 0xE6, 0x28, 0xF9, 0xCD, 0x89, 0x73, 0x8B, 0xBD, 0x08, 0x2F, 0x80, 0x36, 0x42, 170 | 0xA2, 0x73, 0x59, 0xF0, 0x44, 0xA2, 0x7A, 0x68, 0xEE, 0xF0, 0xE2, 0x25, 0x2E, 0xA5, 0xBC, 0x39, 0x2A, 0xC0, 0xF0, 0x08, 0xFA, 171 | 0xAE, 0xBD, 0x4D, 0xC9, 0x85, 0x33, 0x8D, 0x70, 0x29, 0x0D, 0x31, 0x92, 0x28, 0xBE, 0xA1, 0x8D, 0xBE, 0x06, 0x9A, 0xD4, 0x68, 172 | 0xFC, 0x6D, 0xBD, 0x0D, 0x61, 0xDF, 0x62, 0x5F, 0xEE, 0x9C, 0xEA, 0x1A, 0x27, 0x52, 0xF1, 0xF2, 0x63, 0xD3, 0xDD, 0xAD, 0x52, 173 | 0xB7, 0xA0, 0x6E, 0x41, 0x36, 0x80, 0x0C, 0x05, 0x64, 0xB0, 0x97, 0xF3, 0x79, 0x3C, 0x28, 0xEB, 0x14, 0x62, 0x89, 0xD9, 0xE0, 174 | 0xF5, 0x4B, 0xD1, 0x44, 0xF3, 0x27, 0x5A, 0xB3, 0xB2, 0xBF, 0x4F, 0x42, 0x7B, 0x15, 0x8C, 0xA6, 0x45, 0x9E, 0x54, 0xF5, 0x18, 175 | 0xDE, 0x84, 0xE4, 0x87, 0xBC, 0x79, 0x5A, 0x16, 0xE9, 0x2C, 0xCE, 0x09, 0xF1, 0xF3, 0xC8, 0x08, 0x45, 0x40, 0xB0, 0xC5, 0x15, 176 | 0x00, 0x06, 0x8F, 0x86, 0x80, 0xB0, 0x03, 0xA4, 0xD3, 0xD0, 0x5B, 0x48, 0x96, 0xAD, 0xC2, 0x7D, 0x17, 0x0A, 0x80, 0x00, 0x00, 177 | 0x00, 0x00, 0x00, 0x00 178 | }; 179 | 180 | static const uint8_t nricon[] PROGMEM = { 181 | 0x52, 0x49, 0x46, 0x46, 0xC0, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 0x0A, 0x00, 0x00, 0x00, 0x10, 182 | 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x41, 0x4C, 0x50, 0x48, 0x5E, 0x00, 0x00, 0x00, 0x11, 0x47, 0xA0, 0xA6, 183 | 0x6D, 0x23, 0x38, 0x59, 0x12, 0xDD, 0x7A, 0xFF, 0xDD, 0x74, 0x11, 0x11, 0xD0, 0xC5, 0x28, 0x9F, 0x60, 0x1C, 0xDB, 0x56, 0x93, 184 | 0x07, 0x2B, 0x96, 0xC6, 0x06, 0x48, 0x6B, 0x5B, 0xC8, 0xB6, 0x60, 0x21, 0xB1, 0x01, 0xE4, 0xF5, 0x0D, 0x6A, 0x0B, 0x11, 0xFD, 185 | 0x9F, 0x00, 0x14, 0x81, 0xAB, 0xC0, 0x6B, 0xB1, 0x73, 0x15, 0x19, 0xAD, 0xB5, 0x7E, 0xB8, 0x69, 0x2D, 0x01, 0xBE, 0xAB, 0x3F, 186 | 0x00, 0x8A, 0x40, 0x8F, 0x7C, 0x11, 0xE8, 0x91, 0xE5, 0xBB, 0xFA, 0xA3, 0xF5, 0x10, 0xE9, 0xB5, 0xD6, 0x12, 0x69, 0x1F, 0xEF, 187 | 0x16, 0xEF, 0x5D, 0xA4, 0x4B, 0x00, 0x56, 0x50, 0x38, 0x20, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x02, 0x00, 0x9D, 0x01, 0x2A, 0x17, 188 | 0x00, 0x0C, 0x00, 0x3F, 0x75, 0xA4, 0xC7, 0x59, 0xB5, 0x2C, 0x28, 0x23, 0xB0, 0x08, 0x02, 0xA0, 0x2E, 0x89, 0x6C, 0x00, 0xB0, 189 | 0xEC, 0x30, 0x88, 0x40, 0x00, 0xFE, 0xD0, 0xC9, 0xE4, 0xEB, 0x7B, 0xE2, 0xED, 0xA0, 0x7D, 0x65, 0x90, 0xAF, 0x1C, 0x6F, 0xC8, 190 | 0x83, 0x9C, 0xE4, 0xD1, 0x7A, 0x1F, 0x1E, 0x4C, 0x80, 0x00, 0x00 191 | }; 192 | 193 | static const uint8_t tubeicon[] PROGMEM = { 194 | 0x52, 0x49, 0x46, 0x46, 0xD4, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 0x0A, 0x00, 0x00, 0x00, 0x10, 195 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x41, 0x4C, 0x50, 0x48, 0x67, 0x00, 0x00, 0x00, 0x11, 0x60, 0x5A, 0x5B, 196 | 0x7B, 0x93, 0x9C, 0x83, 0xC2, 0x31, 0x4F, 0xEF, 0x30, 0x42, 0xB2, 0x02, 0x23, 0xA0, 0xAA, 0xAC, 0x73, 0xE0, 0x22, 0x39, 0x28, 197 | 0x2C, 0xAA, 0xAE, 0x80, 0x04, 0xC5, 0x00, 0x49, 0xE8, 0x23, 0x44, 0x84, 0x22, 0xB7, 0x6D, 0x1B, 0xE9, 0x94, 0xD9, 0xF1, 0x0C, 198 | 0x70, 0x69, 0xE6, 0x75, 0xE5, 0x81, 0x37, 0x79, 0x4C, 0x2E, 0x1B, 0xE1, 0x85, 0x9E, 0x1C, 0x29, 0x0A, 0x37, 0xB2, 0x70, 0x0F, 199 | 0x7C, 0xD2, 0x82, 0x4B, 0x4C, 0x82, 0x1B, 0x22, 0x5A, 0x49, 0x66, 0x40, 0x04, 0xAD, 0x98, 0xEC, 0x48, 0x3B, 0xE9, 0xC8, 0xBB, 200 | 0xC3, 0x1F, 0xFE, 0x04, 0xDF, 0x2D, 0x7F, 0x16, 0xFD, 0xFE, 0x99, 0x1F, 0xBF, 0x07, 0x00, 0x00, 0x56, 0x50, 0x38, 0x20, 0x46, 201 | 0x00, 0x00, 0x00, 0xF0, 0x02, 0x00, 0x9D, 0x01, 0x2A, 0x12, 0x00, 0x0C, 0x00, 0x3E, 0xB1, 0x46, 0x9D, 0x49, 0xA7, 0x23, 0xA2, 202 | 0xA1, 0x30, 0x08, 0x00, 0xE0, 0x16, 0x09, 0x6C, 0x00, 0xA8, 0xF4, 0x2D, 0x72, 0x00, 0x00, 0xFE, 0xEF, 0xBD, 0x57, 0xEA, 0xCD, 203 | 0xB8, 0xAF, 0xEF, 0x5A, 0x5B, 0x0B, 0xE1, 0x9C, 0x0B, 0xDA, 0x69, 0xE3, 0xF3, 0x71, 0xC0, 0xF6, 0xAB, 0xC3, 0xB4, 0x73, 0x37, 204 | 0x3A, 0xD7, 0xBD, 0x0B, 0xFE, 0x62, 0x18, 0x4E, 0x00, 0x00 205 | }; 206 | 207 | static const uint8_t favicon[] PROGMEM = { 208 | 0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x31, 0x2E, 0x30, 0x22, 0x20, 0x65, 209 | 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x3D, 0x22, 0x55, 0x54, 0x46, 0x2D, 0x38, 0x22, 0x20, 0x73, 0x74, 0x61, 0x6E, 0x64, 210 | 0x61, 0x6C, 0x6F, 0x6E, 0x65, 0x3D, 0x22, 0x6E, 0x6F, 0x22, 0x3F, 0x3E, 0x0A, 0x3C, 0x73, 0x76, 0x67, 0x0A, 0x20, 0x20, 0x20, 211 | 0x77, 0x69, 0x64, 0x74, 0x68, 0x3D, 0x22, 0x32, 0x33, 0x2E, 0x38, 0x35, 0x32, 0x30, 0x35, 0x35, 0x22, 0x0A, 0x20, 0x20, 0x20, 212 | 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3D, 0x22, 0x32, 0x33, 0x2E, 0x38, 0x35, 0x31, 0x33, 0x39, 0x37, 0x22, 0x0A, 0x20, 0x20, 213 | 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x31, 0x2E, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x69, 0x64, 0x3D, 214 | 0x22, 0x73, 0x76, 0x67, 0x31, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3D, 0x22, 0x68, 0x74, 0x74, 215 | 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x77, 0x33, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x32, 0x30, 0x30, 0x30, 0x2F, 0x73, 216 | 0x76, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x73, 0x76, 0x67, 0x3D, 0x22, 0x68, 0x74, 0x74, 217 | 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x77, 0x33, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x32, 0x30, 0x30, 0x30, 0x2F, 0x73, 218 | 0x76, 0x67, 0x22, 0x3E, 0x0A, 0x20, 0x20, 0x3C, 0x64, 0x65, 0x66, 0x73, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x64, 0x3D, 219 | 0x22, 0x64, 0x65, 0x66, 0x73, 0x31, 0x35, 0x22, 0x20, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x3C, 0x70, 0x61, 0x74, 0x68, 0x0A, 0x20, 220 | 0x20, 0x20, 0x20, 0x20, 0x64, 0x3D, 0x22, 0x6D, 0x20, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x37, 0x2C, 0x30, 0x20, 221 | 0x63, 0x20, 0x36, 0x2E, 0x35, 0x38, 0x36, 0x35, 0x32, 0x31, 0x2C, 0x30, 0x20, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 222 | 0x38, 0x2C, 0x35, 0x2E, 0x33, 0x33, 0x38, 0x31, 0x39, 0x31, 0x38, 0x20, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x38, 223 | 0x2C, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x35, 0x33, 0x37, 0x20, 0x30, 0x2C, 0x36, 0x2E, 0x35, 0x38, 0x36, 0x35, 0x32, 0x20, 0x2D, 224 | 0x35, 0x2E, 0x33, 0x33, 0x39, 0x35, 0x30, 0x37, 0x2C, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x37, 0x20, 0x2D, 0x31, 225 | 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x38, 0x2C, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x37, 0x20, 0x43, 0x20, 226 | 0x35, 0x2E, 0x33, 0x33, 0x39, 0x35, 0x30, 0x36, 0x38, 0x2C, 0x32, 0x33, 0x2E, 0x38, 0x35, 0x31, 0x33, 0x39, 0x37, 0x20, 0x30, 227 | 0x2C, 0x31, 0x38, 0x2E, 0x35, 0x31, 0x31, 0x38, 0x39, 0x20, 0x30, 0x2C, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x35, 0x33, 0x37, 0x20, 228 | 0x30, 0x2C, 0x35, 0x2E, 0x33, 0x33, 0x38, 0x31, 0x39, 0x31, 0x38, 0x20, 0x35, 0x2E, 0x33, 0x33, 0x39, 0x35, 0x30, 0x36, 0x38, 229 | 0x2C, 0x30, 0x20, 0x31, 0x31, 0x2E, 0x39, 0x32, 0x36, 0x30, 0x32, 0x37, 0x2C, 0x30, 0x20, 0x5A, 0x22, 0x0A, 0x20, 0x20, 0x20, 230 | 0x20, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x70, 0x61, 0x74, 0x68, 0x39, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x79, 231 | 0x6C, 0x65, 0x3D, 0x22, 0x66, 0x69, 0x6C, 0x6C, 0x3A, 0x23, 0x33, 0x31, 0x32, 0x37, 0x38, 0x33, 0x3B, 0x66, 0x69, 0x6C, 0x6C, 232 | 0x2D, 0x6F, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x3A, 0x31, 0x22, 0x20, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x3C, 0x70, 0x61, 0x74, 233 | 0x68, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64, 0x3D, 0x22, 0x4D, 0x20, 0x31, 0x30, 0x2E, 0x34, 0x39, 0x34, 0x39, 0x30, 0x34, 234 | 0x2C, 0x36, 0x2E, 0x31, 0x31, 0x38, 0x36, 0x38, 0x34, 0x39, 0x20, 0x48, 0x20, 0x36, 0x2E, 0x31, 0x35, 0x37, 0x31, 0x35, 0x30, 235 | 0x37, 0x20, 0x4C, 0x20, 0x31, 0x32, 0x2E, 0x31, 0x39, 0x36, 0x32, 0x37, 0x34, 0x2C, 0x38, 0x2E, 0x38, 0x39, 0x35, 0x34, 0x35, 236 | 0x32, 0x20, 0x48, 0x20, 0x32, 0x2E, 0x38, 0x31, 0x37, 0x38, 0x36, 0x33, 0x20, 0x76, 0x20, 0x31, 0x2E, 0x38, 0x37, 0x32, 0x39, 237 | 0x38, 0x36, 0x20, 0x68, 0x20, 0x39, 0x2E, 0x34, 0x34, 0x30, 0x32, 0x31, 0x39, 0x20, 0x6C, 0x20, 0x2D, 0x34, 0x2E, 0x35, 0x38, 238 | 0x37, 0x32, 0x38, 0x37, 0x35, 0x2C, 0x32, 0x2E, 0x32, 0x33, 0x32, 0x20, 0x48, 0x20, 0x32, 0x2E, 0x38, 0x31, 0x37, 0x38, 0x36, 239 | 0x33, 0x20, 0x76, 0x20, 0x31, 0x2E, 0x38, 0x35, 0x36, 0x35, 0x34, 0x38, 0x20, 0x68, 0x20, 0x34, 0x2E, 0x38, 0x35, 0x32, 0x39, 240 | 0x33, 0x31, 0x35, 0x20, 0x6C, 0x20, 0x35, 0x2E, 0x37, 0x34, 0x31, 0x39, 0x31, 0x37, 0x35, 0x2C, 0x32, 0x2E, 0x37, 0x37, 0x37, 241 | 0x37, 0x35, 0x34, 0x20, 0x68, 0x20, 0x34, 0x2E, 0x33, 0x30, 0x37, 0x35, 0x30, 0x37, 0x20, 0x6C, 0x20, 0x2D, 0x36, 0x2E, 0x30, 242 | 0x35, 0x35, 0x32, 0x33, 0x33, 0x2C, 0x2D, 0x32, 0x2E, 0x37, 0x37, 0x37, 0x37, 0x35, 0x34, 0x20, 0x68, 0x20, 0x39, 0x2E, 0x34, 243 | 0x31, 0x30, 0x33, 0x30, 0x32, 0x20, 0x76, 0x20, 0x2D, 0x31, 0x2E, 0x38, 0x35, 0x36, 0x35, 0x34, 0x38, 0x20, 0x68, 0x20, 0x2D, 244 | 0x39, 0x2E, 0x33, 0x39, 0x34, 0x31, 0x39, 0x32, 0x20, 0x6C, 0x20, 0x34, 0x2E, 0x35, 0x38, 0x37, 0x32, 0x38, 0x38, 0x2C, 0x2D, 245 | 0x32, 0x2E, 0x32, 0x33, 0x32, 0x20, 0x68, 0x20, 0x34, 0x2E, 0x38, 0x30, 0x36, 0x39, 0x30, 0x34, 0x20, 0x56, 0x20, 0x38, 0x2E, 246 | 0x38, 0x39, 0x35, 0x34, 0x35, 0x32, 0x20, 0x68, 0x20, 0x2D, 0x34, 0x2E, 0x38, 0x33, 0x37, 0x38, 0x30, 0x39, 0x20, 0x7A, 0x22, 247 | 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x70, 0x61, 0x74, 0x68, 0x33, 0x37, 0x36, 0x22, 0x0A, 0x20, 0x20, 248 | 0x20, 0x20, 0x20, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x3D, 0x22, 0x66, 0x69, 0x6C, 0x6C, 0x3A, 0x23, 0x66, 0x66, 0x66, 0x66, 0x66, 249 | 0x66, 0x22, 0x20, 0x2F, 0x3E, 0x0A, 0x3C, 0x2F, 0x73, 0x76, 0x67, 0x3E, 0x0A 250 | }; 251 | 252 | static const uint8_t faviconpng[] PROGMEM = { 253 | 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x40, 0x00, 254 | 0x00, 0x00, 0x40, 0x08, 0x06, 0x00, 0x00, 0x00, 0xAA, 0x69, 0x71, 0xDE, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 255 | 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x52, 0x65, 256 | 0x61, 0x64, 0x79, 0x71, 0xC9, 0x65, 0x3C, 0x00, 0x00, 0x04, 0x68, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xEC, 0x5B, 0x41, 0x6B, 257 | 0x13, 0x41, 0x14, 0x9E, 0x4D, 0x23, 0x56, 0x68, 0x6D, 0x84, 0x42, 0x35, 0x2D, 0x98, 0x9E, 0x52, 0x11, 0x4D, 0x8B, 0xA5, 0x47, 258 | 0x69, 0x54, 0x10, 0x84, 0xA2, 0xBD, 0x49, 0x85, 0xB6, 0x39, 0x88, 0x17, 0x41, 0xFA, 0x03, 0x44, 0xC4, 0x83, 0xF4, 0x54, 0x05, 259 | 0x2F, 0x9E, 0x5A, 0x15, 0x0B, 0x3D, 0x48, 0x52, 0x7A, 0x55, 0x12, 0x3C, 0x16, 0xB5, 0x5B, 0x2F, 0x36, 0x1E, 0x34, 0x1E, 0xB4, 260 | 0x15, 0x2C, 0x56, 0xA3, 0xD0, 0x8A, 0xA0, 0xEF, 0x5B, 0x67, 0x6B, 0x4C, 0xB3, 0x9B, 0xD9, 0xCD, 0xCE, 0x66, 0xB3, 0xEB, 0x83, 261 | 0x61, 0x97, 0x36, 0x93, 0xEC, 0xF7, 0xED, 0xF7, 0xDE, 0x9B, 0x79, 0x33, 0xA3, 0x30, 0xC9, 0xD6, 0x17, 0xBF, 0x31, 0x48, 0x17, 262 | 0xB4, 0x04, 0xB5, 0x18, 0xB5, 0xDE, 0x2A, 0x5D, 0x54, 0x6A, 0x05, 0x6A, 0xCB, 0xD4, 0x72, 0x4B, 0xF9, 0xAB, 0x39, 0x99, 0xCF, 263 | 0xA7, 0x48, 0x00, 0x1C, 0xA1, 0xCB, 0x39, 0x6A, 0x67, 0xF9, 0xD5, 0x09, 0xCB, 0x50, 0x9B, 0xC7, 0x95, 0x08, 0xD9, 0xF0, 0x24, 264 | 0x01, 0xFC, 0x4D, 0x8F, 0x51, 0x1B, 0x97, 0x2C, 0xAA, 0x19, 0x6A, 0xB7, 0x89, 0x08, 0xD5, 0x13, 0x04, 0x70, 0xE0, 0xD7, 0xB8, 265 | 0xCC, 0xDD, 0x34, 0xB8, 0xC6, 0xF5, 0x5A, 0x5D, 0x44, 0xA9, 0x51, 0xEA, 0x53, 0x2E, 0xBC, 0x71, 0x11, 0x45, 0x4C, 0xD8, 0x75, 266 | 0x0D, 0xC5, 0x26, 0x78, 0xF8, 0xF6, 0x34, 0xB5, 0x08, 0xF3, 0x86, 0x01, 0x7C, 0x8A, 0x48, 0xC8, 0x48, 0x27, 0x80, 0xC0, 0x4F, 267 | 0x7B, 0xE0, 0xAD, 0x1B, 0xAA, 0x81, 0x48, 0x48, 0x49, 0x21, 0x80, 0x4B, 0x3E, 0x2B, 0x90, 0xC6, 0xEA, 0x6D, 0x08, 0x8E, 0x49, 268 | 0x51, 0x97, 0x08, 0x09, 0x82, 0x8F, 0x35, 0x08, 0x78, 0xC6, 0x9F, 0x31, 0xCB, 0x9F, 0xB9, 0x76, 0x05, 0xF0, 0x2F, 0x5A, 0xF2, 269 | 0x90, 0xBF, 0x5B, 0x89, 0x0B, 0xDD, 0xD5, 0x94, 0x10, 0x12, 0x90, 0x7D, 0xBA, 0x01, 0xC1, 0x33, 0xFE, 0xCC, 0x59, 0x8E, 0xC1, 270 | 0xB6, 0x0B, 0x34, 0x8A, 0xEC, 0x4D, 0xDD, 0xC1, 0x16, 0x01, 0x3C, 0xDA, 0x37, 0x32, 0xF8, 0x6D, 0x12, 0x38, 0x16, 0xF1, 0x18, 271 | 0xC0, 0xF3, 0x7C, 0x9A, 0xF9, 0xCB, 0x86, 0x2B, 0x8D, 0x13, 0x14, 0x03, 0xBF, 0x7F, 0xDB, 0xA0, 0x7E, 0x6F, 0x39, 0x28, 0x56, 272 | 0x72, 0x81, 0x29, 0x1F, 0x82, 0xD7, 0x83, 0xE2, 0x94, 0xA9, 0x02, 0xF8, 0xC4, 0x26, 0x6B, 0xE5, 0x5B, 0xFB, 0x07, 0x0E, 0xB2, 273 | 0x4B, 0x97, 0x8F, 0x0B, 0x7F, 0xFE, 0xC3, 0xFB, 0x2F, 0xEC, 0xEE, 0x9D, 0xA7, 0x74, 0xDD, 0xA8, 0x17, 0x11, 0xC9, 0xD2, 0x09, 274 | 0x54, 0x39, 0x01, 0x59, 0x3B, 0xB3, 0xBA, 0xD6, 0xBD, 0xCD, 0x6C, 0x64, 0x74, 0x80, 0x8D, 0x8C, 0x0D, 0xB0, 0xD6, 0xD6, 0x66, 275 | 0xA1, 0x3E, 0x0B, 0xE9, 0x97, 0xF5, 0x22, 0x02, 0x45, 0x96, 0xE4, 0x0E, 0x02, 0x08, 0x7C, 0x2F, 0x1F, 0xF0, 0xD8, 0x36, 0x10, 276 | 0x31, 0x78, 0x32, 0xAE, 0x29, 0x22, 0xDA, 0xD9, 0xE6, 0x65, 0x22, 0xFA, 0xF4, 0x7A, 0x42, 0x93, 0xFE, 0x97, 0x03, 0xED, 0x27, 277 | 0x6E, 0xD6, 0x9A, 0xF6, 0x7E, 0x6C, 0xFD, 0x64, 0xAF, 0x57, 0x3E, 0xB2, 0xD9, 0xFB, 0x8B, 0x9A, 0xD4, 0xA3, 0x5D, 0x11, 0xD6, 278 | 0xDE, 0xDE, 0x62, 0xDA, 0x27, 0x7E, 0xA8, 0x43, 0x53, 0x0E, 0x5C, 0x69, 0x95, 0xFA, 0xA0, 0x9F, 0x0B, 0xD6, 0xBC, 0xB6, 0x9E, 279 | 0x9D, 0xDF, 0x56, 0x00, 0x8F, 0xFC, 0x9F, 0x65, 0xFC, 0x92, 0x1E, 0x23, 0x8E, 0xD1, 0x55, 0xC4, 0x9E, 0x2F, 0xBE, 0xD3, 0x14, 280 | 0xF1, 0x8C, 0xAE, 0x92, 0x6D, 0x1F, 0x32, 0x42, 0x13, 0x7F, 0xFB, 0xE7, 0x99, 0x73, 0xF5, 0xBB, 0x1D, 0x41, 0x0F, 0x32, 0xCF, 281 | 0x3D, 0xC9, 0xB3, 0xDD, 0xBB, 0x77, 0x69, 0x6F, 0xDC, 0xCC, 0xA2, 0x9D, 0x11, 0x36, 0x34, 0x9C, 0x70, 0x43, 0x11, 0x79, 0x52, 282 | 0x81, 0xAA, 0x13, 0x80, 0x92, 0x56, 0x8F, 0x4C, 0xBA, 0xD7, 0x3F, 0x7D, 0xD7, 0x48, 0x00, 0x19, 0x88, 0x15, 0xD1, 0xAE, 0x36, 283 | 0x22, 0x24, 0x2C, 0x44, 0x04, 0x84, 0x0A, 0xD7, 0x72, 0xDA, 0x88, 0x80, 0x39, 0xDD, 0x05, 0x7E, 0x19, 0x3D, 0x84, 0x68, 0x30, 284 | 0xB3, 0x6A, 0x2D, 0x44, 0xC2, 0x05, 0xCA, 0x1C, 0xA2, 0xAE, 0xA1, 0xA7, 0xCF, 0x55, 0x8B, 0xC1, 0xB2, 0x58, 0xDC, 0x64, 0xF9, 285 | 0x57, 0x95, 0xC9, 0x23, 0x17, 0x50, 0x14, 0xB3, 0xDC, 0x0F, 0xDF, 0xB5, 0x92, 0xE3, 0xBD, 0x68, 0x88, 0x29, 0x17, 0x47, 0x1F, 286 | 0x18, 0x8E, 0x09, 0x42, 0xCC, 0xFD, 0x6A, 0xAE, 0x97, 0x6C, 0x10, 0x04, 0x24, 0x02, 0x4C, 0x40, 0x02, 0x04, 0xC4, 0x02, 0x4C, 287 | 0x40, 0x2C, 0xE4, 0x93, 0x39, 0xBF, 0xED, 0x5A, 0x81, 0x62, 0x94, 0x01, 0x64, 0x1B, 0x32, 0x0C, 0x02, 0xEC, 0xD0, 0xF0, 0xD1, 288 | 0xBA, 0x0E, 0x99, 0xC3, 0x41, 0x05, 0xEE, 0x3A, 0x01, 0x5E, 0x03, 0xEE, 0x1A, 0x01, 0x56, 0xE6, 0x02, 0x18, 0xB4, 0x00, 0xF8, 289 | 0xEC, 0xBD, 0x45, 0xD7, 0x66, 0x87, 0x61, 0xAF, 0x00, 0x07, 0x68, 0xCC, 0x22, 0x8B, 0x5F, 0x37, 0x5D, 0x75, 0xC9, 0x70, 0x35, 290 | 0xD9, 0x5A, 0x1D, 0x0A, 0x5B, 0x1D, 0xE2, 0x62, 0xA4, 0xF6, 0x90, 0x80, 0x7F, 0x23, 0xE0, 0xF1, 0x9E, 0x0E, 0xC7, 0x01, 0x9A, 291 | 0x0D, 0x85, 0xAB, 0x12, 0x00, 0x7F, 0x95, 0x3D, 0x14, 0x06, 0x51, 0xA2, 0x64, 0x49, 0x18, 0x0A, 0x6B, 0x45, 0x51, 0x95, 0x05, 292 | 0xD7, 0x54, 0x10, 0x50, 0x08, 0x30, 0x01, 0x05, 0x10, 0xB0, 0x1C, 0x60, 0x02, 0x96, 0x41, 0x40, 0x2E, 0xC0, 0x04, 0xE4, 0x4C, 293 | 0x0B, 0x22, 0x76, 0xCC, 0x6A, 0x89, 0x5C, 0xCF, 0x02, 0xB9, 0xC7, 0x79, 0xD7, 0xD1, 0xA3, 0x20, 0xA2, 0x97, 0xC4, 0x30, 0x21, 294 | 0x72, 0xA4, 0x24, 0x86, 0xCA, 0x30, 0x40, 0x3D, 0x9A, 0x7B, 0xA1, 0xDD, 0xA3, 0x06, 0x58, 0xAD, 0xF4, 0x75, 0xFA, 0xCC, 0x61, 295 | 0xAD, 0xFC, 0x55, 0x2C, 0x6E, 0x49, 0x29, 0x7D, 0x19, 0x58, 0x06, 0x25, 0x31, 0x9D, 0x80, 0x3D, 0xCC, 0xE1, 0xA2, 0x68, 0x29, 296 | 0x11, 0x85, 0x37, 0xEB, 0x44, 0xC4, 0x7E, 0x4D, 0x1D, 0x66, 0xCA, 0x49, 0x9E, 0x8A, 0x6B, 0x44, 0xE0, 0x1E, 0x44, 0xE0, 0x3B, 297 | 0x24, 0xDA, 0x24, 0x8A, 0xA2, 0xD2, 0xCB, 0xE2, 0xFF, 0x8E, 0x2B, 0x12, 0xC2, 0x8B, 0x26, 0x2E, 0x0C, 0x8B, 0xFF, 0x96, 0xC5, 298 | 0x89, 0x89, 0x4D, 0x52, 0x41, 0x4C, 0x76, 0x6D, 0xA0, 0x74, 0xD1, 0xA4, 0x9A, 0x22, 0xE0, 0x36, 0x47, 0x12, 0x9D, 0x5A, 0x2C, 299 | 0x81, 0x9B, 0xA0, 0x2F, 0x48, 0x71, 0xC8, 0xB0, 0x9B, 0x6C, 0x0E, 0x37, 0xA5, 0x2B, 0x43, 0x28, 0xC0, 0x8F, 0xBB, 0xE1, 0x7C, 300 | 0x56, 0x88, 0x80, 0x49, 0x58, 0x3D, 0x4A, 0xD1, 0x4B, 0x5F, 0xC3, 0x8D, 0x23, 0x8B, 0xA3, 0x6E, 0x4E, 0x9C, 0x60, 0x79, 0x10, 301 | 0x48, 0xAE, 0xB1, 0x90, 0xB6, 0x35, 0x84, 0xA9, 0xBC, 0x38, 0xCA, 0x09, 0x00, 0xF8, 0x2C, 0xAB, 0x93, 0xF5, 0x5B, 0x9C, 0x17, 302 | 0x80, 0x08, 0x1B, 0xE9, 0xD3, 0x78, 0x79, 0x9C, 0x93, 0xE0, 0xE5, 0x9D, 0xA0, 0x4E, 0xF8, 0x7E, 0xAA, 0x7C, 0x32, 0x54, 0x6E, 303 | 0x13, 0xEC, 0xCF, 0x76, 0x12, 0xBF, 0xD9, 0x06, 0xC7, 0xC6, 0x4C, 0x09, 0xE0, 0x7B, 0x68, 0x52, 0x3E, 0x24, 0x20, 0x55, 0x69, 304 | 0xD3, 0x64, 0x53, 0xA5, 0x4F, 0x52, 0x84, 0x5C, 0x71, 0x23, 0x2D, 0xBA, 0x2C, 0xFD, 0xC9, 0x4A, 0xFF, 0x30, 0xDD, 0x2A, 0x4B, 305 | 0xF1, 0x60, 0xC9, 0x07, 0x24, 0xA8, 0x04, 0xBE, 0xCF, 0xAC, 0x20, 0x62, 0x1A, 0x31, 0x1B, 0xBC, 0x60, 0xA2, 0x72, 0x0C, 0x86, 306 | 0x26, 0xB2, 0x59, 0xBA, 0x51, 0xF7, 0x0D, 0xD6, 0xBE, 0x59, 0xBA, 0x24, 0x28, 0x76, 0x37, 0x98, 0x12, 0x54, 0x11, 0xF0, 0x42, 307 | 0x0A, 0x28, 0x53, 0x82, 0xEF, 0x0E, 0x4C, 0xF8, 0xED, 0xC8, 0xCC, 0x2D, 0x02, 0x3E, 0x61, 0xA5, 0x43, 0xE0, 0x0F, 0x4D, 0x85, 308 | 0xEC, 0xFC, 0x1A, 0xFF, 0x21, 0xC4, 0x85, 0x19, 0x2F, 0xE4, 0x78, 0xEE, 0xEF, 0x19, 0x3B, 0x9D, 0xFF, 0x1F, 0x9C, 0x74, 0xEA, 309 | 0x69, 0xF8, 0x56, 0xDB, 0x2B, 0x2C, 0x68, 0x47, 0x67, 0x0D, 0xB2, 0x45, 0xF0, 0x0E, 0x4F, 0x57, 0x71, 0x11, 0x34, 0x4F, 0x1E, 310 | 0x9F, 0xFF, 0x2D, 0xC0, 0x00, 0x68, 0x21, 0x21, 0x7D, 0xBF, 0xC9, 0xBE, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 311 | 0xAE, 0x42, 0x60, 0x82 312 | }; 313 | --------------------------------------------------------------------------------