├── ESP8266_METAR_DECODER_REVISED_NWS_FORMAT_V7_SSL.ino ├── ESP_METAR_DECODER_REVISED_NWS_FORMAT_V7_SSL.ino ├── Licence.txt ├── M5_ESP32_METAR_DECODER_REVISED_NWS_FORMAT_V51_SSL.ino ├── README.md └── Wireless-tag-ESP32_SCT01_METAR_DECODER_NWS_V7_SSL.ino /ESP8266_METAR_DECODER_REVISED_NWS_FORMAT_V7_SSL.ino: -------------------------------------------------------------------------------- 1 | /* Version 6 METAR Decoder and display for ESP8266/ESP32 and ILI9341 TFT screen 2 | 3 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 4 | 5 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 6 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 7 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 8 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 9 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 10 | 11 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 12 | software use is visible to an end-user. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 15 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | See more at http://www.dsbird.org.uk 19 | 20 | */ 21 | //////////////////////////////////////////////////////////////////////////////////// 22 | String version_num = "METAR ESP Version 6.0"; 23 | #ifdef ESP32 24 | #include 25 | #define CS 17 // ESP32 GPIO 17 goes to TFT CS 26 | #define DC 16 // ESP32 GPIO 16 goes to TFT DC 27 | #define MOSI 23 // ESP32 GPIO 23 goes to TFT MOSI 28 | #define SCLK 18 // ESP32 GPIO 18 goes to TFT SCK/CLK 29 | #define RST 5 // ESP32 GPIO 5 ESP RST to TFT RESET 30 | #define MISO // Not connected 31 | // 3.3V // Goes to TFT LED 32 | // 5v // Goes to TFT Vcc 33 | // Gnd // Goes to TFT Gnd 34 | #else 35 | #include 36 | #define CS D0 // Wemos D1 Mini D0 goes to TFT CS 37 | #define DC D8 // Wemos D1 Mini D8 goes to TFT DC 38 | #define MOSI D7 // Wemos D1 Mini D7 goes to TFT MOSI 39 | #define SCLK D5 // Wemos D1 Mini D5 goes to TFT SCK/CLK 40 | #define RST // Wemos D1 Mini RST on ESP goes to TFT RESET 41 | #define MISO // Wemos D1 Mini Not connected 42 | // 3.3V // Goes to TFT LED 43 | // 5v // Goes to TFT Vcc 44 | // Gnd // Goes to TFT Gnd 45 | #endif 46 | 47 | #include 48 | #include 49 | #include "SPI.h" 50 | #include "Adafruit_GFX.h" 51 | #include "Adafruit_ILI9341.h" 52 | 53 | // Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC 54 | Adafruit_ILI9341 tft = Adafruit_ILI9341(CS, DC); 55 | 56 | const char *ssid = "yourSSID"; 57 | const char *password = "yourPASSWORD"; 58 | const char* host = "aviationweather.gov"; 59 | const int httpsPort = 443; 60 | 61 | WiFiClientSecure client; 62 | 63 | const int centreX = 274; // Location of the compass display on screen 64 | const int centreY = 60; 65 | const int diameter = 41; // Size of the compass 66 | 67 | // Assign human-readable names to common 16-bit color values: 68 | #define BLACK 0x0000 69 | #define RED 0xF800 70 | #define GREEN 0x07E0 71 | #define BLUE 0x001F 72 | #define CYAN 0x07FF 73 | #define MAGENTA 0xF81F 74 | #define YELLOW 0xFFE0 75 | #define WHITE 0xFFFF 76 | 77 | void setup() { 78 | Serial.begin(115200); 79 | tft.begin(); 80 | tft.setRotation(3); 81 | clear_screen(); 82 | display_progress("Connecting to Network", 25); 83 | WiFi.begin(ssid, password); 84 | while (WiFi.status() != WL_CONNECTED) { 85 | delay(100); 86 | Serial.print("."); 87 | } 88 | Serial.println("\nWiFi connected at: " + WiFi.localIP().toString()); 89 | display_status(); 90 | } 91 | 92 | void loop() { 93 | // Change these METAR Stations to suit your needs see: Use this URL address = ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/ 94 | // to establish your list of sites to retrieve (you must know the 4-letter 95 | // site dentification) 96 | GET_METAR("EGGD", "1 EGGD Bristol/Lulsgate"); 97 | GET_METAR("EGVN", "2 EGVN Brize Norton"); 98 | GET_METAR("EGCC", "3 EGCC Manchester Airport"); 99 | GET_METAR("EGHQ", "4 EGHQ Newquay"); 100 | GET_METAR("EGSS", "5 EGSS Stansted"); 101 | } 102 | 103 | //---------------------------------------------------------------------------------------------------- 104 | void GET_METAR(String station, String Name) { //client function to send/receive GET request data. 105 | String metar = " "; 106 | bool metar_status = false; 107 | const int time_delay = 20000; 108 | display_item(35, 100, "Decoding METAR", GREEN, 3); 109 | display_item(90, 135, "for " + station, GREEN, 3); 110 | Serial.println("Requesting data for : " + Name); 111 | //--------------------------------------------------- 112 | HTTPClient http; 113 | WiFiClientSecure client; 114 | client.setInsecure(); 115 | http.begin(client, host); 116 | String payload; 117 | if (http.GET() == HTTP_CODE_OK) { 118 | payload = http.getString(); 119 | Serial.println(payload); 120 | } 121 | //--------------------------------------------------- 122 | if (!client.connect(host, httpsPort)) { 123 | Serial.println("Connection failed"); 124 | delay(1000); 125 | return; 126 | } 127 | // http://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 (example) 128 | // https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 129 | // https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=json&stationString=EGLL&hoursBeforeNow=1 130 | // https://aviationweather.gov/cgi-bin/data/metar.php?datasource=metars&requestType=retrieve&ids=EGLL&hoursBeforeNow=1 131 | // https://aviationweather.gov/cgi-bin/data/metar.php?datasource=metars&requestType=retrieve&format=xml&hours=0&ids=EGLL 132 | // ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/EGCC.TXT 133 | display_item(265, 230, "Connected", RED, 1); 134 | String url = "/cgi-bin/data/metar.php?dataSource=metars&requestType=retrieve&format=xml&ids=" + station + "&hoursBeforeNow=1"; 135 | client.print(String("GET ") + url + " HTTP/1.0\r\n" + 136 | "Host: " + host + "\r\n" + 137 | "Content-Type: text/html; charset=utf-8\r\n" + 138 | "Access-Control-Max-Age: 10\r\n" + 139 | "User-Agent: Mozilla/5.0\r\n" + 140 | "Connection: close\r\n\r\n"); 141 | // *See Annex for a detailed response example: 142 | while (client.available()) Serial.print(client.readString()); 143 | metar_status = client.find(""); 144 | client.find(""); 145 | if (metar_status == true) metar = client.readStringUntil('<'); else metar = "Station off-air"; 146 | Serial.println(metar); 147 | client.stop(); 148 | clear_screen(); // Clear screen 149 | display_item(265, 230, "Connected", RED, 1); 150 | display_item(0, 230, version_num, GREEN, 1); 151 | display_item(0, 190, Name, YELLOW, 2); 152 | tft.drawLine(0, 185, 320, 185, YELLOW); 153 | display_item(0, 210, metar, YELLOW, 1); 154 | if (metar_status == true) { 155 | display_metar(metar); 156 | delay(time_delay); // Delay for set time 157 | } 158 | else 159 | { 160 | display_item(70, 100, metar, YELLOW, 2); // Now decode METAR 161 | delay(5000); // Wait less time if station off-air 162 | } 163 | clear_screen(); // Clear screen before moving to next station display 164 | } 165 | 166 | void display_metar(String metar) { 167 | int temperature = 0; 168 | int dew_point = 0; 169 | int wind_speedKTS = 0; 170 | char str[130] = " "; //EGDM 261550Z AUTO 22017G23KT 9999 4000SE -SHRA SCT012/// BKN019/// BKN060/// 14/13 Q1001 RERA NOSIG BLU"; 171 | char *parameter; // a pointer variable 172 | String conditions_start = " "; 173 | String temp_strA = ""; 174 | String temp_strB = ""; 175 | String temp_Pres = ""; 176 | String conditions_test = "+-BDFGHIMPRSTUV"; // Test for light or heavy (+/-) BR - Mist, RA - Rain SH-Showers VC - Vicinity, etc 177 | // Test metar, press enter after colon to include for test):metar = "EGDM 261550Z AUTO 26517G23KT 250V290 6999 R04/1200 4200SW -SH SN SCT002TCU NCD FEW222/// SCT090 28/M15 Q1001 RERA NOSIG BLU"; 178 | // Test metar exercises all decoder functions on screen 179 | metar.toCharArray(str, 130); 180 | parameter = strtok(str, " "); // Find tokens that are seperated by SPACE 181 | while (parameter != NULL) { 182 | //display_item(0,0, String(parameter),RED,2); // Station Name - omitted at V2.01 183 | temp_strA = strtok(NULL, " "); 184 | display_item(0, 0, "Date:" + temp_strA.substring(0, 2) + " @ " + temp_strA.substring(2, 6) + "Hr", GREEN, 2); // Date-time 185 | 186 | //---------------------------------------------------------------------------------------------------- 187 | // Process any reported station type e.g. AUTO means Automatic 188 | temp_strA = strtok(NULL, " "); 189 | if (temp_strA == "AUTO") { 190 | //display_item(54,0,"A",CYAN,2); // Omitted at V2.01 191 | temp_strA = strtok(NULL, " "); 192 | } 193 | 194 | //---------------------------------------------------------------------------------------------------- 195 | // Process any reported wind direction and speed e.g. 270019KTS means direction is 270 deg and speed 19Kts 196 | // radians = (degrees * 71) / 4068 from Pi/180 to convert to degrees 197 | Draw_Compass_Rose(); // Draw compass rose 198 | if (temp_strA == "/////KT") { 199 | temp_strA = "00000KT"; 200 | } 201 | temp_strB = temp_strA.substring(3, 5); 202 | wind_speedKTS = temp_strB.toInt(); // Knots/sec 203 | int wind_speedMPH = wind_speedKTS * 1.15077 + 0.5; 204 | if (temp_strA.indexOf('G') >= 0) { 205 | temp_strB = temp_strA.substring(8); 206 | } 207 | else { 208 | temp_strB = temp_strA.substring(5); 209 | } // Now get units of wind speed either KT or MPS 210 | if (temp_strB == "MPS") { 211 | temp_strB = "MS"; 212 | } 213 | if (wind_speedMPH < 10) display_item((centreX - 28), (centreY + 50), (String(wind_speedMPH) + " MPH"), YELLOW, 2); 214 | else { 215 | display_item((centreX - 35), (centreY + 50), (String(wind_speedMPH) + " MPH"), YELLOW, 2); 216 | if (wind_speedMPH >= 18) display_item((centreX - 35), (centreY + 50), (String(wind_speedMPH) + " MPH"), RED, 2); 217 | } 218 | if (temp_strA.indexOf('G') >= 0) { 219 | tft.fillRect(centreX - 40, centreY + 48, 82, 18, BLACK); 220 | display_item((centreX - 40), (centreY + 50), String(wind_speedKTS) + "g" + temp_strA.substring(temp_strA.indexOf('G') + 1, temp_strA.indexOf('G') + 3) + temp_strB, YELLOW, 2); 221 | } 222 | int wind_direction = 0; 223 | if (temp_strA.substring(0, 3) == "VRB") { 224 | display_item((centreX - 15), (centreY - 5), "VRB", YELLOW, 2); 225 | } 226 | else { 227 | wind_direction = temp_strA.substring(0, 3).toInt() - 90; 228 | int dx = (diameter * cos((wind_direction) * 0.017444)) + centreX; 229 | int dy = (diameter * sin((wind_direction) * 0.017444)) + centreY; 230 | arrow(dx, dy, centreX, centreY, 5, 5, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 231 | } 232 | 233 | //---------------------------------------------------------------------------------------------------- 234 | // get next token, this could contain Veering information e.g. 0170V220 235 | temp_strA = strtok(NULL, " "); 236 | if (temp_strA.indexOf('V') >= 0 && temp_strA != "CAVOK") { // Check for variable wind direction 237 | int startV = temp_strA.substring(0, 3).toInt(); 238 | int endV = temp_strA.substring(4, 7).toInt(); 239 | // Minimum angle is either ABS(AngleA- AngleB) or (360-ABS(AngleA-AngleB)) 240 | int veering = min_val(360 - abs(startV - endV), abs(startV - endV)); 241 | display_item(136, 110, "V" + String(veering) + char(247), RED, 2); 242 | display_item((centreX - 40), (centreY + 30), "v", RED, 2); // Signify 'Variable wind direction 243 | draw_veering_arrow(startV); 244 | draw_veering_arrow(endV); 245 | temp_strA = strtok(NULL, " "); // Move to the next token/item 246 | } 247 | 248 | //---------------------------------------------------------------------------------------------------- 249 | // Process any reported visibility e.g. 6000 means 6000 Metres of visibility 250 | if (temp_strA == "////") { 251 | temp_strB = "No Visibility Rep."; 252 | } else temp_strB = ""; 253 | if (temp_strA == "CAVOK") { 254 | display_item(0, 20, "Visibility good", WHITE, 1); 255 | display_item(0, 30, "Conditions good", WHITE, 1); 256 | } 257 | else { 258 | if (temp_strA != "////") { 259 | if (temp_strA == "9999") { 260 | temp_strB = "Visibility excellent"; 261 | } 262 | else { 263 | String vis = temp_strA; 264 | while (vis.startsWith("0")) { 265 | vis = vis.substring(1); // trim leading '0' 266 | } 267 | temp_strB = vis + " Metres of visibility"; 268 | } 269 | } 270 | } 271 | display_item(0, 20, temp_strB, WHITE, 1); 272 | temp_strA = strtok(NULL, " "); 273 | 274 | //---------------------------------------------------------------------------------------------------- 275 | // Process any secondary Runway visibility reports e.g. R04/1200 means Runway 4, visibility 1200M 276 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 277 | // Ignore the report for now 278 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 279 | } 280 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 281 | // Ignore the report for now, there can be two! 282 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 283 | } 284 | 285 | //---------------------------------------------------------------------------------------------------- 286 | // Process any secondary reported visibility e.g. 6000S (or 6000SW) means 6000 Metres in a Southerly direction (or from SW) 287 | if (temp_strA.length() >= 5 && temp_strA.substring(0, 1) != "+" && temp_strA.substring(0, 1) != "-" && (temp_strA.endsWith("N") || temp_strA.endsWith("S") || temp_strA.endsWith("E") || temp_strA.endsWith("W")) ) { 288 | conditions_start = temp_strA.substring(4); 289 | conditions_start = conditions_start.substring(0, 1); 290 | if (conditions_start == "N" || conditions_start == "S" || conditions_start == "E" || conditions_start == "W") { 291 | tft.fillRect(25, 20, 170, 20, BLACK); 292 | display_item(25, 20, "/" + temp_strA + " Mts Visibility", WHITE, 1); 293 | } 294 | temp_strA = strtok(NULL, " "); 295 | } 296 | 297 | //---------------------------------------------------------------------------------------------------- 298 | // Process any reported weather conditions e.g. -RA means light rain 299 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 300 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV/// 301 | tft.drawRect(132, 129, 188, 12, YELLOW); 302 | display_item(136, 132, "Additional Wx Reports:", WHITE, 1); 303 | conditions_start = temp_strA.substring(0, 1); 304 | temp_strB = temp_strA.substring(0, 3); 305 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 306 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 307 | && temp_strB != "NSC" 308 | && !temp_strA.startsWith("M0")) { 309 | temp_strB = ""; 310 | if (conditions_start == "-" || conditions_start == "+") { 311 | if (conditions_start == "-") { 312 | temp_strB = "Light "; 313 | } else { 314 | temp_strB = "Heavy "; 315 | } 316 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 317 | } 318 | if (temp_strA.length() == 2) { 319 | display_item(136, 163, temp_strB + display_conditions(temp_strA), WHITE, 1); 320 | } 321 | else { 322 | display_item(136, 163, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 323 | display_item_nxy("/" + display_conditions(temp_strA.substring(2, 4)), WHITE, 1); 324 | if (temp_strA.length() >= 6) { // sometimes up to three cateries are reported 325 | display_item_nxy("/" + display_conditions(temp_strA.substring(4, 6)), WHITE, 1); 326 | } 327 | } 328 | parameter = strtok(NULL, " "); 329 | temp_strA = parameter; 330 | } 331 | 332 | // Process any reported weather conditions e.g. -RA means light rain 333 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 334 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV///(poor vertical visibility) 335 | conditions_start = temp_strA.substring(0, 1); 336 | temp_strB = temp_strA.substring(0, 3); 337 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 338 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 339 | && temp_strB != "NSC" 340 | && !temp_strA.startsWith("M0")) { 341 | temp_strB = ""; 342 | if (conditions_start == "-" || conditions_start == "+") { 343 | if (conditions_start == "-") { 344 | temp_strB = "Light "; 345 | } else { 346 | temp_strB = "Heavy "; 347 | } 348 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 349 | } 350 | if (temp_strA.length() == 2) { 351 | display_item(136, 173, temp_strB + display_conditions(temp_strA), WHITE, 1); 352 | } 353 | else { 354 | if (temp_strA.length() >= 5) { // sometimes up to three cateries are reported 355 | display_item(136, 173, "Poor Vert Visibility", WHITE, 1); 356 | } 357 | else { 358 | display_item(136, 173, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 359 | } 360 | } 361 | parameter = strtok(NULL, " "); 362 | temp_strA = parameter; 363 | } 364 | 365 | //---------------------------------------------------------------------------------------------------- 366 | // Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 367 | tft.drawLine(0, 40, 229, 40, YELLOW); 368 | // BLOCK-1 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 369 | if (valid_cloud_report(temp_strA) || temp_strA.startsWith("VV/")) { 370 | temp_strA = convert_clouds(temp_strA); 371 | display_item(0, 45, temp_strA, WHITE, 1); 372 | temp_strA = strtok(NULL, " "); 373 | } 374 | 375 | // BLOCK-2 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 376 | if (valid_cloud_report(temp_strA)) { 377 | temp_strA = convert_clouds(temp_strA); 378 | display_item(0, 55, temp_strA, WHITE, 1); 379 | temp_strA = strtok(NULL, " "); 380 | } 381 | 382 | // BLOCK-3 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 383 | if (valid_cloud_report(temp_strA)) { 384 | temp_strA = convert_clouds(temp_strA); 385 | display_item(0, 65, temp_strA, WHITE, 1); 386 | temp_strA = strtok(NULL, " "); 387 | } 388 | 389 | // BLOCK-4 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 390 | if (valid_cloud_report(temp_strA)) { 391 | temp_strA = convert_clouds(temp_strA); 392 | display_item(0, 75, temp_strA, WHITE, 1); 393 | temp_strA = strtok(NULL, " "); 394 | } 395 | 396 | //---------------------------------------------------------------------------------------------------- 397 | // Process any reported temperatures e.g. 14/12 means Temp 14C Dewpoint 12 398 | // Test first section of temperature/dewpoint which is 'temperature' so either 12/nn or M12/Mnn 399 | if (temp_strA.indexOf("/") <= 0) { 400 | temp_Pres = temp_strA; 401 | temp_strA = "00/00"; 402 | } // Ignore no temp reports and save the next message, which is air pressure for later processing 403 | String temp_sign = ""; 404 | if (temp_strA.startsWith("M")) { 405 | temperature = 0 - temp_strA.substring(1, 3).toInt(); 406 | if (temperature == 0) { 407 | temp_sign = "-"; // Reports of M00, meaning between -0.1 and -0.9C 408 | } 409 | } 410 | else { 411 | temperature = temp_strA.substring(0, 2).toInt(); 412 | if (temperature >= 0 && temperature < 10) { 413 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 414 | } 415 | } 416 | if (temperature <= 25) { 417 | display_item(0, 110, " Temp " + temp_sign + String(temperature) + char(247) + "C", CYAN, 2); 418 | } 419 | else { 420 | display_item(0, 110, " Temp " + temp_sign + String(temperature) + char(247) + "C", RED, 2); 421 | } 422 | 423 | //---------------------------------------------------------------------------------------------------- 424 | // Test second section of temperature/dewpoint which is 'dewpoint' so either nn/04 or Mnn/M04 425 | temp_strB = temp_strA.substring(temp_strA.indexOf('/') + 1); 426 | if (temp_strB.startsWith("M")) { 427 | dew_point = 0 - temp_strB.substring(1, 3).toInt(); 428 | } 429 | else { 430 | dew_point = temp_strB.substring(0, 2).toInt(); 431 | } 432 | 433 | //---------------------------------------------------------------------------------------------------- 434 | // Don't display windchill unless wind speed is greater than 3 MPH and temperature is less than 14'C 435 | if (wind_speedKTS > 3 && temperature <= 14) { 436 | temp_sign = ""; 437 | int wind_chill = int(calc_windchill(temperature, wind_speedKTS)); 438 | if (wind_chill >= 0 && wind_chill < 10) { 439 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 440 | } 441 | display_item(0, 164, "WindC " + temp_sign + String(wind_chill) + char(247) + "C", CYAN, 2); 442 | } 443 | 444 | //---------------------------------------------------------------------------------------------------- 445 | // Calculate and display Relative Humidity 446 | temp_sign = ""; 447 | if (dew_point >= 0 && dew_point < 10) { 448 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 449 | } 450 | display_item(0, 127, " Dewp " + temp_sign + String(dew_point) + char(247) + "C", CYAN, 2); 451 | int RH = calc_rh(temperature, dew_point); 452 | display_item(00, 146, "Rel.H " + String(RH) + "%", CYAN, 2); 453 | 454 | //---------------------------------------------------------------------------------------------------- 455 | // Don't display heatindex unless temperature > 18C 456 | if (temperature >= 20) { 457 | float T = (temperature * 9 / 5) + 32; 458 | float RHx = RH; 459 | int tHI = (-42.379 + (2.04901523 * T) + (10.14333127 * RHx) - (0.22475541 * T * RHx) - (0.00683783 * T * T) - (0.05481717 * RHx * RHx) + (0.00122874 * T * T * RHx) + (0.00085282 * T * RHx * RHx) - (0.00000199 * T * T * RHx * RHx) - 32 ) * 5 / 9; 460 | display_item(0, 164, "HeatX " + String(tHI) + char(247) + "C", CYAN, 2); 461 | } 462 | //where HI = -42.379 + 2.04901523*T + 10.14333127*RH - 0.22475541*T*RH - 0.00683783*T*T - 0.05481717*RH*RH + 0.00122874*T*T*RH + 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH 463 | //tHI = heat index (oF) 464 | //t = air temperature (oF) (t > 57oF) 465 | //φ = relative humidity (%) 466 | //---------------------------------------------------------------------------------------------------- 467 | //Process air pressure (QNH) e.g. Q1018 means 1018mB 468 | temp_strA = strtok(NULL, " "); 469 | temp_strA.trim(); 470 | if (temp_Pres.startsWith("Q")) temp_strA = temp_Pres; // Pickup a temporary copy of air pressure, found when no temp broadcast 471 | if (temp_strA.startsWith("Q")) { 472 | if (temp_strA.substring(1, 2) == "0") { 473 | temp_strA = " " + temp_strA.substring(2); 474 | } 475 | else { 476 | temp_strA = temp_strA.substring(1); 477 | } 478 | display_item((centreX - 35), (centreY - 57), temp_strA + "mB", YELLOW, 2); 479 | } 480 | 481 | //---------------------------------------------------------------------------------------------------- 482 | // Now process up to two secondary weather reports 483 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 484 | Process_secondary_reports(temp_strA, 143); 485 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 486 | Process_secondary_reports(temp_strA, 153); 487 | parameter = NULL; // Reset the 'str' pointer, so end of report decoding 488 | } 489 | } // finished decoding the METAR string 490 | //---------------------------------------------------------------------------------------------------- 491 | //---------------------------------------------------------------------------------------------------- 492 | 493 | String convert_clouds(String source) { 494 | String height = source.substring(3, 6); 495 | String cloud = source.substring(0, 3); 496 | String warning = " "; 497 | while (height.startsWith("0")) { 498 | height = height.substring(1); // trim leading '0' 499 | } 500 | if (source.endsWith("TCU") || source.endsWith("CB")) { 501 | display_item(0, 95, "Warning - storm clouds detected", WHITE, 1); 502 | warning = " (storm) "; 503 | } 504 | // 'adjust offset if 0 replaced by space 505 | if (cloud != "SKC" && cloud != "CLR" && height != " ") { 506 | height = " at " + height + "00ft"; 507 | } else height = ""; 508 | if (source == "VV///") { 509 | return "No cloud reported"; 510 | } 511 | if (cloud == "BKN") { 512 | return "Broken" + warning + "clouds" + height; 513 | } 514 | if (cloud == "SKC") { 515 | return "Clear skies"; 516 | } 517 | if (cloud == "FEW") { 518 | return "Few" + warning + "clouds" + height; 519 | } 520 | if (cloud == "NCD") { 521 | return "No clouds detected"; 522 | } 523 | if (cloud == "NSC") { 524 | return "No signficiant clouds"; 525 | } 526 | if (cloud == "OVC") { 527 | return "Overcast" + warning + height; 528 | } 529 | if (cloud == "SCT") { 530 | return "Scattered" + warning + "clouds" + height; 531 | } 532 | return ""; 533 | } 534 | 535 | 536 | int min_val(int num1, int num2) { 537 | if (num1 > num2) 538 | { 539 | return num2; 540 | } 541 | else 542 | { 543 | return num1; 544 | } 545 | } 546 | 547 | float calc_rh(int temp, int dewp) { 548 | return 100 * (exp((17.271 * dewp) / (237.7 + dewp))) / (exp((17.271 * temp) / (237.7 + temp))) + 0.5; 549 | } 550 | 551 | float calc_windchill(int temperature, int wind_speed) { 552 | float result; 553 | // Derived from wind_chill = 13.12 + 0.6215 * Tair - 11.37 * POWER(wind_speed,0.16)+0.3965 * Tair * POWER(wind_speed,0.16) 554 | wind_speed = wind_speed * 1.852; // Convert to Kph 555 | result = 13.12 + 0.6215 * temperature - 11.37 * pow(wind_speed, 0.16) + 0.3965 * temperature * pow(wind_speed, 0.16); 556 | if (result < 0 ) { 557 | return result - 0.5; 558 | } else { 559 | return result + 0.5; 560 | } 561 | } 562 | 563 | void Process_secondary_reports(String temp_strA, int line_pos) { 564 | temp_strA.trim(); 565 | if (temp_strA == "NOSIG") { 566 | display_item(136, line_pos, "No significant change expected", WHITE, 1); 567 | } 568 | if (temp_strA == "TEMPO") { 569 | display_item(136, line_pos, "Temporary conditions expected", WHITE, 1); 570 | } 571 | if (temp_strA == "RADZ") { 572 | display_item(136, line_pos, "Recent Rain/Drizzle", WHITE, 1); 573 | } 574 | if (temp_strA == "RERA") { 575 | display_item(136, line_pos, "Recent Moderate/Heavy Rain", WHITE, 1); 576 | } 577 | if (temp_strA == "REDZ") { 578 | display_item(136, line_pos, "Recent Drizzle", WHITE, 1); 579 | } 580 | if (temp_strA == "RESN") { 581 | display_item(136, line_pos, "Recent Moderate/Heavy Snow", WHITE, 1); 582 | } 583 | if (temp_strA == "RESG") { 584 | display_item(136, line_pos, "Recent Moderate/Heavy Snow grains", WHITE, 1); 585 | } 586 | if (temp_strA == "REGR") { 587 | display_item(136, line_pos, "Recent Moderate/Heavy Hail", WHITE, 1); 588 | } 589 | if (temp_strA == "RETS") { 590 | display_item(136, line_pos, "Recent Thunder storms", WHITE, 1); 591 | } 592 | } 593 | 594 | String display_conditions(String WX_state) { 595 | if (WX_state == "//") { 596 | return "No weather reported"; 597 | } 598 | if (WX_state == "VC") { 599 | return "Vicinity has"; 600 | } 601 | if (WX_state == "BL") { 602 | return "Blowing"; 603 | } 604 | if (WX_state == "SH") { 605 | return "Showers"; 606 | } 607 | if (WX_state == "TS") { 608 | return "Thunderstorms"; 609 | } 610 | if (WX_state == "FZ") { 611 | return "Freezing"; 612 | } 613 | if (WX_state == "UP") { 614 | return "Unknown"; 615 | } 616 | //---------------- 617 | if (WX_state == "MI") { 618 | return "Shallow"; 619 | } 620 | if (WX_state == "PR") { 621 | return "Partial"; 622 | } 623 | if (WX_state == "BC") { 624 | return "Patches"; 625 | } 626 | if (WX_state == "DR") { 627 | return "Low drifting"; 628 | } 629 | if (WX_state == "IC") { 630 | return "Ice crystals"; 631 | } 632 | if (WX_state == "PL") { 633 | return "Ice pellets"; 634 | } 635 | if (WX_state == "GR") { 636 | return "Hail"; 637 | } 638 | if (WX_state == "GS") { 639 | return "Small hail"; 640 | } 641 | //---------------- 642 | if (WX_state == "DZ") { 643 | return "Drizzle"; 644 | } 645 | if (WX_state == "RA") { 646 | return "Rain"; 647 | } 648 | if (WX_state == "SN") { 649 | return "Snow"; 650 | } 651 | if (WX_state == "SG") { 652 | return "Snow grains"; 653 | } 654 | if (WX_state == "DU") { 655 | return "Widespread dust"; 656 | } 657 | if (WX_state == "SA") { 658 | return "Sand"; 659 | } 660 | if (WX_state == "HZ") { 661 | return "Haze"; 662 | } 663 | if (WX_state == "PY") { 664 | return "Spray"; 665 | } 666 | //---------------- 667 | if (WX_state == "BR") { 668 | return "Mist"; 669 | } 670 | if (WX_state == "FG") { 671 | return "Fog"; 672 | } 673 | if (WX_state == "FU") { 674 | return "Smoke"; 675 | } 676 | if (WX_state == "VA") { 677 | return "Volcanic ash"; 678 | } 679 | if (WX_state == "DS") { 680 | return "Dust storm"; 681 | } 682 | if (WX_state == "PO") { 683 | return "Well developed dust/sand swirls"; 684 | } 685 | if (WX_state == "SQ") { 686 | return "Squalls"; 687 | } 688 | if (WX_state == "FC") { 689 | return "Funnel clouds/Tornadoes"; 690 | } 691 | if (WX_state == "SS") { 692 | return "Sandstorm"; 693 | } 694 | return ""; 695 | } 696 | 697 | void display_item(int x, int y, String token, int txt_colour, int txt_size) { 698 | tft.setCursor(x, y); 699 | tft.setTextColor(txt_colour); 700 | tft.setTextSize(txt_size); 701 | tft.print(token); 702 | tft.setTextSize(2); // Back to default text size 703 | } 704 | 705 | void display_item_nxy(String token, int txt_colour, int txt_size) { 706 | tft.setTextColor(txt_colour); 707 | tft.setTextSize(txt_size); 708 | tft.print(token); 709 | tft.setTextSize(2); // Back to default text size 710 | } 711 | 712 | void clear_screen() { 713 | tft.fillScreen(BLACK); 714 | } 715 | 716 | void arrow(int x1, int y1, int x2, int y2, int alength, int awidth, int colour) { 717 | float distance; 718 | int dx, dy, x2o, y2o, x3, y3, x4, y4, k; 719 | distance = sqrt(pow((x1 - x2), 2) + pow((y1 - y2), 2)); 720 | dx = x2 + (x1 - x2) * alength / distance; 721 | dy = y2 + (y1 - y2) * alength / distance; 722 | k = awidth / alength; 723 | x2o = x2 - dx; 724 | y2o = dy - y2; 725 | x3 = y2o * k + dx; 726 | y3 = x2o * k + dy; 727 | // 728 | x4 = dx - y2o * k; 729 | y4 = dy - x2o * k; 730 | tft.drawLine(x1, y1, x2, y2, colour); 731 | tft.drawLine(x1, y1, dx, dy, colour); 732 | tft.drawLine(x3, y3, x4, y4, colour); 733 | tft.drawLine(x3, y3, x2, y2, colour); 734 | tft.drawLine(x2, y2, x4, y4, colour); 735 | } 736 | 737 | void draw_veering_arrow(int a_direction) { 738 | int dx = (diameter * 0.75 * cos((a_direction - 90) * 3.14 / 180)) + centreX; 739 | int dy = (diameter * 0.75 * sin((a_direction - 90) * 3.14 / 180)) + centreY; 740 | arrow(centreX, centreY, dx, dy, 2, 5, RED); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 741 | } 742 | 743 | void Draw_Compass_Rose() { 744 | int dxo, dyo, dxi, dyi; 745 | tft.drawCircle(centreX, centreY, diameter, GREEN); // Draw compass circle 746 | // tft.fillCircle(centreX,centreY,diameter,GREY); // Draw compass circle 747 | tft.drawRoundRect((centreX - 45), (centreY - 60), (diameter + 50), (centreY + diameter / 2 + 50), 10, YELLOW); // Draw compass rose 748 | tft.drawLine(0, 105, 228, 105, YELLOW); // Seperating line for relative-humidity, temp, windchill, temp-index and dewpoint 749 | tft.drawLine(132, 105, 132, 185, YELLOW); // Seperating vertical line for relative-humidity, temp, windchill, temp-index and dewpoint 750 | for (float i = 0; i < 360; i = i + 22.5) { 751 | dxo = diameter * cos((i - 90) * 3.14 / 180); 752 | dyo = diameter * sin((i - 90) * 3.14 / 180); 753 | dxi = dxo * 0.9; 754 | dyi = dyo * 0.9; 755 | tft.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 756 | dxo = dxo * 0.5; 757 | dyo = dyo * 0.5; 758 | dxi = dxo * 0.9; 759 | dyi = dyo * 0.9; 760 | tft.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 761 | } 762 | display_item((centreX - 2), (centreY - 33), "N", GREEN, 1); 763 | display_item((centreX - 2), (centreY + 26), "S", GREEN, 1); 764 | display_item((centreX + 30), (centreY - 3), "E", GREEN, 1); 765 | display_item((centreX - 32), (centreY - 3), "W", GREEN, 1); 766 | } 767 | 768 | boolean valid_cloud_report(String temp_strA) { 769 | if (temp_strA.startsWith("BKN") || 770 | temp_strA.startsWith("CLR") || 771 | temp_strA.startsWith("FEW") || 772 | temp_strA.startsWith("NCD") || 773 | temp_strA.startsWith("NSC") || 774 | temp_strA.startsWith("OVC") || 775 | temp_strA.startsWith("SCT") || 776 | temp_strA.startsWith("SKC") || 777 | temp_strA.endsWith("CB") || 778 | temp_strA.endsWith("TCU")) { 779 | return true; 780 | } else { 781 | return false; 782 | } 783 | } 784 | 785 | void display_status() { 786 | display_progress("Initialising", 50); 787 | tft.setTextSize(2); 788 | display_progress("Waiting for IP address", 75); 789 | display_progress("Ready...", 100); 790 | clear_screen(); 791 | } 792 | 793 | void display_progress (String title, int percent) { 794 | int title_pos = (320 - title.length() * 12) / 2; // Centre title 795 | int x_pos = 35; int y_pos = 105; 796 | int bar_width = 250; int bar_height = 15; 797 | display_item(title_pos, y_pos - 20, title, GREEN, 2); 798 | tft.drawRoundRect(x_pos, y_pos, bar_width + 2, bar_height, 5, YELLOW); // Draw progress bar outline 799 | tft.fillRoundRect(x_pos + 2, y_pos + 1, percent * bar_width / 100 - 2, bar_height - 3, 4, BLUE); // Draw progress 800 | delay(2000); 801 | tft.fillRect(x_pos - 30, y_pos - 20, 320, 16, BLACK); // Clear titles 802 | } 803 | 804 | 805 | /* Meaning of various codes 806 | Moderate/heavy rain RERA 807 | Moderate/heavy snow RESN 808 | Moderate/heavy small hail REGS 809 | Moderate/heavy snow pellets REGS Moderate/heavy ice pellets REPL 810 | Moderate/heavy hail REGR 811 | Moderate/heavy snow grains RESG 812 | 813 | Intensity Description Precipitation Obscuration Other 814 | - Light MI Shallow DZ Drizzle BR Mist PO Well developed dust / sand whirls 815 | Moderate PR Partial RA Rain FG Fog SQ Squalls 816 | + Heavy BC Patches SN Snow FU Smoke FC Funnel clouds inc tornadoes or waterspouts 817 | VC Vicinity DR Low drifting SG Snow grains VA Volcanic ash SS Sandstorm 818 | BL Blowing IC Ice crystals DU Widespread dust DS Duststorm 819 | SH Showers PL Ice pellets SA Sand 820 | TS Thunderstorm GR Hail HZ Haze 821 | FZ Freezing GS Small hail PY Spray 822 | UP Unknown 823 | 824 | e.g. -SHRA - Light showers of rain 825 | TSRA - Thunderstorms and rain. 826 | */ 827 | 828 | 829 | 830 | /* Returns the following METAR data from the server address 831 | 832 | - 833 | 18793217 834 | 835 | 836 | 837 | 838 | 4 839 | - 840 | - 841 | EGLL 241950Z AUTO 07009KT 9999 NCD 03/M02 Q1023 842 | EGLL 843 | 2018-02-24T19:50:00Z 844 | 51.48 845 | -0.45 846 | 3.0 847 | -2.0 848 | 70 849 | 9 850 | 6.21 851 | 30.206694 852 | - 853 | TRUE 854 | 855 | 856 | VFR 857 | METAR 858 | 24.0 859 | 860 | - 861 | EGLL 241920Z AUTO 07008KT 9999 NCD 03/M03 Q1023 862 | EGLL 863 | 2018-02-24T19:20:00Z 864 | 51.48 865 | -0.45 866 | 3.0 867 | -3.0 868 | 70 869 | 8 870 | 6.21 871 | 30.206694 872 | - 873 | TRUE 874 | 875 | 876 | VFR 877 | METAR 878 | 24.0 879 | 880 | 881 | 882 | 883 | EGDM 121621Z 35005KT 9999 5000SW BR FEW001 BKN002 11/11 Q1014 AMB 884 | EGDM 885 | 2016-11-12T16:21:00Z 886 | 51.17 887 | -1.75 888 | 11.0 889 | 11.0 890 | 350 891 | 5 892 | 6.21 893 | 29.940945 894 | BR 895 | 896 | 897 | LIFR 898 | METAR 899 | 124.0 900 | 901 | */ 902 | -------------------------------------------------------------------------------- /ESP_METAR_DECODER_REVISED_NWS_FORMAT_V7_SSL.ino: -------------------------------------------------------------------------------- 1 | /* Version 6 METAR Decoder and display for ESP8266/ESP32 and ILI9341 TFT screen 2 | 3 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 4 | 5 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 6 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 7 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 8 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 9 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 10 | 11 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 12 | software use is visible to an end-user. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 15 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | See more at http://www.dsbird.org.uk 19 | 20 | */ 21 | //////////////////////////////////////////////////////////////////////////////////// 22 | String version_num = "METAR ESP Version 7.0"; 23 | #ifdef ESP32 24 | #include 25 | #include "HTTPClient.h" 26 | #define CS 17 // ESP32 GPIO 17 goes to TFT CS 27 | #define DC 16 // ESP32 GPIO 16 goes to TFT DC 28 | #define MOSI 23 // ESP32 GPIO 23 goes to TFT MOSI 29 | #define SCLK 18 // ESP32 GPIO 18 goes to TFT SCK/CLK 30 | #define RST 5 // ESP32 GPIO 5 ESP RST to TFT RESET 31 | #define MISO // Not connected 32 | // 3.3V // Goes to TFT LED 33 | // 5v // Goes to TFT Vcc 34 | // Gnd // Goes to TFT Gnd 35 | #else 36 | #include 37 | #include "ESP8266HTTPClient.h" 38 | #define CS D0 // Wemos D1 Mini D0 goes to TFT CS 39 | #define DC D8 // Wemos D1 Mini D8 goes to TFT DC 40 | #define MOSI D7 // Wemos D1 Mini D7 goes to TFT MOSI 41 | #define SCLK D5 // Wemos D1 Mini D5 goes to TFT SCK/CLK 42 | #define RST // Wemos D1 Mini RST on ESP goes to TFT RESET 43 | #define MISO // Wemos D1 Mini Not connected 44 | // 3.3V // Goes to TFT LED 45 | // 5v // Goes to TFT Vcc 46 | // Gnd // Goes to TFT Gnd 47 | #endif 48 | 49 | #include 50 | #include "SPI.h" 51 | #include "Adafruit_GFX.h" 52 | #include "Adafruit_ILI9341.h" 53 | 54 | // Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC 55 | Adafruit_ILI9341 tft = Adafruit_ILI9341(CS, DC); 56 | 57 | const char *ssid = "yourSSID"; 58 | const char *password = "yourPASSWORD"; 59 | const char* host = "https://aviationweather.gov"; 60 | const int httpsPort = 443; 61 | 62 | WiFiClientSecure client; 63 | 64 | const int centreX = 274; // Location of the compass display on screen 65 | const int centreY = 60; 66 | const int diameter = 41; // Size of the compass 67 | 68 | // Assign human-readable names to common 16-bit color values: 69 | #define BLACK 0x0000 70 | #define RED 0xF800 71 | #define GREEN 0x07E0 72 | #define BLUE 0x001F 73 | #define CYAN 0x07FF 74 | #define MAGENTA 0xF81F 75 | #define YELLOW 0xFFE0 76 | #define WHITE 0xFFFF 77 | 78 | void setup() { 79 | Serial.begin(115200); 80 | tft.begin(); 81 | tft.setRotation(3); 82 | clear_screen(); 83 | display_progress("Connecting to Network", 25); 84 | WiFi.begin(ssid, password); 85 | while (WiFi.status() != WL_CONNECTED) { 86 | delay(100); 87 | Serial.print("."); 88 | } 89 | Serial.println("\nWiFi connected at: " + WiFi.localIP().toString()); 90 | display_status(); 91 | } 92 | 93 | void loop() { 94 | // Change these METAR Stations to suit your needs see: Use this URL address = ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/ 95 | // to establish your list of sites to retrieve (you must know the 4-letter 96 | // site dentification) 97 | GET_METAR("EGGD", "1 EGGD Bristol/Lulsgate"); 98 | GET_METAR("EGVN", "2 EGVN Brize Norton"); 99 | GET_METAR("EGCC", "3 EGCC Manchester Airport"); 100 | GET_METAR("EGHQ", "4 EGHQ Newquay"); 101 | GET_METAR("EGSS", "5 EGSS Stansted"); 102 | } 103 | 104 | //---------------------------------------------------------------------------------------------------- 105 | void GET_METAR(String station, String Name) { //client function to send/receive GET request data. 106 | String metar, raw_metar; 107 | bool metar_status = true; 108 | const int time_delay = 20000; 109 | display_item(35, 100, "Decoding METAR", GREEN, 3); 110 | display_item(90, 135, "for " + station, GREEN, 3); 111 | // http://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 (example) 112 | // https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 113 | // ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/EGCC.TXT 114 | String uri = String(host) + "/cgi-bin/data/metar.php?dataSource=metars&requestType=retrieve&format=xml&ids=" + station + "&hoursBeforeNow=1"; 115 | //String uri = String(host) + "/adds/dataserver_current/httpparam?datasource=metars&requestType=retrieve&format=xml&mostRecentForEachStation=constraint&hoursBeforeNow=1.25&stationString=" + station; 116 | Serial.println("Connected, Requesting data for : " + Name); 117 | HTTPClient http; 118 | http.begin(uri.c_str()); // Specify the URL and maybe a certificate 119 | int httpCode = http.GET(); // Start connection and send HTTP header 120 | Serial.println("Connection status: " + String(httpCode > 0 ? "Connected": "Connection Error")); 121 | if (httpCode > 0) { // HTTP header has been sent and Server response header has been handled 122 | if (httpCode == HTTP_CODE_OK) raw_metar = http.getString(); 123 | http.end(); 124 | metar = raw_metar.substring(raw_metar.indexOf("", 0) + 10, raw_metar.indexOf("", 0)); 125 | } 126 | else 127 | { 128 | http.end(); 129 | Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); 130 | metar_status = false; 131 | metar = "Station off-air"; 132 | } 133 | display_item(265, 230, "Connected", RED, 1); 134 | Serial.println(metar); 135 | client.stop(); 136 | clear_screen(); // Clear screen 137 | display_item(265, 230, "Connected", RED, 1); 138 | display_item(0, 230, version_num, GREEN, 1); 139 | display_item(0, 190, Name, YELLOW, 2); 140 | tft.drawLine(0, 185, 320, 185, YELLOW); 141 | display_item(0, 210, metar, YELLOW, 1); 142 | if (metar_status == true) { 143 | display_metar(metar); 144 | delay(time_delay); // Delay for set time 145 | } 146 | else 147 | { 148 | display_item(70, 100, metar, YELLOW, 2); // Now decode METAR 149 | delay(5000); // Wait less time if station off-air 150 | } 151 | clear_screen(); // Clear screen before moving to next station display 152 | } 153 | 154 | void display_metar(String metar) { 155 | int temperature = 0; 156 | int dew_point = 0; 157 | int wind_speedKTS = 0; 158 | char str[130] = " "; //EGDM 261550Z AUTO 22017G23KT 9999 4000SE -SHRA SCT012/// BKN019/// BKN060/// 14/13 Q1001 RERA NOSIG BLU"; 159 | char *parameter; // a pointer variable 160 | String conditions_start = " "; 161 | String temp_strA = ""; 162 | String temp_strB = ""; 163 | String temp_Pres = ""; 164 | String conditions_test = "+-BDFGHIMPRSTUV"; // Test for light or heavy (+/-) BR - Mist, RA - Rain SH-Showers VC - Vicinity, etc 165 | // Test metar, press enter after colon to include for test):metar = "EGDM 261550Z AUTO 26517G23KT 250V290 6999 R04/1200 4200SW -SH SN SCT002TCU NCD FEW222/// SCT090 28/M15 Q1001 RERA NOSIG BLU"; 166 | // Test metar exercises all decoder functions on screen 167 | metar.toCharArray(str, 130); 168 | parameter = strtok(str, " "); // Find tokens that are seperated by SPACE 169 | while (parameter != NULL) { 170 | //display_item(0,0, String(parameter),RED,2); // Station Name - omitted at V2.01 171 | temp_strA = strtok(NULL, " "); 172 | display_item(0, 0, "Date:" + temp_strA.substring(0, 2) + " @ " + temp_strA.substring(2, 6) + "Hr", GREEN, 2); // Date-time 173 | 174 | //---------------------------------------------------------------------------------------------------- 175 | // Process any reported station type e.g. AUTO means Automatic 176 | temp_strA = strtok(NULL, " "); 177 | if (temp_strA == "AUTO") { 178 | //display_item(54,0,"A",CYAN,2); // Omitted at V2.01 179 | temp_strA = strtok(NULL, " "); 180 | } 181 | 182 | //---------------------------------------------------------------------------------------------------- 183 | // Process any reported wind direction and speed e.g. 270019KTS means direction is 270 deg and speed 19Kts 184 | // radians = (degrees * 71) / 4068 from Pi/180 to convert to degrees 185 | Draw_Compass_Rose(); // Draw compass rose 186 | if (temp_strA == "/////KT") { 187 | temp_strA = "00000KT"; 188 | } 189 | temp_strB = temp_strA.substring(3, 5); 190 | wind_speedKTS = temp_strB.toInt(); // Knots/sec 191 | int wind_speedMPH = wind_speedKTS * 1.15077 + 0.5; 192 | if (temp_strA.indexOf('G') >= 0) { 193 | temp_strB = temp_strA.substring(8); 194 | } 195 | else { 196 | temp_strB = temp_strA.substring(5); 197 | } // Now get units of wind speed either KT or MPS 198 | if (temp_strB == "MPS") { 199 | temp_strB = "MS"; 200 | } 201 | if (wind_speedMPH < 10) display_item((centreX - 28), (centreY + 50), (String(wind_speedMPH) + " MPH"), YELLOW, 2); 202 | else { 203 | display_item((centreX - 35), (centreY + 50), (String(wind_speedMPH) + " MPH"), YELLOW, 2); 204 | if (wind_speedMPH >= 18) display_item((centreX - 35), (centreY + 50), (String(wind_speedMPH) + " MPH"), RED, 2); 205 | } 206 | if (temp_strA.indexOf('G') >= 0) { 207 | tft.fillRect(centreX - 40, centreY + 48, 82, 18, BLACK); 208 | display_item((centreX - 40), (centreY + 50), String(wind_speedKTS) + "g" + temp_strA.substring(temp_strA.indexOf('G') + 1, temp_strA.indexOf('G') + 3) + temp_strB, YELLOW, 2); 209 | } 210 | int wind_direction = 0; 211 | if (temp_strA.substring(0, 3) == "VRB") { 212 | display_item((centreX - 15), (centreY - 5), "VRB", YELLOW, 2); 213 | } 214 | else { 215 | wind_direction = temp_strA.substring(0, 3).toInt() - 90; 216 | int dx = (diameter * cos((wind_direction) * 0.017444)) + centreX; 217 | int dy = (diameter * sin((wind_direction) * 0.017444)) + centreY; 218 | arrow(dx, dy, centreX, centreY, 5, 5, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 219 | } 220 | 221 | //---------------------------------------------------------------------------------------------------- 222 | // get next token, this could contain Veering information e.g. 0170V220 223 | temp_strA = strtok(NULL, " "); 224 | if (temp_strA.indexOf('V') >= 0 && temp_strA != "CAVOK") { // Check for variable wind direction 225 | int startV = temp_strA.substring(0, 3).toInt(); 226 | int endV = temp_strA.substring(4, 7).toInt(); 227 | // Minimum angle is either ABS(AngleA- AngleB) or (360-ABS(AngleA-AngleB)) 228 | int veering = min_val(360 - abs(startV - endV), abs(startV - endV)); 229 | display_item(136, 110, "V" + String(veering) + char(247), RED, 2); 230 | display_item((centreX - 40), (centreY + 30), "v", RED, 2); // Signify 'Variable wind direction 231 | draw_veering_arrow(startV); 232 | draw_veering_arrow(endV); 233 | temp_strA = strtok(NULL, " "); // Move to the next token/item 234 | } 235 | 236 | //---------------------------------------------------------------------------------------------------- 237 | // Process any reported visibility e.g. 6000 means 6000 Metres of visibility 238 | if (temp_strA == "////") { 239 | temp_strB = "No Visibility Rep."; 240 | } else temp_strB = ""; 241 | if (temp_strA == "CAVOK") { 242 | display_item(0, 20, "Visibility good", WHITE, 1); 243 | display_item(0, 30, "Conditions good", WHITE, 1); 244 | } 245 | else { 246 | if (temp_strA != "////") { 247 | if (temp_strA == "9999") { 248 | temp_strB = "Visibility excellent"; 249 | } 250 | else { 251 | String vis = temp_strA; 252 | while (vis.startsWith("0")) { 253 | vis = vis.substring(1); // trim leading '0' 254 | } 255 | temp_strB = vis + " Metres of visibility"; 256 | } 257 | } 258 | } 259 | display_item(0, 20, temp_strB, WHITE, 1); 260 | temp_strA = strtok(NULL, " "); 261 | 262 | //---------------------------------------------------------------------------------------------------- 263 | // Process any secondary Runway visibility reports e.g. R04/1200 means Runway 4, visibility 1200M 264 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 265 | // Ignore the report for now 266 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 267 | } 268 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 269 | // Ignore the report for now, there can be two! 270 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 271 | } 272 | 273 | //---------------------------------------------------------------------------------------------------- 274 | // Process any secondary reported visibility e.g. 6000S (or 6000SW) means 6000 Metres in a Southerly direction (or from SW) 275 | if (temp_strA.length() >= 5 && temp_strA.substring(0, 1) != "+" && temp_strA.substring(0, 1) != "-" && (temp_strA.endsWith("N") || temp_strA.endsWith("S") || temp_strA.endsWith("E") || temp_strA.endsWith("W")) ) { 276 | conditions_start = temp_strA.substring(4); 277 | conditions_start = conditions_start.substring(0, 1); 278 | if (conditions_start == "N" || conditions_start == "S" || conditions_start == "E" || conditions_start == "W") { 279 | tft.fillRect(25, 20, 170, 20, BLACK); 280 | display_item(25, 20, "/" + temp_strA + " Mts Visibility", WHITE, 1); 281 | } 282 | temp_strA = strtok(NULL, " "); 283 | } 284 | 285 | //---------------------------------------------------------------------------------------------------- 286 | // Process any reported weather conditions e.g. -RA means light rain 287 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 288 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV/// 289 | tft.drawRect(132, 129, 188, 12, YELLOW); 290 | display_item(136, 132, "Additional Wx Reports:", WHITE, 1); 291 | conditions_start = temp_strA.substring(0, 1); 292 | temp_strB = temp_strA.substring(0, 3); 293 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 294 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 295 | && temp_strB != "NSC" 296 | && !temp_strA.startsWith("M0")) { 297 | temp_strB = ""; 298 | if (conditions_start == "-" || conditions_start == "+") { 299 | if (conditions_start == "-") { 300 | temp_strB = "Light "; 301 | } else { 302 | temp_strB = "Heavy "; 303 | } 304 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 305 | } 306 | if (temp_strA.length() == 2) { 307 | display_item(136, 163, temp_strB + display_conditions(temp_strA), WHITE, 1); 308 | } 309 | else { 310 | display_item(136, 163, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 311 | display_item_nxy("/" + display_conditions(temp_strA.substring(2, 4)), WHITE, 1); 312 | if (temp_strA.length() >= 6) { // sometimes up to three cateries are reported 313 | display_item_nxy("/" + display_conditions(temp_strA.substring(4, 6)), WHITE, 1); 314 | } 315 | } 316 | parameter = strtok(NULL, " "); 317 | temp_strA = parameter; 318 | } 319 | 320 | // Process any reported weather conditions e.g. -RA means light rain 321 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 322 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV///(poor vertical visibility) 323 | conditions_start = temp_strA.substring(0, 1); 324 | temp_strB = temp_strA.substring(0, 3); 325 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 326 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 327 | && temp_strB != "NSC" 328 | && !temp_strA.startsWith("M0")) { 329 | temp_strB = ""; 330 | if (conditions_start == "-" || conditions_start == "+") { 331 | if (conditions_start == "-") { 332 | temp_strB = "Light "; 333 | } else { 334 | temp_strB = "Heavy "; 335 | } 336 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 337 | } 338 | if (temp_strA.length() == 2) { 339 | display_item(136, 173, temp_strB + display_conditions(temp_strA), WHITE, 1); 340 | } 341 | else { 342 | if (temp_strA.length() >= 5) { // sometimes up to three cateries are reported 343 | display_item(136, 173, "Poor Vert Visibility", WHITE, 1); 344 | } 345 | else { 346 | display_item(136, 173, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 347 | } 348 | } 349 | parameter = strtok(NULL, " "); 350 | temp_strA = parameter; 351 | } 352 | 353 | //---------------------------------------------------------------------------------------------------- 354 | // Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 355 | tft.drawLine(0, 40, 229, 40, YELLOW); 356 | if (temp_strA == "////" || temp_strA == "/////" || temp_strA == "//////") { 357 | temp_strA = "No CC Rep."; 358 | temp_strA = strtok(NULL, " "); 359 | } 360 | else 361 | { 362 | // BLOCK-1 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 363 | if (valid_cloud_report(temp_strA) || temp_strA.startsWith("VV/")) { 364 | temp_strA = convert_clouds(temp_strA); 365 | display_item(0, 45, temp_strA, WHITE, 1); 366 | temp_strA = strtok(NULL, " "); 367 | } 368 | 369 | // BLOCK-2 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 370 | if (valid_cloud_report(temp_strA)) { 371 | temp_strA = convert_clouds(temp_strA); 372 | display_item(0, 55, temp_strA, WHITE, 1); 373 | temp_strA = strtok(NULL, " "); 374 | } 375 | 376 | // BLOCK-3 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 377 | if (valid_cloud_report(temp_strA)) { 378 | temp_strA = convert_clouds(temp_strA); 379 | display_item(0, 65, temp_strA, WHITE, 1); 380 | temp_strA = strtok(NULL, " "); 381 | } 382 | 383 | // BLOCK-4 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 384 | if (valid_cloud_report(temp_strA)) { 385 | temp_strA = convert_clouds(temp_strA); 386 | display_item(0, 75, temp_strA, WHITE, 1); 387 | temp_strA = strtok(NULL, " "); 388 | } 389 | } 390 | 391 | //---------------------------------------------------------------------------------------------------- 392 | // Process any reported temperatures e.g. 14/12 means Temp 14C Dewpoint 12 393 | // Test first section of temperature/dewpoint which is 'temperature' so either 12/nn or M12/Mnn 394 | if (temp_strA.indexOf("/") <= 0) { 395 | temp_Pres = temp_strA; 396 | temp_strA = "00/00"; 397 | } // Ignore no temp reports and save the next message, which is air pressure for later processing 398 | String temp_sign = ""; 399 | if (temp_strA.startsWith("M")) { 400 | temperature = 0 - temp_strA.substring(1, 3).toInt(); 401 | if (temperature == 0) { 402 | temp_sign = "-"; // Reports of M00, meaning between -0.1 and -0.9C 403 | } 404 | } 405 | else { 406 | temperature = temp_strA.substring(0, 2).toInt(); 407 | if (temperature >= 0 && temperature < 10) { 408 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 409 | } 410 | } 411 | if (temperature <= 25) { 412 | display_item(0, 110, " Temp " + temp_sign + String(temperature) + char(247) + "C", CYAN, 2); 413 | } 414 | else { 415 | display_item(0, 110, " Temp " + temp_sign + String(temperature) + char(247) + "C", RED, 2); 416 | } 417 | 418 | //---------------------------------------------------------------------------------------------------- 419 | // Test second section of temperature/dewpoint which is 'dewpoint' so either nn/04 or Mnn/M04 420 | temp_strB = temp_strA.substring(temp_strA.indexOf('/') + 1); 421 | if (temp_strB.startsWith("M")) { 422 | dew_point = 0 - temp_strB.substring(1, 3).toInt(); 423 | } 424 | else { 425 | dew_point = temp_strB.substring(0, 2).toInt(); 426 | } 427 | 428 | //---------------------------------------------------------------------------------------------------- 429 | // Don't display windchill unless wind speed is greater than 3 MPH and temperature is less than 14'C 430 | if (wind_speedKTS > 3 && temperature <= 14) { 431 | temp_sign = ""; 432 | int wind_chill = int(calc_windchill(temperature, wind_speedKTS)); 433 | if (wind_chill >= 0 && wind_chill < 10) { 434 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 435 | } 436 | display_item(0, 164, "WindC " + temp_sign + String(wind_chill) + char(247) + "C", CYAN, 2); 437 | } 438 | 439 | //---------------------------------------------------------------------------------------------------- 440 | // Calculate and display Relative Humidity 441 | temp_sign = ""; 442 | if (dew_point >= 0 && dew_point < 10) { 443 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 444 | } 445 | display_item(0, 127, " Dewp " + temp_sign + String(dew_point) + char(247) + "C", CYAN, 2); 446 | int RH = calc_rh(temperature, dew_point); 447 | display_item(00, 146, "Rel.H " + String(RH) + "%", CYAN, 2); 448 | 449 | //---------------------------------------------------------------------------------------------------- 450 | // Don't display heatindex unless temperature > 18C 451 | if (temperature >= 20) { 452 | float T = (temperature * 9 / 5) + 32; 453 | float RHx = RH; 454 | int tHI = (-42.379 + (2.04901523 * T) + (10.14333127 * RHx) - (0.22475541 * T * RHx) - (0.00683783 * T * T) - (0.05481717 * RHx * RHx) + (0.00122874 * T * T * RHx) + (0.00085282 * T * RHx * RHx) - (0.00000199 * T * T * RHx * RHx) - 32 ) * 5 / 9; 455 | display_item(0, 164, "HeatX " + String(tHI) + char(247) + "C", CYAN, 2); 456 | } 457 | //where HI = -42.379 + 2.04901523*T + 10.14333127*RH - 0.22475541*T*RH - 0.00683783*T*T - 0.05481717*RH*RH + 0.00122874*T*T*RH + 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH 458 | //tHI = heat index (oF) 459 | //t = air temperature (oF) (t > 57oF) 460 | //φ = relative humidity (%) 461 | //---------------------------------------------------------------------------------------------------- 462 | //Process air pressure (QNH) e.g. Q1018 means 1018mB 463 | temp_strA = strtok(NULL, " "); 464 | temp_strA.trim(); 465 | if (temp_Pres.startsWith("Q")) temp_strA = temp_Pres; // Pickup a temporary copy of air pressure, found when no temp broadcast 466 | if (temp_strA.startsWith("Q")) { 467 | if (temp_strA.substring(1, 2) == "0") { 468 | temp_strA = " " + temp_strA.substring(2); 469 | } 470 | else { 471 | temp_strA = temp_strA.substring(1); 472 | } 473 | display_item((centreX - 35), (centreY - 57), temp_strA + "mB", YELLOW, 2); 474 | } 475 | 476 | //---------------------------------------------------------------------------------------------------- 477 | // Now process up to two secondary weather reports 478 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 479 | Process_secondary_reports(temp_strA, 143); 480 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 481 | Process_secondary_reports(temp_strA, 153); 482 | parameter = NULL; // Reset the 'str' pointer, so end of report decoding 483 | } 484 | } // finished decoding the METAR string 485 | //---------------------------------------------------------------------------------------------------- 486 | //---------------------------------------------------------------------------------------------------- 487 | 488 | String convert_clouds(String source) { 489 | String height = source.substring(3, 6); 490 | String cloud = source.substring(0, 3); 491 | String warning = " "; 492 | while (height.startsWith("0")) { 493 | height = height.substring(1); // trim leading '0' 494 | } 495 | if (source.endsWith("TCU") || source.endsWith("CB")) { 496 | display_item(0, 95, "Warning - storm clouds detected", WHITE, 1); 497 | warning = " (storm) "; 498 | } 499 | // 'adjust offset if 0 replaced by space 500 | if (cloud != "SKC" && cloud != "CLR" && height != " ") { 501 | height = " at " + height + "00ft"; 502 | } else height = ""; 503 | if (source == "VV///") { 504 | return "No cloud reported"; 505 | } 506 | if (cloud == "BKN") { 507 | return "Broken" + warning + "clouds" + height; 508 | } 509 | if (cloud == "SKC") { 510 | return "Clear skies"; 511 | } 512 | if (cloud == "FEW") { 513 | return "Few" + warning + "clouds" + height; 514 | } 515 | if (cloud == "NCD") { 516 | return "No clouds detected"; 517 | } 518 | if (cloud == "NSC") { 519 | return "No signficiant clouds"; 520 | } 521 | if (cloud == "OVC") { 522 | return "Overcast" + warning + height; 523 | } 524 | if (cloud == "SCT") { 525 | return "Scattered" + warning + "clouds" + height; 526 | } 527 | return ""; 528 | } 529 | 530 | 531 | int min_val(int num1, int num2) { 532 | if (num1 > num2) 533 | { 534 | return num2; 535 | } 536 | else 537 | { 538 | return num1; 539 | } 540 | } 541 | 542 | float calc_rh(int temp, int dewp) { 543 | return 100 * (exp((17.271 * dewp) / (237.7 + dewp))) / (exp((17.271 * temp) / (237.7 + temp))) + 0.5; 544 | } 545 | 546 | float calc_windchill(int temperature, int wind_speed) { 547 | float result; 548 | // Derived from wind_chill = 13.12 + 0.6215 * Tair - 11.37 * POWER(wind_speed,0.16)+0.3965 * Tair * POWER(wind_speed,0.16) 549 | wind_speed = wind_speed * 1.852; // Convert to Kph 550 | result = 13.12 + 0.6215 * temperature - 11.37 * pow(wind_speed, 0.16) + 0.3965 * temperature * pow(wind_speed, 0.16); 551 | if (result < 0 ) { 552 | return result - 0.5; 553 | } else { 554 | return result + 0.5; 555 | } 556 | } 557 | 558 | void Process_secondary_reports(String temp_strA, int line_pos) { 559 | temp_strA.trim(); 560 | if (temp_strA == "NOSIG") { 561 | display_item(136, line_pos, "No significant change expected", WHITE, 1); 562 | } 563 | if (temp_strA == "TEMPO") { 564 | display_item(136, line_pos, "Temporary conditions expected", WHITE, 1); 565 | } 566 | if (temp_strA == "RADZ") { 567 | display_item(136, line_pos, "Recent Rain/Drizzle", WHITE, 1); 568 | } 569 | if (temp_strA == "RERA") { 570 | display_item(136, line_pos, "Recent Moderate/Heavy Rain", WHITE, 1); 571 | } 572 | if (temp_strA == "REDZ") { 573 | display_item(136, line_pos, "Recent Drizzle", WHITE, 1); 574 | } 575 | if (temp_strA == "RESN") { 576 | display_item(136, line_pos, "Recent Moderate/Heavy Snow", WHITE, 1); 577 | } 578 | if (temp_strA == "RESG") { 579 | display_item(136, line_pos, "Recent Moderate/Heavy Snow grains", WHITE, 1); 580 | } 581 | if (temp_strA == "REGR") { 582 | display_item(136, line_pos, "Recent Moderate/Heavy Hail", WHITE, 1); 583 | } 584 | if (temp_strA == "RETS") { 585 | display_item(136, line_pos, "Recent Thunder storms", WHITE, 1); 586 | } 587 | } 588 | 589 | String display_conditions(String WX_state) { 590 | if (WX_state == "//") { 591 | return "No weather reported"; 592 | } 593 | if (WX_state == "VC") { 594 | return "Vicinity has"; 595 | } 596 | if (WX_state == "BL") { 597 | return "Blowing"; 598 | } 599 | if (WX_state == "SH") { 600 | return "Showers"; 601 | } 602 | if (WX_state == "TS") { 603 | return "Thunderstorms"; 604 | } 605 | if (WX_state == "FZ") { 606 | return "Freezing"; 607 | } 608 | if (WX_state == "UP") { 609 | return "Unknown"; 610 | } 611 | //---------------- 612 | if (WX_state == "MI") { 613 | return "Shallow"; 614 | } 615 | if (WX_state == "PR") { 616 | return "Partial"; 617 | } 618 | if (WX_state == "BC") { 619 | return "Patches"; 620 | } 621 | if (WX_state == "DR") { 622 | return "Low drifting"; 623 | } 624 | if (WX_state == "IC") { 625 | return "Ice crystals"; 626 | } 627 | if (WX_state == "PL") { 628 | return "Ice pellets"; 629 | } 630 | if (WX_state == "GR") { 631 | return "Hail"; 632 | } 633 | if (WX_state == "GS") { 634 | return "Small hail"; 635 | } 636 | //---------------- 637 | if (WX_state == "DZ") { 638 | return "Drizzle"; 639 | } 640 | if (WX_state == "RA") { 641 | return "Rain"; 642 | } 643 | if (WX_state == "SN") { 644 | return "Snow"; 645 | } 646 | if (WX_state == "SG") { 647 | return "Snow grains"; 648 | } 649 | if (WX_state == "DU") { 650 | return "Widespread dust"; 651 | } 652 | if (WX_state == "SA") { 653 | return "Sand"; 654 | } 655 | if (WX_state == "HZ") { 656 | return "Haze"; 657 | } 658 | if (WX_state == "PY") { 659 | return "Spray"; 660 | } 661 | //---------------- 662 | if (WX_state == "BR") { 663 | return "Mist"; 664 | } 665 | if (WX_state == "FG") { 666 | return "Fog"; 667 | } 668 | if (WX_state == "FU") { 669 | return "Smoke"; 670 | } 671 | if (WX_state == "VA") { 672 | return "Volcanic ash"; 673 | } 674 | if (WX_state == "DS") { 675 | return "Dust storm"; 676 | } 677 | if (WX_state == "PO") { 678 | return "Well developed dust/sand swirls"; 679 | } 680 | if (WX_state == "SQ") { 681 | return "Squalls"; 682 | } 683 | if (WX_state == "FC") { 684 | return "Funnel clouds/Tornadoes"; 685 | } 686 | if (WX_state == "SS") { 687 | return "Sandstorm"; 688 | } 689 | return ""; 690 | } 691 | 692 | void display_item(int x, int y, String token, int txt_colour, int txt_size) { 693 | tft.setCursor(x, y); 694 | tft.setTextColor(txt_colour); 695 | tft.setTextSize(txt_size); 696 | tft.print(token); 697 | tft.setTextSize(2); // Back to default text size 698 | } 699 | 700 | void display_item_nxy(String token, int txt_colour, int txt_size) { 701 | tft.setTextColor(txt_colour); 702 | tft.setTextSize(txt_size); 703 | tft.print(token); 704 | tft.setTextSize(2); // Back to default text size 705 | } 706 | 707 | void clear_screen() { 708 | tft.fillScreen(BLACK); 709 | } 710 | 711 | void arrow(int x1, int y1, int x2, int y2, int alength, int awidth, int colour) { 712 | float distance; 713 | int dx, dy, x2o, y2o, x3, y3, x4, y4, k; 714 | distance = sqrt(pow((x1 - x2), 2) + pow((y1 - y2), 2)); 715 | dx = x2 + (x1 - x2) * alength / distance; 716 | dy = y2 + (y1 - y2) * alength / distance; 717 | k = awidth / alength; 718 | x2o = x2 - dx; 719 | y2o = dy - y2; 720 | x3 = y2o * k + dx; 721 | y3 = x2o * k + dy; 722 | // 723 | x4 = dx - y2o * k; 724 | y4 = dy - x2o * k; 725 | tft.drawLine(x1, y1, x2, y2, colour); 726 | tft.drawLine(x1, y1, dx, dy, colour); 727 | tft.drawLine(x3, y3, x4, y4, colour); 728 | tft.drawLine(x3, y3, x2, y2, colour); 729 | tft.drawLine(x2, y2, x4, y4, colour); 730 | } 731 | 732 | void draw_veering_arrow(int a_direction) { 733 | int dx = (diameter * 0.75 * cos((a_direction - 90) * 3.14 / 180)) + centreX; 734 | int dy = (diameter * 0.75 * sin((a_direction - 90) * 3.14 / 180)) + centreY; 735 | arrow(centreX, centreY, dx, dy, 2, 5, RED); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 736 | } 737 | 738 | void Draw_Compass_Rose() { 739 | int dxo, dyo, dxi, dyi; 740 | tft.drawCircle(centreX, centreY, diameter, GREEN); // Draw compass circle 741 | // tft.fillCircle(centreX,centreY,diameter,GREY); // Draw compass circle 742 | tft.drawRoundRect((centreX - 45), (centreY - 60), (diameter + 50), (centreY + diameter / 2 + 50), 10, YELLOW); // Draw compass rose 743 | tft.drawLine(0, 105, 228, 105, YELLOW); // Seperating line for relative-humidity, temp, windchill, temp-index and dewpoint 744 | tft.drawLine(132, 105, 132, 185, YELLOW); // Seperating vertical line for relative-humidity, temp, windchill, temp-index and dewpoint 745 | for (float i = 0; i < 360; i = i + 22.5) { 746 | dxo = diameter * cos((i - 90) * 3.14 / 180); 747 | dyo = diameter * sin((i - 90) * 3.14 / 180); 748 | dxi = dxo * 0.9; 749 | dyi = dyo * 0.9; 750 | tft.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 751 | dxo = dxo * 0.5; 752 | dyo = dyo * 0.5; 753 | dxi = dxo * 0.9; 754 | dyi = dyo * 0.9; 755 | tft.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 756 | } 757 | display_item((centreX - 2), (centreY - 33), "N", GREEN, 1); 758 | display_item((centreX - 2), (centreY + 26), "S", GREEN, 1); 759 | display_item((centreX + 30), (centreY - 3), "E", GREEN, 1); 760 | display_item((centreX - 32), (centreY - 3), "W", GREEN, 1); 761 | } 762 | 763 | boolean valid_cloud_report(String temp_strA) { 764 | if (temp_strA.startsWith("BKN") || 765 | temp_strA.startsWith("CLR") || 766 | temp_strA.startsWith("FEW") || 767 | temp_strA.startsWith("NCD") || 768 | temp_strA.startsWith("NSC") || 769 | temp_strA.startsWith("OVC") || 770 | temp_strA.startsWith("SCT") || 771 | temp_strA.startsWith("SKC") || 772 | temp_strA.endsWith("CB") || 773 | temp_strA.endsWith("TCU")) { 774 | return true; 775 | } else { 776 | return false; 777 | } 778 | } 779 | 780 | void display_status() { 781 | display_progress("Initialising", 50); 782 | tft.setTextSize(2); 783 | display_progress("Waiting for IP address", 75); 784 | display_progress("Ready...", 100); 785 | clear_screen(); 786 | } 787 | 788 | void display_progress (String title, int percent) { 789 | int title_pos = (320 - title.length() * 12) / 2; // Centre title 790 | int x_pos = 35; int y_pos = 105; 791 | int bar_width = 250; int bar_height = 15; 792 | display_item(title_pos, y_pos - 20, title, GREEN, 2); 793 | tft.drawRoundRect(x_pos, y_pos, bar_width + 2, bar_height, 5, YELLOW); // Draw progress bar outline 794 | tft.fillRoundRect(x_pos + 2, y_pos + 1, percent * bar_width / 100 - 2, bar_height - 3, 4, BLUE); // Draw progress 795 | delay(2000); 796 | tft.fillRect(x_pos - 30, y_pos - 20, 320, 16, BLACK); // Clear titles 797 | } 798 | 799 | /* Meaning of various codes 800 | Moderate/heavy rain RERA 801 | Moderate/heavy snow RESN 802 | Moderate/heavy small hail REGS 803 | Moderate/heavy snow pellets REGS Moderate/heavy ice pellets REPL 804 | Moderate/heavy hail REGR 805 | Moderate/heavy snow grains RESG 806 | 807 | Intensity Description Precipitation Obscuration Other 808 | - Light MI Shallow DZ Drizzle BR Mist PO Well developed dust / sand whirls 809 | Moderate PR Partial RA Rain FG Fog SQ Squalls 810 | + Heavy BC Patches SN Snow FU Smoke FC Funnel clouds inc tornadoes or waterspouts 811 | VC Vicinity DR Low drifting SG Snow grains VA Volcanic ash SS Sandstorm 812 | BL Blowing IC Ice crystals DU Widespread dust DS Duststorm 813 | SH Showers PL Ice pellets SA Sand 814 | TS Thunderstorm GR Hail HZ Haze 815 | FZ Freezing GS Small hail PY Spray 816 | UP Unknown 817 | 818 | e.g. -SHRA - Light showers of rain 819 | TSRA - Thunderstorms and rain. 820 | */ 821 | 822 | 823 | 824 | /* Returns the following METAR data from the server address 825 | 826 | - 827 | 18793217 828 | 829 | 830 | 831 | 832 | 4 833 | - 834 | - 835 | EGLL 241950Z AUTO 07009KT 9999 NCD 03/M02 Q1023 836 | EGLL 837 | 2018-02-24T19:50:00Z 838 | 51.48 839 | -0.45 840 | 3.0 841 | -2.0 842 | 70 843 | 9 844 | 6.21 845 | 30.206694 846 | - 847 | TRUE 848 | 849 | 850 | VFR 851 | METAR 852 | 24.0 853 | 854 | - 855 | EGLL 241920Z AUTO 07008KT 9999 NCD 03/M03 Q1023 856 | EGLL 857 | 2018-02-24T19:20:00Z 858 | 51.48 859 | -0.45 860 | 3.0 861 | -3.0 862 | 70 863 | 8 864 | 6.21 865 | 30.206694 866 | - 867 | TRUE 868 | 869 | 870 | VFR 871 | METAR 872 | 24.0 873 | 874 | 875 | 876 | 877 | EGDM 121621Z 35005KT 9999 5000SW BR FEW001 BKN002 11/11 Q1014 AMB 878 | EGDM 879 | 2016-11-12T16:21:00Z 880 | 51.17 881 | -1.75 882 | 11.0 883 | 11.0 884 | 350 885 | 5 886 | 6.21 887 | 29.940945 888 | BR 889 | 890 | 891 | LIFR 892 | METAR 893 | 124.0 894 | 895 | */ 896 | -------------------------------------------------------------------------------- /Licence.txt: -------------------------------------------------------------------------------- 1 | This software, the ideas and concepts is Copyright (c) David Bird 2014 and beyond. 2 | 3 | All rights to this software are reserved. 4 | 5 | It is prohibited to redistribute or reproduce of any part or all of the software contents in any form other than the following: 6 | 7 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 8 | 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 11 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 12 | 13 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 14 | 15 | 5. You MUST include all of this copyright and permission notice ('as annotated') and this shall be included in all copies or substantial portions of the software and where the software use is visible to an end-user. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. 18 | 19 | FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | 21 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /M5_ESP32_METAR_DECODER_REVISED_NWS_FORMAT_V51_SSL.ino: -------------------------------------------------------------------------------- 1 | /* Version 5.1 METAR Decoder and display for ESp8266/ESP32 and ILI9341 TFT screen 2 | * Improved WiFi connection reliability with the M5-Stack 3 | * 4 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 5 | 6 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 7 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 8 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 9 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 10 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 11 | 12 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 13 | software use is visible to an end-user. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 16 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | See more at http://www.dsbird.org.uk 20 | * 21 | */ 22 | //////////////////////////////////////////////////////////////////////////////////// 23 | String version_num = "METAR ESP Version 5.1"; 24 | #include 25 | #include 26 | #include 27 | 28 | const char *ssid = "your_SSID"; 29 | const char *password = "your_PASSWORD"; 30 | const char* host = "aviationweather.gov"; 31 | const int httpsPort = 443; 32 | 33 | WiFiClientSecure client; 34 | 35 | const int centreX = 274; // Location of the compass display on screen 36 | const int centreY = 60; 37 | const int diameter = 41; // Size of the compass 38 | 39 | // Assign human-readable names to common 16-bit color values: 40 | #define BLACK 0x0000 41 | #define RED 0xF800 42 | #define GREEN 0x07E0 43 | #define BLUE 0x001F 44 | #define CYAN 0x07FF 45 | #define MAGENTA 0xF81F 46 | #define YELLOW 0xFFE0 47 | #define WHITE 0xFFFF 48 | 49 | void setup(){ 50 | Serial.begin(115200); 51 | M5.begin(); 52 | clear_screen(); 53 | display_progress("Connecting to Network",25); 54 | StartWiFi(ssid, password); 55 | display_status(); 56 | } 57 | 58 | void loop(){ 59 | // Change these METAR Stations to suit your needs see: Use this URL address = ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/ 60 | // to establish your list of sites to retrieve (you must know the 4-letter 61 | // site dentification) 62 | GET_METAR("EGGD", "1 EGGD Bristol/Lulsgate"); 63 | GET_METAR("EGVN", "2 EGVN Brize Norton"); 64 | GET_METAR("EGCC", "3 EGCC Manchester Airport"); 65 | GET_METAR("EGHQ", "4 EGHQ Newquay"); 66 | GET_METAR("EGSS", "5 EGSS Stansted"); 67 | } 68 | 69 | //---------------------------------------------------------------------------------------------------- 70 | void GET_METAR(String station, String Name) { //client function to send/receive GET request data. 71 | String metar = " "; 72 | bool metar_status = false; 73 | const int time_delay = 60000; 74 | display_item(35,100,"Decoding METAR",GREEN,3); 75 | display_item(90,135,"for "+station,GREEN,3); 76 | if (!client.connect(host, httpsPort)) { 77 | Serial.println("Connection failed"); 78 | delay(1000); 79 | if (!client.connect(host, httpsPort)) Serial.println("Connection failed again..."); 80 | } 81 | // http://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 (example) 82 | // https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 83 | // ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/EGCC.TXT 84 | display_item(265,230,"Connected",RED,1); 85 | String url = "/cgi-bin/data/metar.php?dataSource=metars&requestType=retrieve&format=xml&ids=" + station + "&hoursBeforeNow=1"; 86 | //String url = "/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString="+station+"&hoursBeforeNow=1"; 87 | Serial.println("Requesting data for : "+Name); 88 | client.print(String("GET ") + url + " HTTP/1.1\r\n" + 89 | "Host: " + host + "\r\n" + 90 | "Content-Type: text/html; charset=utf-8\r\n" + 91 | "Access-Control-Max-Age: 10\r\n" + 92 | "User-Agent: Mozilla/5.0\r\n" + 93 | "Connection: close\r\n\r\n"); 94 | // *See Annex for a detailed response example: 95 | metar_status = client.find(""); 96 | client.find(""); 97 | if (metar_status == true) metar = client.readStringUntil('<'); else metar = "Station off-air"; 98 | Serial.println(metar); 99 | client.stop(); 100 | clear_screen(); // Clear screen 101 | display_item(265,230,"Connected",RED,1); 102 | display_item(0,230,version_num,GREEN,1); 103 | display_item(0,190,Name,YELLOW,2); 104 | M5.Lcd.drawLine(0,185,320,185,YELLOW); 105 | display_item(0,210,metar,YELLOW,1); 106 | if (metar_status == true) { 107 | display_metar(metar); 108 | delay(time_delay); // Delay for set time 109 | } 110 | else 111 | { 112 | display_item(70,100,metar,YELLOW,2); // Now decode METAR which at this point will be 'Station off-air' 113 | delay(5000); // Wait less time if station off-air 114 | } 115 | clear_screen(); // Clear screen before moving to next station display 116 | } 117 | 118 | void display_metar(String metar) { 119 | int temperature = 0; 120 | int dew_point = 0; 121 | int wind_speedKTS = 0; 122 | char str[130] = " "; //EGDM 261550Z AUTO 22017G23KT 9999 4000SE -SHRA SCT012/// BKN019/// BKN060/// 14/13 Q1001 RERA NOSIG BLU"; 123 | char *parameter; // a pointer variable 124 | String conditions_start = " "; 125 | String temp_strA = ""; 126 | String temp_strB = ""; 127 | String temp_Pres = ""; 128 | String conditions_test = "+-BDFGHIMPRSTUV"; // Test for light or heavy (+/-) BR - Mist, RA - Rain SH-Showers VC - Vicinity, etc 129 | // Test metar, press enter after colon to include for test):metar = "EGDM 261550Z AUTO 26517G23KT 250V290 6999 R04/1200 4200SW -SH SN SCT002TCU NCD FEW222/// SCT090 28/M15 Q1001 RERA NOSIG BLU"; 130 | // Test metar exercises all decoder functions on screen 131 | metar.toCharArray(str,130); 132 | parameter = strtok(str," "); // Find tokens that are seperated by SPACE 133 | while (parameter != NULL) { 134 | //display_item(0,0, String(parameter),RED,2); // Station Name - omitted at V2.01 135 | temp_strA = strtok(NULL," "); 136 | display_item(0,0, "Date:" + temp_strA.substring(0,2) + " @ " + temp_strA.substring(2,6)+"Hr",GREEN,2); // Date-time 137 | 138 | //---------------------------------------------------------------------------------------------------- 139 | // Process any reported station type e.g. AUTO means Automatic 140 | temp_strA = strtok(NULL," "); 141 | if (temp_strA == "AUTO") { 142 | //display_item(54,0,"A",CYAN,2); // Omitted at V2.01 143 | temp_strA = strtok(NULL," "); 144 | } 145 | 146 | //---------------------------------------------------------------------------------------------------- 147 | // Process any reported wind direction and speed e.g. 270019KTS means direction is 270 deg and speed 19Kts 148 | // radians = (degrees * 71) / 4068 from Pi/180 to convert to degrees 149 | Draw_Compass_Rose(); // Draw compass rose 150 | if (temp_strA == "/////KT") {temp_strA = "00000KT";} 151 | temp_strB = temp_strA.substring(3,5); 152 | wind_speedKTS = temp_strB.toInt(); // Knots/sec 153 | int wind_speedMPH = wind_speedKTS * 1.15077 + 0.5; 154 | if (temp_strA.indexOf('G') >= 0) { 155 | temp_strB = temp_strA.substring(8); 156 | } 157 | else { 158 | temp_strB = temp_strA.substring(5); 159 | } // Now get units of wind speed either KT or MPS 160 | if (temp_strB == "MPS") { 161 | temp_strB = "MS"; 162 | } 163 | if (wind_speedMPH < 10) display_item((centreX-28),(centreY+50),(String(wind_speedMPH)+" MPH"),YELLOW,2); 164 | else { 165 | display_item((centreX-35),(centreY+50),(String(wind_speedMPH)+" MPH"),YELLOW,2); 166 | if (wind_speedMPH >= 18) display_item((centreX-35),(centreY+50),(String(wind_speedMPH)+" MPH"),RED,2); 167 | } 168 | if (temp_strA.indexOf('G') >= 0) { 169 | M5.Lcd.fillRect(centreX-40,centreY+48,82,18,BLACK); 170 | display_item((centreX-40),(centreY+50),String(wind_speedKTS)+"g"+temp_strA.substring(temp_strA.indexOf('G')+1,temp_strA.indexOf('G')+3)+temp_strB,YELLOW,2); 171 | } 172 | int wind_direction = 0; 173 | if (temp_strA.substring(0,3) == "VRB") { 174 | display_item((centreX-15),(centreY-5),"VRB",YELLOW,2); 175 | } 176 | else { 177 | wind_direction = temp_strA.substring(0,3).toInt() - 90; 178 | int dx = (diameter * cos((wind_direction)*0.017444)) + centreX; 179 | int dy = (diameter * sin((wind_direction)*0.017444)) + centreY; 180 | arrow(dx,dy,centreX, centreY, 5, 5,YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 181 | } 182 | 183 | //---------------------------------------------------------------------------------------------------- 184 | // get next token, this could contain Veering information e.g. 0170V220 185 | temp_strA = strtok(NULL," "); 186 | if (temp_strA.indexOf('V') >= 0 && temp_strA != "CAVOK") { // Check for variable wind direction 187 | int startV = temp_strA.substring(0,3).toInt(); 188 | int endV = temp_strA.substring(4,7).toInt(); 189 | // Minimum angle is either ABS(AngleA- AngleB) or (360-ABS(AngleA-AngleB)) 190 | int veering = min_val(360 - abs(startV - endV), abs(startV - endV)); 191 | display_item(136,110,"V"+String(veering)+char(247),RED,2); 192 | display_item((centreX-40),(centreY+30),"v",RED,2); // Signify 'Variable wind direction 193 | draw_veering_arrow(startV); 194 | draw_veering_arrow(endV); 195 | temp_strA = strtok(NULL," "); // Move to the next token/item 196 | } 197 | 198 | //---------------------------------------------------------------------------------------------------- 199 | // Process any reported visibility e.g. 6000 means 6000 Metres of visibility 200 | if (temp_strA == "////") { 201 | temp_strB= "No Visibility Rep."; 202 | } else temp_strB = ""; 203 | if (temp_strA == "CAVOK") { 204 | display_item(0,20,"Visibility good",WHITE,1); 205 | display_item(0,30,"Conditions good",WHITE,1); 206 | } 207 | else { 208 | if (temp_strA != "////") { 209 | if (temp_strA == "9999") {temp_strB = "Visibility excellent"; } 210 | else { 211 | String vis = temp_strA; 212 | while (vis.startsWith("0")) {vis = vis.substring(1);} // trim leading '0' 213 | temp_strB = vis + " Metres of visibility"; 214 | } 215 | } 216 | } 217 | display_item(0,20,temp_strB,WHITE,1); 218 | temp_strA = strtok(NULL," "); 219 | 220 | //---------------------------------------------------------------------------------------------------- 221 | // Process any secondary Runway visibility reports e.g. R04/1200 means Runway 4, visibility 1200M 222 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 223 | // Ignore the report for now 224 | temp_strA = strtok(NULL," "); // If there was a Variable report, move to the next item 225 | } 226 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 227 | // Ignore the report for now, there can be two! 228 | temp_strA = strtok(NULL," "); // If there was a Variable report, move to the next item 229 | } 230 | 231 | //---------------------------------------------------------------------------------------------------- 232 | // Process any secondary reported visibility e.g. 6000S (or 6000SW) means 6000 Metres in a Southerly direction (or from SW) 233 | if (temp_strA.length() >= 5 && temp_strA.substring(0,1)!= "+" && temp_strA.substring(0,1)!= "-" && (temp_strA.endsWith("N") || temp_strA.endsWith("S") || temp_strA.endsWith("E") || temp_strA.endsWith("W")) ) { 234 | conditions_start = temp_strA.substring(4); 235 | conditions_start = conditions_start.substring(0,1); 236 | if (conditions_start == "N" || conditions_start == "S" || conditions_start == "E" || conditions_start == "W") { 237 | M5.Lcd.fillRect(25,20,170,20,BLACK); 238 | display_item(25,20,"/"+temp_strA+" Mts Visibility",WHITE,1); 239 | } 240 | temp_strA = strtok(NULL," "); 241 | } 242 | 243 | //---------------------------------------------------------------------------------------------------- 244 | // Process any reported weather conditions e.g. -RA means light rain 245 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 246 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV/// 247 | M5.Lcd.drawRect(132,129,188,12,YELLOW); 248 | display_item(136,132,"Additional Wx Reports:",WHITE,1); 249 | conditions_start = temp_strA.substring(0,1); 250 | temp_strB = temp_strA.substring(0,3); 251 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 252 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 253 | && temp_strB != "NSC" 254 | && !temp_strA.startsWith("M0")) { 255 | temp_strB = ""; 256 | if (conditions_start == "-" || conditions_start == "+") { 257 | if (conditions_start == "-") {temp_strB = "Light "; } else { temp_strB = "Heavy "; } 258 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 259 | } 260 | if (temp_strA.length() == 2) { display_item(136,163,temp_strB + display_conditions(temp_strA),WHITE,1);} 261 | else { 262 | display_item(136,163,temp_strB + display_conditions(temp_strA.substring(0,2)),WHITE,1); 263 | display_item_nxy("/" + display_conditions(temp_strA.substring(2,4)),WHITE,1); 264 | if (temp_strA.length() >= 6) { // sometimes up to three cateries are reported 265 | display_item_nxy("/" + display_conditions(temp_strA.substring(4,6)),WHITE,1); 266 | } 267 | } 268 | parameter = strtok(NULL," "); 269 | temp_strA = parameter; 270 | } 271 | 272 | // Process any reported weather conditions e.g. -RA means light rain 273 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 274 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV///(poor vertical visibility) 275 | conditions_start = temp_strA.substring(0,1); 276 | temp_strB = temp_strA.substring(0,3); 277 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 278 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 279 | && temp_strB != "NSC" 280 | && !temp_strA.startsWith("M0")) { 281 | temp_strB = ""; 282 | if (conditions_start == "-" || conditions_start == "+") { 283 | if (conditions_start == "-") {temp_strB = "Light "; } else { temp_strB = "Heavy "; } 284 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 285 | } 286 | if (temp_strA.length() == 2) { display_item(136,173,temp_strB + display_conditions(temp_strA),WHITE,1);} 287 | else { 288 | if (temp_strA.length() >= 5) { // sometimes up to three cateries are reported 289 | display_item(136,173,"Poor Vert Visibility",WHITE,1); 290 | } 291 | else { 292 | display_item(136,173,temp_strB + display_conditions(temp_strA.substring(0,2)),WHITE,1); 293 | } 294 | } 295 | parameter = strtok(NULL," "); 296 | temp_strA = parameter; 297 | } 298 | 299 | //---------------------------------------------------------------------------------------------------- 300 | // Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 301 | M5.Lcd.drawLine(0,40,229,40,YELLOW); 302 | if (temp_strA == "////" || temp_strA == "/////" || temp_strA == "//////") { 303 | temp_strA = "No CC Rep."; 304 | temp_strA = strtok(NULL, " "); 305 | } 306 | else 307 | { 308 | // BLOCK-1 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 309 | if (valid_cloud_report(temp_strA) || temp_strA.startsWith("VV/")) { 310 | temp_strA = convert_clouds(temp_strA); 311 | display_item(0, 45, temp_strA, WHITE, 1); 312 | temp_strA = strtok(NULL, " "); 313 | } 314 | 315 | // BLOCK-2 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 316 | if (valid_cloud_report(temp_strA)) { 317 | temp_strA = convert_clouds(temp_strA); 318 | display_item(0, 55, temp_strA, WHITE, 1); 319 | temp_strA = strtok(NULL, " "); 320 | } 321 | 322 | // BLOCK-3 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 323 | if (valid_cloud_report(temp_strA)) { 324 | temp_strA = convert_clouds(temp_strA); 325 | display_item(0, 65, temp_strA, WHITE, 1); 326 | temp_strA = strtok(NULL, " "); 327 | } 328 | 329 | // BLOCK-4 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 330 | if (valid_cloud_report(temp_strA)) { 331 | temp_strA = convert_clouds(temp_strA); 332 | display_item(0, 75, temp_strA, WHITE, 1); 333 | temp_strA = strtok(NULL, " "); 334 | } 335 | } 336 | 337 | //---------------------------------------------------------------------------------------------------- 338 | // Process any reported temperatures e.g. 14/12 means Temp 14C Dewpoint 12 339 | // Test first section of temperature/dewpoint which is 'temperature' so either 12/nn or M12/Mnn 340 | if (temp_strA.indexOf("/") <= 0) { 341 | temp_Pres = temp_strA; 342 | temp_strA = "00/00"; 343 | } // Ignore no temp reports and save the next message, which is air pressure for later processing 344 | String temp_sign = ""; 345 | if (temp_strA.startsWith("M")) { 346 | temperature = 0 - temp_strA.substring(1,3).toInt(); 347 | if (temperature == 0) {temp_sign = "-";} // Reports of M00, meaning between -0.1 and -0.9C 348 | } 349 | else { 350 | temperature = temp_strA.substring(0,2).toInt(); 351 | if (temperature >=0 && temperature < 10) {temp_sign = " ";} // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 352 | } 353 | if (temperature <= 25) { display_item(0,110," Temp " + temp_sign + String(temperature) + char(247) + "C",CYAN,2); } 354 | else { display_item(0,110," Temp " + temp_sign + String(temperature) + char(247) + "C",RED,2); } 355 | 356 | //---------------------------------------------------------------------------------------------------- 357 | // Test second section of temperature/dewpoint which is 'dewpoint' so either nn/04 or Mnn/M04 358 | temp_strB = temp_strA.substring(temp_strA.indexOf('/')+1); 359 | if (temp_strB.startsWith("M")) { dew_point = 0 - temp_strB.substring(1,3).toInt(); } 360 | else { dew_point = temp_strB.substring(0,2).toInt(); 361 | } 362 | 363 | //---------------------------------------------------------------------------------------------------- 364 | // Don't display windchill unless wind speed is greater than 3 MPH and temperature is less than 14'C 365 | if (wind_speedKTS > 3 && temperature <= 14) { 366 | temp_sign = ""; 367 | int wind_chill = int(calc_windchill(temperature,wind_speedKTS)); 368 | if (wind_chill >=0 && wind_chill < 10) {temp_sign = " ";} // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 369 | display_item(0,164,"WindC " + temp_sign + String(wind_chill) + char(247) + "C",CYAN,2); 370 | } 371 | 372 | //---------------------------------------------------------------------------------------------------- 373 | // Calculate and display Relative Humidity 374 | temp_sign = ""; 375 | if (dew_point >=0 && dew_point < 10) {temp_sign = " ";} // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 376 | display_item(0,127," Dewp " + temp_sign + String(dew_point) + char(247) + "C",CYAN,2); 377 | int RH = calc_rh(temperature,dew_point); 378 | display_item(00,146,"Rel.H " + String(RH) + "%",CYAN,2); 379 | 380 | //---------------------------------------------------------------------------------------------------- 381 | // Don't display heatindex unless temperature > 18C 382 | if (temperature >= 20) { 383 | float T = (temperature * 9 / 5) + 32; 384 | float RHx = RH; 385 | int tHI = (-42.379 + (2.04901523 * T) + (10.14333127 * RHx) - (0.22475541 * T * RHx) - (0.00683783 * T * T) - (0.05481717 * RHx * RHx) + (0.00122874 * T * T * RHx) + (0.00085282 * T * RHx * RHx) - (0.00000199 * T * T * RHx * RHx) - 32 ) * 5 / 9; 386 | display_item(0,164,"HeatX " + String(tHI) + char(247) + "C",CYAN,2); 387 | } 388 | //where HI = -42.379 + 2.04901523*T + 10.14333127*RH - 0.22475541*T*RH - 0.00683783*T*T - 0.05481717*RH*RH + 0.00122874*T*T*RH + 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH 389 | //tHI = heat index (oF) 390 | //t = air temperature (oF) (t > 57oF) 391 | //φ = relative humidity (%) 392 | //---------------------------------------------------------------------------------------------------- 393 | //Process air pressure (QNH) e.g. Q1018 means 1018mB 394 | temp_strA = strtok(NULL," "); 395 | temp_strA.trim(); 396 | if (temp_Pres.startsWith("Q")) temp_strA = temp_Pres; // Pickup a temporary copy of air pressure, found when no temp broadcast 397 | if (temp_strA.startsWith("Q")) { 398 | if (temp_strA.substring(1,2) == "0") { 399 | temp_strA = " " + temp_strA.substring(2); 400 | } 401 | else { 402 | temp_strA = temp_strA.substring(1); 403 | } 404 | display_item((centreX-35),(centreY-57),temp_strA + "mB",YELLOW,2); 405 | } 406 | 407 | //---------------------------------------------------------------------------------------------------- 408 | // Now process up to two secondary weather reports 409 | temp_strA = strtok(NULL," "); // Get last tokens, can be a secondary weather report 410 | Process_secondary_reports(temp_strA,143); 411 | temp_strA = strtok(NULL," "); // Get last tokens, can be a secondary weather report 412 | Process_secondary_reports(temp_strA,153); 413 | parameter = NULL; // Reset the 'str' pointer, so end of report decoding 414 | } 415 | } // finished decoding the METAR string 416 | //---------------------------------------------------------------------------------------------------- 417 | //---------------------------------------------------------------------------------------------------- 418 | 419 | String convert_clouds(String source) { 420 | String height = source.substring(3,6); 421 | String cloud = source.substring(0,3); 422 | String warning = " "; 423 | while (height.startsWith("0")) {height = height.substring(1);} // trim leading '0' 424 | if (source.endsWith("TCU") || source.endsWith("CB")) { 425 | display_item(0,95,"Warning - storm clouds detected",WHITE,1); 426 | warning = " (storm) "; 427 | } 428 | // 'adjust offset if 0 replaced by space 429 | if (cloud !="SKC" && cloud != "CLR" && height != " ") { 430 | height = " at " + height + "00ft"; 431 | } else height = ""; 432 | if (source == "VV///") {return "No cloud reported";} 433 | if (cloud == "BKN") {return "Broken" + warning + "clouds" + height;} 434 | if (cloud == "SKC") {return "Clear skies";} 435 | if (cloud == "FEW") {return "Few" + warning + "clouds" + height;} 436 | if (cloud == "NCD") {return "No clouds detected";} 437 | if (cloud == "NSC") {return "No signficiant clouds";} 438 | if (cloud == "OVC") {return "Overcast" + warning + height;} 439 | if (cloud == "SCT") {return "Scattered" + warning + "clouds" + height;} 440 | return ""; 441 | } 442 | 443 | 444 | int min_val(int num1, int num2) { 445 | if(num1 > num2) 446 | { return num2;} 447 | else 448 | { return num1;} 449 | } 450 | 451 | float calc_rh(int temp, int dewp) { 452 | return 100 * (exp((17.271 * dewp)/(237.7 + dewp))) / (exp((17.271 * temp)/(237.7 + temp))) + 0.5; 453 | } 454 | 455 | float calc_windchill(int temperature, int wind_speed) { 456 | float result; 457 | // Derived from wind_chill = 13.12 + 0.6215 * Tair - 11.37 * POWER(wind_speed,0.16)+0.3965 * Tair * POWER(wind_speed,0.16) 458 | wind_speed = wind_speed * 1.852; // Convert to Kph 459 | result = 13.12 + 0.6215 * temperature - 11.37 * pow(wind_speed,0.16) + 0.3965 * temperature * pow(wind_speed,0.16); 460 | if (result < 0 ) { return result - 0.5; } else { return result + 0.5; } 461 | } 462 | 463 | void Process_secondary_reports(String temp_strA, int line_pos) { 464 | temp_strA.trim(); 465 | if (temp_strA == "NOSIG") {display_item(136,line_pos,"No significant change expected",WHITE,1);} 466 | if (temp_strA == "TEMPO") {display_item(136,line_pos,"Temporary conditions expected",WHITE,1);} 467 | if (temp_strA == "RADZ") {display_item(136,line_pos,"Recent Rain/Drizzle",WHITE,1);} 468 | if (temp_strA == "RERA") {display_item(136,line_pos,"Recent Moderate/Heavy Rain",WHITE,1);} 469 | if (temp_strA == "REDZ") {display_item(136,line_pos,"Recent Drizzle",WHITE,1);} 470 | if (temp_strA == "RESN") {display_item(136,line_pos,"Recent Moderate/Heavy Snow",WHITE,1);} 471 | if (temp_strA == "RESG") {display_item(136,line_pos,"Recent Moderate/Heavy Snow grains",WHITE,1);} 472 | if (temp_strA == "REGR") {display_item(136,line_pos,"Recent Moderate/Heavy Hail",WHITE,1);} 473 | if (temp_strA == "RETS") {display_item(136,line_pos,"Recent Thunder storms",WHITE,1);} 474 | } 475 | 476 | String display_conditions(String WX_state) { 477 | if (WX_state == "//") {return "No weather reported";} 478 | if (WX_state == "VC") {return "Vicinity has";} 479 | if (WX_state == "BL") {return "Blowing";} 480 | if (WX_state == "SH") {return "Showers";} 481 | if (WX_state == "TS") {return "Thunderstorms";} 482 | if (WX_state == "FZ") {return "Freezing";} 483 | if (WX_state == "UP") {return "Unknown";} 484 | //---------------- 485 | if (WX_state == "MI") {return "Shallow";} 486 | if (WX_state == "PR") {return "Partial";} 487 | if (WX_state == "BC") {return "Patches";} 488 | if (WX_state == "DR") {return "Low drifting";} 489 | if (WX_state == "IC") {return "Ice crystals";} 490 | if (WX_state == "PL") {return "Ice pellets";} 491 | if (WX_state == "GR") {return "Hail";} 492 | if (WX_state == "GS") {return "Small hail";} 493 | //---------------- 494 | if (WX_state == "DZ") {return "Drizzle";} 495 | if (WX_state == "RA") {return "Rain";} 496 | if (WX_state == "SN") {return "Snow";} 497 | if (WX_state == "SG") {return "Snow grains";} 498 | if (WX_state == "DU") {return "Widespread dust";} 499 | if (WX_state == "SA") {return "Sand";} 500 | if (WX_state == "HZ") {return "Haze";} 501 | if (WX_state == "PY") {return "Spray";} 502 | //---------------- 503 | if (WX_state == "BR") {return "Mist";} 504 | if (WX_state == "FG") {return "Fog";} 505 | if (WX_state == "FU") {return "Smoke";} 506 | if (WX_state == "VA") {return "Volcanic ash";} 507 | if (WX_state == "DS") {return "Dust storm";} 508 | if (WX_state == "PO") {return "Well developed dust/sand swirls";} 509 | if (WX_state == "SQ") {return "Squalls";} 510 | if (WX_state == "FC") {return "Funnel clouds/Tornadoes";} 511 | if (WX_state == "SS") {return "Sandstorm";} 512 | return ""; 513 | } 514 | 515 | void display_item(int x, int y, String token, int txt_colour, int txt_size) { 516 | M5.Lcd.setCursor(x, y); 517 | M5.Lcd.setTextColor(txt_colour); 518 | M5.Lcd.setTextSize(txt_size); 519 | M5.Lcd.print(token); 520 | M5.Lcd.setTextSize(2); // Back to default text size 521 | } 522 | 523 | void display_item_nxy(String token, int txt_colour, int txt_size) { 524 | M5.Lcd.setTextColor(txt_colour); 525 | M5.Lcd.setTextSize(txt_size); 526 | M5.Lcd.print(token); 527 | M5.Lcd.setTextSize(2); // Back to default text size 528 | } 529 | 530 | void clear_screen() { 531 | M5.Lcd.fillScreen(BLACK); 532 | } 533 | 534 | void arrow(int x1, int y1, int x2, int y2, int alength, int awidth, int colour) { 535 | float distance; 536 | int dx, dy, x2o,y2o,x3,y3,x4,y4,k; 537 | distance = sqrt(pow((x1 - x2),2) + pow((y1 - y2), 2)); 538 | dx = x2 + (x1 - x2) * alength / distance; 539 | dy = y2 + (y1 - y2) * alength / distance; 540 | k = awidth / alength; 541 | x2o = x2 - dx; 542 | y2o = dy - y2; 543 | x3 = y2o * k + dx; 544 | y3 = x2o * k + dy; 545 | // 546 | x4 = dx - y2o * k; 547 | y4 = dy - x2o * k; 548 | M5.Lcd.drawLine(x1, y1, x2, y2,colour); 549 | M5.Lcd.drawLine(x1, y1, dx, dy,colour); 550 | M5.Lcd.drawLine(x3, y3, x4, y4,colour); 551 | M5.Lcd.drawLine(x3, y3, x2, y2,colour); 552 | M5.Lcd.drawLine(x2, y2, x4, y4,colour); 553 | } 554 | 555 | void draw_veering_arrow(int a_direction) { 556 | int dx = (diameter * 0.75 * cos((a_direction-90)*3.14/180)) + centreX; 557 | int dy = (diameter * 0.75 * sin((a_direction-90)*3.14/180)) + centreY; 558 | arrow(centreX, centreY, dx, dy, 2, 5, RED); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 559 | } 560 | 561 | void Draw_Compass_Rose() { 562 | int dxo, dyo, dxi, dyi; 563 | M5.Lcd.drawCircle(centreX,centreY,diameter,GREEN); // Draw compass circle 564 | // M5.Lcd.fillCircle(centreX,centreY,diameter,GREY); // Draw compass circle 565 | M5.Lcd.drawRoundRect((centreX-45),(centreY-60),(diameter+50),(centreY+diameter/2+50),10,YELLOW); // Draw compass rose 566 | M5.Lcd.drawLine(0,105,228,105,YELLOW); // Seperating line for relative-humidity, temp, windchill, temp-index and dewpoint 567 | M5.Lcd.drawLine(132,105,132,185,YELLOW); // Seperating vertical line for relative-humidity, temp, windchill, temp-index and dewpoint 568 | for (float i = 0; i <360; i = i + 22.5) { 569 | dxo = diameter * cos((i-90)*3.14/180); 570 | dyo = diameter * sin((i-90)*3.14/180); 571 | dxi = dxo * 0.9; 572 | dyi = dyo * 0.9; 573 | M5.Lcd.drawLine(dxo+centreX,dyo+centreY,dxi+centreX,dyi+centreY,YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 574 | dxo = dxo * 0.5; 575 | dyo = dyo * 0.5; 576 | dxi = dxo * 0.9; 577 | dyi = dyo * 0.9; 578 | M5.Lcd.drawLine(dxo+centreX,dyo+centreY,dxi+centreX,dyi+centreY,YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 579 | } 580 | display_item((centreX-2),(centreY-33),"N",GREEN,1); 581 | display_item((centreX-2),(centreY+26),"S",GREEN,1); 582 | display_item((centreX+30),(centreY-3),"E",GREEN,1); 583 | display_item((centreX-32),(centreY-3),"W",GREEN,1); 584 | } 585 | 586 | boolean valid_cloud_report(String temp_strA) { 587 | if (temp_strA.startsWith("BKN") || 588 | temp_strA.startsWith("CLR") || 589 | temp_strA.startsWith("FEW") || 590 | temp_strA.startsWith("NCD") || 591 | temp_strA.startsWith("NSC") || 592 | temp_strA.startsWith("OVC") || 593 | temp_strA.startsWith("SCT") || 594 | temp_strA.startsWith("SKC") || 595 | temp_strA.endsWith("CB") || 596 | temp_strA.endsWith("TCU")) { 597 | return true;} else { return false; } 598 | } 599 | 600 | void display_status() { 601 | display_progress("Initialising",50); 602 | M5.Lcd.setTextSize(2); 603 | display_progress("Waiting for IP address",75); 604 | display_progress("Ready...",100); 605 | clear_screen(); 606 | } 607 | 608 | int StartWiFi(const char* ssid, const char* password) { 609 | int connAttempts = 0; 610 | Serial.print(F("\r\nConnecting to: ")); Serial.println(String(ssid)); 611 | WiFi.begin(ssid, password); 612 | while (WiFi.status() != WL_CONNECTED ) { 613 | delay(500); Serial.print("."); 614 | if (connAttempts > 20) return -5; 615 | connAttempts++; 616 | } 617 | Serial.print(F("WiFi connected at: ")); 618 | Serial.println(WiFi.localIP()); 619 | return 1; 620 | } 621 | 622 | void display_progress (String title, int percent) { 623 | int title_pos = (320 - title.length()*12)/2; // Centre title 624 | int x_pos = 35; int y_pos = 105; 625 | int bar_width = 250; int bar_height = 15; 626 | display_item(title_pos,y_pos-20,title,GREEN,2); 627 | M5.Lcd.drawRoundRect(x_pos,y_pos,bar_width+2,bar_height,5,YELLOW); // Draw progress bar outline 628 | M5.Lcd.fillRoundRect(x_pos+2,y_pos+1,percent*bar_width/100-2,bar_height-3,4,BLUE); // Draw progress 629 | delay(2000); 630 | M5.Lcd.fillRect(x_pos-30,y_pos-20,320,16,BLACK); // Clear titles 631 | } 632 | 633 | 634 | /* Meaning of various codes 635 | Moderate/heavy rain RERA 636 | Moderate/heavy snow RESN 637 | Moderate/heavy small hail REGS 638 | Moderate/heavy snow pellets REGS Moderate/heavy ice pellets REPL 639 | Moderate/heavy hail REGR 640 | Moderate/heavy snow grains RESG 641 | 642 | Intensity Description Precipitation Obscuration Other 643 | - Light MI Shallow DZ Drizzle BR Mist PO Well developed dust / sand whirls 644 | Moderate PR Partial RA Rain FG Fog SQ Squalls 645 | + Heavy BC Patches SN Snow FU Smoke FC Funnel clouds inc tornadoes or waterspouts 646 | VC Vicinity DR Low drifting SG Snow grains VA Volcanic ash SS Sandstorm 647 | BL Blowing IC Ice crystals DU Widespread dust DS Duststorm 648 | SH Showers PL Ice pellets SA Sand 649 | TS Thunderstorm GR Hail HZ Haze 650 | FZ Freezing GS Small hail PY Spray 651 | UP Unknown 652 | 653 | e.g. -SHRA - Light showers of rain 654 | TSRA - Thunderstorms and rain. 655 | */ 656 | 657 | 658 | 659 | /* Returns the following METAR data from the server address 660 | 661 | - 662 | 18793217 663 | 664 | 665 | 666 | 667 | 4 668 | - 669 | - 670 | EGLL 241950Z AUTO 07009KT 9999 NCD 03/M02 Q1023 671 | EGLL 672 | 2018-02-24T19:50:00Z 673 | 51.48 674 | -0.45 675 | 3.0 676 | -2.0 677 | 70 678 | 9 679 | 6.21 680 | 30.206694 681 | - 682 | TRUE 683 | 684 | 685 | VFR 686 | METAR 687 | 24.0 688 | 689 | - 690 | EGLL 241920Z AUTO 07008KT 9999 NCD 03/M03 Q1023 691 | EGLL 692 | 2018-02-24T19:20:00Z 693 | 51.48 694 | -0.45 695 | 3.0 696 | -3.0 697 | 70 698 | 8 699 | 6.21 700 | 30.206694 701 | - 702 | TRUE 703 | 704 | 705 | VFR 706 | METAR 707 | 24.0 708 | 709 | 710 | 711 | 712 | EGDM 121621Z 35005KT 9999 5000SW BR FEW001 BKN002 11/11 Q1014 AMB 713 | EGDM 714 | 2016-11-12T16:21:00Z 715 | 51.17 716 | -1.75 717 | 11.0 718 | 11.0 719 | 350 720 | 5 721 | 6.21 722 | 29.940945 723 | BR 724 | 725 | 726 | LIFR 727 | METAR 728 | 124.0 729 | 730 | */ 731 | 732 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266 or ESP32 METAR-Decoder-and-Display 2 | 3 | Using an ESP32 or ESP8266 linked to a TFT display (ILI9341) to decode and display weather data from METAR services. 4 | 5 | The M5-Stack version is for the ESP32 platform. 6 | 7 | The ESP32 SCT01 (wireless-tag) in-built TFT with 480x320 resolution display, uses an ST779 display driver 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Wireless-tag-ESP32_SCT01_METAR_DECODER_NWS_V7_SSL.ino: -------------------------------------------------------------------------------- 1 | /* Version 7 METAR Decoder and display for ESP32 and ST7796 lcd screen 2 | 3 | This software, the ideas and concepts is Copyright (c) David Bird 2022. 4 | All rights to this software are reserved. 5 | 6 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 7 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 8 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 9 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 10 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 11 | 12 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 13 | software use is visible to an end-user. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 16 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | See more at http://www.dsbird.org.uk 20 | 21 | */ 22 | // Screen size = 480 x 320 (size ratio relative to 320x240 = 1.5 x 1.33 23 | // Screen Name = ESP32 SCT01 24 | // Drive Type = ST7796 25 | //////////////////////////////////////////////////////////////////////////////////// 26 | String version_num = "METAR ESP Version 7.0"; 27 | #include 28 | #define LGFX_AUTODETECT 29 | #define LGFX_USE_V1 30 | #include 31 | #include // "LGFX" 32 | #define LGFX_WT32_SC01 33 | 34 | static LGFX lcd; // LGFX 35 | 36 | #include 37 | #include "HTTPClient.h" 38 | 39 | const char *ssid = "yourSSID"; 40 | const char *password = "yourPASSWORD"; 41 | const char* host = "https://aviationweather.gov"; 42 | const int httpsPort = 443; 43 | 44 | WiFiClientSecure client; 45 | 46 | const int centreX = 409; // x,y Location of the compass display on screen 47 | const int centreY = 83; 48 | const int diameter = 60; // Size of the compass 49 | 50 | // Assign human-readable names to common 16-bit color values: 51 | #define BLACK 0x0000 52 | #define RED 0xF800 53 | #define GREEN 0x07E0 54 | #define BLUE 0x001F 55 | #define CYAN 0x07FF 56 | #define MAGENTA 0xF81F 57 | #define YELLOW 0xFFE0 58 | #define WHITE 0xFFFF 59 | 60 | void setup() { 61 | Serial.begin(115200); 62 | lcd.init(); 63 | lcd.setRotation(1); // 0-3 64 | lcd.setBrightness(128); // 0-255 65 | lcd.setColorDepth(16); // RGB565 66 | //lcd.setColorDepth(24); // RGB888の24 RGB666の18 67 | lcd.startWrite(); 68 | clear_screen(); 69 | display_progress("Connecting to Network", 25); 70 | WiFi.begin(ssid, password); 71 | while (WiFi.status() != WL_CONNECTED) { 72 | delay(100); 73 | Serial.print("."); 74 | } 75 | Serial.println("\nWiFi connected at: " + WiFi.localIP().toString()); 76 | display_status(); 77 | } 78 | 79 | void loop() { 80 | // Change these METAR Stations to suit your needs see: Use this URL address = ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/ 81 | // to establish your list of sites to retrieve (you must know the 4-letter 82 | // site dentification) 83 | GET_METAR("EGGD", "1 EGGD Bristol/Lulsgate"); 84 | GET_METAR("EGVN", "2 EGVN Brize Norton"); 85 | GET_METAR("EGCC", "3 EGCC Manchester Airport"); 86 | GET_METAR("EGHQ", "4 EGHQ Newquay"); 87 | GET_METAR("EGSS", "5 EGSS Stansted"); 88 | } 89 | 90 | //---------------------------------------------------------------------------------------------------- 91 | void GET_METAR(String Station, String Name) { //client function to send/receive GET request data. 92 | String metar, raw_metar; 93 | bool metar_status = true; 94 | const int time_delay = 20000; 95 | display_item(35, 100, "Decoding METAR", GREEN, 3); 96 | display_item(90, 135, "for " + Station, GREEN, 3); 97 | // http://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 (example) 98 | // https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=EGLL&hoursBeforeNow=1 99 | // ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/EGCC.TXT 100 | String uri = "/cgi-bin/data/metar.php?dataSource=metars&requestType=retrieve&format=xml&ids=" + station + "&hoursBeforeNow=1"; 101 | //String uri = "/adds/dataserver_current/httpparam?datasource=metars&requestType=retrieve&format=xml&mostRecentForEachStation=constraint&hoursBeforeNow=1.25&stationString=" + Station; 102 | Serial.println("Connected, \nRequesting data for : " + Name); 103 | HTTPClient http; 104 | http.begin(host + uri); // Specify the URL and certificate 105 | int httpCode = http.GET(); // Start connection and send HTTP header 106 | if (httpCode > 0) { // HTTP header has been sent and Server response header has been handled 107 | if (httpCode == HTTP_CODE_OK) raw_metar = http.getString(); 108 | http.end(); 109 | metar = raw_metar.substring(raw_metar.indexOf("", 0) + 10, raw_metar.indexOf("", 0)); 110 | } 111 | else 112 | { 113 | http.end(); 114 | Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); 115 | metar_status = false; 116 | metar = "Station off-air"; 117 | } 118 | display_item(265, 230, "Connected", RED, 1); 119 | Serial.println(metar); 120 | client.stop(); 121 | clear_screen(); // Clear screen 122 | display_item(400, 306, "Connected", RED, 1); 123 | display_item(10, 306, version_num, GREEN, 1); 124 | display_item(10, 253, Name, YELLOW, 2); 125 | lcd.drawLine(0, 246, 480, 246, YELLOW); 126 | display_item(10, 279, metar, YELLOW, 1); 127 | if (metar_status == true) { 128 | display_metar(metar); 129 | lcd.display(); 130 | delay(time_delay); // Delay for set time 131 | } 132 | else 133 | { 134 | display_item(105, 133, metar, YELLOW, 2); // Now decode METAR 135 | lcd.display(); 136 | delay(5000); // Wait less time if station off-air 137 | } 138 | clear_screen(); // Clear screen before moving to next station display 139 | lcd.startWrite(); 140 | } 141 | 142 | void display_metar(String metar) { 143 | int temperature = 0; 144 | int dew_point = 0; 145 | int wind_speedKTS = 0; 146 | char str[130] = ""; 147 | char *parameter; // a pointer variable 148 | String conditions_start = " "; 149 | String temp_strA = ""; 150 | String temp_strB = ""; 151 | String temp_Pres = ""; 152 | String conditions_test = "+-BDFGHIMPRSTUV"; // Test for light or heavy (+/-) BR - Mist, RA - Rain SH-Showers VC - Vicinity, etc 153 | // Test metar, press enter after colon to include for testing:metar = "EGDM 261550Z AUTO 26517G23KT 250V290 6999 R04/1200 4200SW -SH SN SCT002TCU NCD FEW222/// SCT090 28/M15 Q1001 RERA NOSIG BLU"; 154 | // Test metar exercises all decoder functions on screen 155 | metar.toCharArray(str, 130); 156 | parameter = strtok(str, " "); // Find tokens that are seperated by SPACE 157 | while (parameter != NULL) { 158 | //display_item(5,0, String(parameter),RED,2); // Station Name - omitted at V2.01 159 | temp_strA = strtok(NULL, " "); 160 | display_item(5, 0, "Date:" + temp_strA.substring(0, 2) + " @ " + temp_strA.substring(2, 6) + "Hr", GREEN, 2); // Date-time 161 | 162 | //---------------------------------------------------------------------------------------------------- 163 | // Process any reported station type e.g. AUTO means Automatic 164 | temp_strA = strtok(NULL, " "); 165 | if (temp_strA == "AUTO") { 166 | //display_item(54,0,"A",CYAN,2); // Omitted at V2.01 167 | temp_strA = strtok(NULL, " "); 168 | } 169 | 170 | //---------------------------------------------------------------------------------------------------- 171 | // Process any reported wind direction and speed e.g. 270019KTS means direction is 270 deg and speed 19Kts 172 | // radians = (degrees * 71) / 4068 from Pi/180 to convert to degrees 173 | Draw_Compass_Rose(); // Draw compass rose 174 | if (temp_strA == "/////KT") { 175 | temp_strA = "00000KT"; 176 | } 177 | temp_strB = temp_strA.substring(3, 5); 178 | wind_speedKTS = temp_strB.toInt(); // Knots/sec 179 | int wind_speedMPH = wind_speedKTS * 1.15077 + 0.5; 180 | if (temp_strA.indexOf('G') >= 0) { 181 | temp_strB = temp_strA.substring(8); 182 | } 183 | else { 184 | temp_strB = temp_strA.substring(5); 185 | } // Now get units of wind speed either KT or MPS 186 | if (temp_strB == "MPS") { 187 | temp_strB = "MS"; 188 | } 189 | if (wind_speedMPH < 18) display_item((centreX - 28), (centreY + 70), (String(wind_speedMPH) + " MPH"), YELLOW, 2); 190 | else display_item((centreX - 35), (centreY + 70), (String(wind_speedMPH) + " MPH"), RED, 2); 191 | if (temp_strA.indexOf('G') >= 0) { 192 | lcd.fillRect((centreX - 40), (centreY + 68), 82, 18, BLACK); 193 | display_item((centreX - 40), (centreY + 70), String(wind_speedKTS) + "g" + temp_strA.substring(temp_strA.indexOf('G') + 1, temp_strA.indexOf('G') + 3) + temp_strB, YELLOW, 2); 194 | } 195 | int wind_direction = 0; 196 | if (temp_strA.substring(0, 3) == "VRB") { 197 | display_item((centreX - 23), (centreY - 7), "VRB", YELLOW, 2); 198 | } 199 | else { 200 | wind_direction = temp_strA.substring(0, 3).toInt() - 90; 201 | int dx = (diameter * cos((wind_direction) * 0.017444)) + centreX; 202 | int dy = (diameter * sin((wind_direction) * 0.017444)) + centreY; 203 | arrow(dx, dy, centreX, centreY, 5, 5, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 204 | } 205 | 206 | //---------------------------------------------------------------------------------------------------- 207 | // get next token, this could contain Veering information e.g. 0170V220 208 | temp_strA = strtok(NULL, " "); 209 | if (temp_strA.indexOf('V') >= 0 && temp_strA != "CAVOK") { // Check for variable wind direction 210 | int startV = temp_strA.substring(0, 3).toInt(); 211 | int endV = temp_strA.substring(4, 7).toInt(); 212 | // Minimum angle is either ABS(AngleA- AngleB) or (360-ABS(AngleA-AngleB)) 213 | int veering = min_val(360 - abs(startV - endV), abs(startV - endV)); 214 | display_item((centreX - 207), (centreY + 65), "V" + String(veering) + char(247), RED, 2); 215 | display_item((centreX - 60), (centreY + 65), "v", RED, 2); // Signify 'Variable wind direction 216 | draw_veering_arrow(startV); 217 | draw_veering_arrow(endV); 218 | temp_strA = strtok(NULL, " "); // Move to the next token/item 219 | } 220 | 221 | //---------------------------------------------------------------------------------------------------- 222 | // Process any reported visibility e.g. 6000 means 6000 Metres of visibility 223 | if (temp_strA == "////") { 224 | temp_strB = "No Visibility Rep."; 225 | } else temp_strB = ""; 226 | if (temp_strA == "CAVOK") { 227 | display_item(5, 27, "Visibility good", WHITE, 1); 228 | display_item(5, 40, "Conditions good", WHITE, 1); 229 | } 230 | else { 231 | if (temp_strA != "////") { 232 | if (temp_strA == "9999") { 233 | temp_strB = "Visibility excellent"; 234 | } 235 | else { 236 | String vis = temp_strA; 237 | while (vis.startsWith("0")) { 238 | vis = vis.substring(1); // trim leading '0' 239 | } 240 | temp_strB = vis + " Metres of visibility"; 241 | } 242 | } 243 | } 244 | display_item(5, 27, temp_strB, WHITE, 1); 245 | temp_strA = strtok(NULL, " "); 246 | 247 | //---------------------------------------------------------------------------------------------------- 248 | // Process any secondary Runway visibility reports e.g. R04/1200 means Runway 4, visibility 1200M 249 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 250 | // Ignore the report for now 251 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 252 | } 253 | if ( temp_strA.startsWith("R") && !temp_strA.startsWith("RA")) { // Ignore a RA for Rain weather report 254 | // Ignore the report for now, there can be two! 255 | temp_strA = strtok(NULL, " "); // If there was a Variable report, move to the next item 256 | } 257 | 258 | //---------------------------------------------------------------------------------------------------- 259 | // Process any secondary reported visibility e.g. 6000S (or 6000SW) means 6000 Metres in a Southerly direction (or from SW) 260 | if (temp_strA.length() >= 5 && temp_strA.substring(0, 1) != "+" && temp_strA.substring(0, 1) != "-" && (temp_strA.endsWith("N") || temp_strA.endsWith("S") || temp_strA.endsWith("E") || temp_strA.endsWith("W")) ) { 261 | conditions_start = temp_strA.substring(4); 262 | conditions_start = conditions_start.substring(0, 1); 263 | if (conditions_start == "N" || conditions_start == "S" || conditions_start == "E" || conditions_start == "W") { 264 | lcd.fillRect(38, 27, 255, 27, BLACK); 265 | display_item(38, 27, "/" + temp_strA + " Mts Visibility", WHITE, 1); 266 | } 267 | temp_strA = strtok(NULL, " "); 268 | } 269 | 270 | //---------------------------------------------------------------------------------------------------- 271 | // Process any reported weather conditions e.g. -RA means light rain 272 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 273 | // Ignore any cloud reports at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV/// 274 | lcd.drawRect(199, 172, 279, 16, YELLOW); 275 | display_item(206, 176, "Additional Wx Reports:", WHITE, 1); 276 | conditions_start = temp_strA.substring(0, 1); 277 | temp_strB = temp_strA.substring(0, 3); 278 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 279 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 280 | && temp_strB != "NSC" 281 | && !temp_strA.startsWith("M0")) { 282 | temp_strB = ""; 283 | if (conditions_start == "-" || conditions_start == "+") { 284 | if (conditions_start == "-") { 285 | temp_strB = "Light "; 286 | } else { 287 | temp_strB = "Heavy "; 288 | } 289 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 290 | } 291 | if (temp_strA.length() == 2) { 292 | display_item(204, 162, temp_strB + display_conditions(temp_strA), WHITE, 1); 293 | } 294 | else { 295 | display_item(204, 216, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 296 | display_item_nxy("/" + display_conditions(temp_strA.substring(2, 4)), WHITE, 1); 297 | if (temp_strA.length() >= 6) { // sometimes up to three cateries are reported 298 | display_item_nxy("/" + display_conditions(temp_strA.substring(4, 6)), WHITE, 1); 299 | } 300 | } 301 | parameter = strtok(NULL, " "); 302 | temp_strA = parameter; 303 | } 304 | 305 | // Process any reported weather conditions e.g. -RA means light rain 306 | // Test for cloud reports that conflict with weather condition descriptors and ignore them, test for occassions when there is no Conditions report 307 | // Ignore any cloud reprts at this stage BKN, CLR, FEW, NCD, OVC, SCT, SKC, or VV///(poor vertical visibility) 308 | conditions_start = temp_strA.substring(0, 1); 309 | temp_strB = temp_strA.substring(0, 3); 310 | if ((temp_strA == "//" || conditions_test.indexOf(conditions_start) >= 0) 311 | && !(valid_cloud_report(temp_strB)) // Don't process a cloud report that starts with the same letter as a weather condition report 312 | && temp_strB != "NSC" 313 | && !temp_strA.startsWith("M0")) { 314 | temp_strB = ""; 315 | if (conditions_start == "-" || conditions_start == "+") { 316 | if (conditions_start == "-") { 317 | temp_strB = "Light "; 318 | } else { 319 | temp_strB = "Heavy "; 320 | } 321 | temp_strA = temp_strA.substring(1); // Remove leading + or - and can't modify same variable recursively 322 | } 323 | if (temp_strA.length() == 2) { 324 | display_item(170, 230, temp_strB + display_conditions(temp_strA), WHITE, 1); 325 | } 326 | else { 327 | if (temp_strA.length() >= 5) { // sometimes up to three cateries are reported 328 | display_item(170, 230, "Poor Vert Visibility", WHITE, 1); 329 | } 330 | else { 331 | display_item(170, 230, temp_strB + display_conditions(temp_strA.substring(0, 2)), WHITE, 1); 332 | } 333 | } 334 | parameter = strtok(NULL, " "); 335 | temp_strA = parameter; 336 | } 337 | 338 | //---------------------------------------------------------------------------------------------------- 339 | // Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 340 | lcd.drawLine(0, 53, 343, 53, YELLOW); 341 | if (temp_strA == "////" || temp_strA == "/////" || temp_strA == "//////") { 342 | temp_strA = "No CC Rep."; 343 | temp_strA = strtok(NULL, " "); 344 | } 345 | else 346 | { 347 | // BLOCK-1 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 348 | if (valid_cloud_report(temp_strA) || temp_strA.startsWith("VV/")) { 349 | temp_strA = convert_clouds(temp_strA); 350 | display_item(5, 60, temp_strA, WHITE, 1); 351 | temp_strA = strtok(NULL, " "); 352 | } 353 | 354 | // BLOCK-2 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 355 | if (valid_cloud_report(temp_strA)) { 356 | temp_strA = convert_clouds(temp_strA); 357 | display_item(5, 72, temp_strA, WHITE, 1); 358 | temp_strA = strtok(NULL, " "); 359 | } 360 | 361 | // BLOCK-3 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 362 | if (valid_cloud_report(temp_strA)) { 363 | temp_strA = convert_clouds(temp_strA); 364 | display_item(5, 84, temp_strA, WHITE, 1); 365 | temp_strA = strtok(NULL, " "); 366 | } 367 | 368 | // BLOCK-4 Process any reported cloud cover e.g. SCT018 means Scattered clouds at 1800 ft 369 | if (valid_cloud_report(temp_strA)) { 370 | temp_strA = convert_clouds(temp_strA); 371 | display_item(5, 96, temp_strA, WHITE, 1); 372 | temp_strA = strtok(NULL, " "); 373 | } 374 | } 375 | 376 | //---------------------------------------------------------------------------------------------------- 377 | // Process any reported temperatures e.g. 14/12 means Temp 14C Dewpoint 12 378 | // Test first section of temperature/dewpoint which is 'temperature' so either 12/nn or M12/Mnn 379 | if (temp_strA.indexOf("/") <= 0) { 380 | temp_Pres = temp_strA; 381 | temp_strA = "00/00"; 382 | } // Ignore no temp reports and save the next message, which is air pressure for later processing 383 | String temp_sign = ""; 384 | if (temp_strA.startsWith("M")) { 385 | temperature = 0 - temp_strA.substring(1, 3).toInt(); 386 | if (temperature == 0) { 387 | temp_sign = "-"; // Reports of M00, meaning between -0.1 and -0.9C 388 | } 389 | } 390 | else { 391 | temperature = temp_strA.substring(0, 2).toInt(); 392 | if (temperature >= 0 && temperature < 10) { 393 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 394 | } 395 | } 396 | if (temperature <= 25) { 397 | display_item(5, 146, " Temp " + temp_sign + String(temperature) + char(247) + "C", CYAN, 2); 398 | } 399 | else { 400 | display_item(5, 146, " Temp " + temp_sign + String(temperature) + char(247) + "C", RED, 2); 401 | } 402 | 403 | //---------------------------------------------------------------------------------------------------- 404 | // Test second section of temperature/dewpoint which is 'dewpoint' so either nn/04 or Mnn/M04 405 | temp_strB = temp_strA.substring(temp_strA.indexOf('/') + 1); 406 | if (temp_strB.startsWith("M")) { 407 | dew_point = 0 - temp_strB.substring(1, 3).toInt(); 408 | } 409 | else { 410 | dew_point = temp_strB.substring(0, 2).toInt(); 411 | } 412 | 413 | //---------------------------------------------------------------------------------------------------- 414 | // Don't display windchill unless wind speed is greater than 3 MPH and temperature is less than 14'C 415 | if (wind_speedKTS > 3 && temperature <= 14) { 416 | temp_sign = ""; 417 | int wind_chill = int(calc_windchill(temperature, wind_speedKTS)); 418 | if (wind_chill >= 0 && wind_chill < 10) { 419 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 420 | } 421 | display_item(5, 218, "WindC " + temp_sign + String(wind_chill) + char(247) + "C", CYAN, 2); 422 | } 423 | 424 | //---------------------------------------------------------------------------------------------------- 425 | // Calculate and display Relative Humidity 426 | temp_sign = ""; 427 | if (dew_point >= 0 && dew_point < 10) { 428 | temp_sign = " "; // Aligns for 2-digit temperatures e.g. " 1'C" and "11'C" 429 | } 430 | display_item(5, 169, " Dewp " + temp_sign + String(dew_point) + char(247) + "C", CYAN, 2); 431 | int RH = calc_rh(temperature, dew_point); 432 | display_item(5, 193, "Rel.H " + String(RH) + "%", CYAN, 2); 433 | 434 | //---------------------------------------------------------------------------------------------------- 435 | // Don't display heatindex unless temperature > 18C 436 | if (temperature >= 20) { 437 | float T = (temperature * 9 / 5) + 32; 438 | float RHx = RH; 439 | int tHI = (-42.379 + (2.04901523 * T) + (10.14333127 * RHx) - (0.22475541 * T * RHx) - (0.00683783 * T * T) - (0.05481717 * RHx * RHx) + (0.00122874 * T * T * RHx) + (0.00085282 * T * RHx * RHx) - (0.00000199 * T * T * RHx * RHx) - 32 ) * 5 / 9; 440 | display_item(5, 218, "HeatX " + String(tHI) + char(247) + "C", CYAN, 2); 441 | } 442 | //where HI = -42.379 + 2.04901523*T + 10.14333127*RH - 0.22475541*T*RH - 0.00683783*T*T - 0.05481717*RH*RH + 0.00122874*T*T*RH + 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH 443 | //tHI = heat index (oF) 444 | //t = air temperature (oF) (t > 57oF) 445 | //φ = relative humidity (%) 446 | //---------------------------------------------------------------------------------------------------- 447 | //Process air pressure (QNH) e.g. Q1018 means 1018mB 448 | temp_strA = strtok(NULL, " "); 449 | temp_strA.trim(); 450 | if (temp_Pres.startsWith("Q")) temp_strA = temp_Pres; // Pickup a temporary copy of air pressure, found when no temp broadcast 451 | if (temp_strA.startsWith("Q")) { 452 | if (temp_strA.substring(1, 2) == "0") { 453 | temp_strA = " " + temp_strA.substring(2); 454 | } 455 | else { 456 | temp_strA = temp_strA.substring(1); 457 | } 458 | display_item((centreX - 40), (centreY - 80), temp_strA + "hPa", YELLOW, 2); 459 | } 460 | 461 | //---------------------------------------------------------------------------------------------------- 462 | // Now process up to two secondary weather reports 463 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 464 | Process_secondary_reports(temp_strA, 190); 465 | temp_strA = strtok(NULL, " "); // Get last tokens, can be a secondary weather report 466 | Process_secondary_reports(temp_strA, 203); 467 | parameter = NULL; // Reset the 'str' pointer, so end of report decoding 468 | } 469 | } // finished decoding the METAR string 470 | 471 | //---------------------------------------------------------------------------------------------------- 472 | 473 | String convert_clouds(String source) { 474 | String height = source.substring(3, 6); 475 | String cloud = source.substring(0, 3); 476 | String warning = " "; 477 | while (height.startsWith("0")) { 478 | height = height.substring(1); // trim leading '0' 479 | } 480 | if (source.endsWith("TCU") || source.endsWith("CB")) { 481 | display_item(5, 108, "Warning - storm clouds detected", WHITE, 1); 482 | warning = " (storm) "; 483 | } 484 | // 'adjust offset if 0 replaced by space 485 | if (cloud != "SKC" && cloud != "CLR" && height != " ") { 486 | height = " at " + height + "00ft"; 487 | } else height = ""; 488 | if (source == "VV///") return "No cloud reported"; 489 | if (cloud == "BKN") return "Broken" + warning + "clouds" + height; 490 | if (cloud == "SKC") return "Clear skies"; 491 | if (cloud == "FEW") return "Few" + warning + "clouds" + height; 492 | if (cloud == "NCD") return "No clouds detected"; 493 | if (cloud == "NSC") return "No signficiant clouds"; 494 | if (cloud == "OVC") return "Overcast" + warning + height; 495 | if (cloud == "SCT") return "Scattered" + warning + "clouds" + height; 496 | return ""; 497 | } 498 | 499 | 500 | int min_val(int num1, int num2) { 501 | if (num1 > num2)return num2; 502 | else return num1; 503 | } 504 | 505 | float calc_rh(int temp, int dewp) { 506 | return 100 * (exp((17.271 * dewp) / (237.7 + dewp))) / (exp((17.271 * temp) / (237.7 + temp))) + 0.5; 507 | } 508 | 509 | float calc_windchill(int temperature, int wind_speed) { 510 | float result; 511 | // Derived from wind_chill = 13.12 + 0.6215 * Tair - 11.37 * POWER(wind_speed,0.16)+0.3965 * Tair * POWER(wind_speed,0.16) 512 | wind_speed = wind_speed * 1.852; // Convert to Kph 513 | result = 13.12 + 0.6215 * temperature - 11.37 * pow(wind_speed, 0.16) + 0.3965 * temperature * pow(wind_speed, 0.16); 514 | if (result < 0 ) { 515 | return result - 0.5; 516 | } else { 517 | return result + 0.5; 518 | } 519 | } 520 | 521 | void Process_secondary_reports(String temp_strA, int line_pos) { 522 | temp_strA.trim(); 523 | String report = ""; 524 | line_pos += 5; 525 | int x_pos = 205; 526 | if (temp_strA == "NOSIG") report = "No significant change expected"; 527 | if (temp_strA == "TEMPO") report = "Temporary conditions expected"; 528 | if (temp_strA == "RADZ") report = "Recent Rain/Drizzle"; 529 | if (temp_strA == "RERA") report = "Recent Moderate/Heavy Rain"; 530 | if (temp_strA == "REDZ") report = "Recent Drizzle"; 531 | if (temp_strA == "RESN") report = "Recent Moderate/Heavy Snow"; 532 | if (temp_strA == "RESG") report = "Recent Moderate/Heavy Snow grains"; 533 | if (temp_strA == "REGR") report = "Recent Moderate/Heavy Hail"; 534 | if (temp_strA == "RETS") report = "No significant change expected"; 535 | display_item(x_pos, line_pos, report, WHITE, 1); 536 | } 537 | 538 | String display_conditions(String WX_state) { 539 | if (WX_state == "//") { 540 | return "No weather reported"; 541 | } 542 | if (WX_state == "VC") { 543 | return "Vicinity has"; 544 | } 545 | if (WX_state == "BL") { 546 | return "Blowing"; 547 | } 548 | if (WX_state == "SH") { 549 | return "Showers"; 550 | } 551 | if (WX_state == "TS") { 552 | return "Thunderstorms"; 553 | } 554 | if (WX_state == "FZ") { 555 | return "Freezing"; 556 | } 557 | if (WX_state == "UP") { 558 | return "Unknown"; 559 | } 560 | if (WX_state == "MI") { 561 | return "Shallow"; 562 | } 563 | if (WX_state == "PR") { 564 | return "Partial"; 565 | } 566 | if (WX_state == "BC") { 567 | return "Patches"; 568 | } 569 | if (WX_state == "DR") { 570 | return "Low drifting"; 571 | } 572 | if (WX_state == "IC") { 573 | return "Ice crystals"; 574 | } 575 | if (WX_state == "PL") { 576 | return "Ice pellets"; 577 | } 578 | if (WX_state == "GR") { 579 | return "Hail"; 580 | } 581 | if (WX_state == "GS") { 582 | return "Small hail"; 583 | } 584 | if (WX_state == "DZ") { 585 | return "Drizzle"; 586 | } 587 | if (WX_state == "RA") { 588 | return "Rain"; 589 | } 590 | if (WX_state == "SN") { 591 | return "Snow"; 592 | } 593 | if (WX_state == "SG") { 594 | return "Snow grains"; 595 | } 596 | if (WX_state == "DU") { 597 | return "Widespread dust"; 598 | } 599 | if (WX_state == "SA") { 600 | return "Sand"; 601 | } 602 | if (WX_state == "HZ") { 603 | return "Haze"; 604 | } 605 | if (WX_state == "PY") { 606 | return "Spray"; 607 | } 608 | if (WX_state == "BR") { 609 | return "Mist"; 610 | } 611 | if (WX_state == "FG") { 612 | return "Fog"; 613 | } 614 | if (WX_state == "FU") { 615 | return "Smoke"; 616 | } 617 | if (WX_state == "VA") { 618 | return "Volcanic ash"; 619 | } 620 | if (WX_state == "DS") { 621 | return "Dust storm"; 622 | } 623 | if (WX_state == "PO") { 624 | return "Well developed dust/sand swirls"; 625 | } 626 | if (WX_state == "SQ") { 627 | return "Squalls"; 628 | } 629 | if (WX_state == "FC") { 630 | return "Funnel clouds/Tornadoes"; 631 | } 632 | if (WX_state == "SS") { 633 | return "Sandstorm"; 634 | } 635 | return ""; 636 | } 637 | 638 | void display_item(int x, int y, String token, int txt_colour, int txt_size) { 639 | lcd.setCursor(x, y); 640 | lcd.setTextColor(txt_colour); 641 | lcd.setTextSize(txt_size); 642 | lcd.print(token); 643 | lcd.setTextSize(2); // Back to default text size 644 | } 645 | 646 | void display_item_nxy(String token, int txt_colour, int txt_size) { 647 | lcd.setTextColor(txt_colour); 648 | lcd.setTextSize(txt_size); 649 | lcd.print(token); 650 | lcd.setTextSize(2); // Back to default text size 651 | } 652 | 653 | void clear_screen() { 654 | lcd.clear(); 655 | lcd.startWrite(); 656 | } 657 | 658 | void arrow(int x1, int y1, int x2, int y2, int alength, int awidth, int colour) { 659 | float distance; 660 | int dx, dy, x2o, y2o, x3, y3, x4, y4, k; 661 | distance = sqrt(pow((x1 - x2), 2) + pow((y1 - y2), 2)); 662 | dx = x2 + (x1 - x2) * alength / distance; 663 | dy = y2 + (y1 - y2) * alength / distance; 664 | k = awidth / alength; 665 | x2o = x2 - dx; 666 | y2o = dy - y2; 667 | x3 = y2o * k + dx; 668 | y3 = x2o * k + dy; 669 | // 670 | x4 = dx - y2o * k; 671 | y4 = dy - x2o * k; 672 | lcd.drawLine(x1, y1, x2, y2, colour); 673 | lcd.drawLine(x1, y1, dx, dy, colour); 674 | lcd.drawLine(x3, y3, x4, y4, colour); 675 | lcd.drawLine(x3, y3, x2, y2, colour); 676 | lcd.drawLine(x2, y2, x4, y4, colour); 677 | } 678 | 679 | void draw_veering_arrow(int a_direction) { 680 | int dx = (diameter * 0.75 * cos((a_direction - 90) * 3.14 / 180)) + centreX; 681 | int dy = (diameter * 0.75 * sin((a_direction - 90) * 3.14 / 180)) + centreY; 682 | arrow(centreX, centreY, dx, dy, 2, 5, RED); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 683 | } 684 | 685 | void Draw_Compass_Rose() { 686 | int dxo, dyo, dxi, dyi; 687 | lcd.drawCircle(centreX, centreY, diameter, GREEN); // Draw compass circle 688 | // lcd.fillCircle(centreX,centreY,diameter,GREY); // Draw compass circle 689 | lcd.drawRoundRect((centreX - 67), (centreY - 64), (diameter + 75), (centreY + diameter / 2 + 40), 10, YELLOW); // Draw compass rose 690 | lcd.drawLine(0, 139, 342, 139, YELLOW); // Separating line for relative-humidity, temp, windchill, temp-index and dewpoint 691 | lcd.drawLine(198, 139, 198, 246, YELLOW); // Separating vertical line for relative-humidity, temp, windchill, temp-index and dewpoint 692 | for (float i = 0; i < 360; i = i + 22.5) { 693 | dxo = diameter * cos((i - 90) * 3.14 / 180); 694 | dyo = diameter * sin((i - 90) * 3.14 / 180); 695 | dxi = dxo * 0.9; 696 | dyi = dyo * 0.9; 697 | lcd.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 698 | dxo = dxo * 0.5; 699 | dyo = dyo * 0.5; 700 | dxi = dxo * 0.9; 701 | dyi = dyo * 0.9; 702 | lcd.drawLine(dxo + centreX, dyo + centreY, dxi + centreX, dyi + centreY, YELLOW); // u8g.drawLine(centreX,centreY,dx,dy); would be the u8g drawing equivalent 703 | } 704 | display_item((centreX - 2), (centreY - 33), "N", GREEN, 1); 705 | display_item((centreX - 2), (centreY + 26), "S", GREEN, 1); 706 | display_item((centreX + 30), (centreY - 3), "E", GREEN, 1); 707 | display_item((centreX - 32), (centreY - 3), "W", GREEN, 1); 708 | } 709 | 710 | boolean valid_cloud_report(String temp_strA) { 711 | if (temp_strA.startsWith("BKN") || 712 | temp_strA.startsWith("CLR") || 713 | temp_strA.startsWith("FEW") || 714 | temp_strA.startsWith("NCD") || 715 | temp_strA.startsWith("NSC") || 716 | temp_strA.startsWith("OVC") || 717 | temp_strA.startsWith("SCT") || 718 | temp_strA.startsWith("SKC") || 719 | temp_strA.endsWith("CB") || 720 | temp_strA.endsWith("TCU")) { 721 | return true; 722 | } else { 723 | return false; 724 | } 725 | } 726 | 727 | void display_status() { 728 | display_progress("Initialising", 50); 729 | lcd.setTextSize(2); 730 | display_progress("Waiting for IP address", 75); 731 | while (WiFi.status() != WL_CONNECTED) { 732 | delay(10); 733 | } 734 | display_progress("Ready...", 100); 735 | clear_screen(); 736 | } 737 | 738 | void display_progress (String title, int percent) { 739 | int title_pos = (320 - title.length() * 12) / 2; // Centre title 740 | int x_pos = 40; int y_pos = 105; 741 | int bar_width = 250; int bar_height = 15; 742 | display_item(title_pos * 1.5, (y_pos - 20) * 1.33, title, GREEN, 2); 743 | lcd.drawRoundRect(x_pos * 1.5, y_pos * 1.33, bar_width + 2, bar_height, 5, YELLOW); // Draw progress bar outline 744 | lcd.fillRoundRect((x_pos + 2) * 1.5, (y_pos + 1) * 1.33, percent * bar_width / 100 - 2, bar_height - 3, 4, BLUE); // Draw progress 745 | delay(2000); 746 | lcd.fillRect((x_pos - 30) * 1.5, (y_pos - 20) * 1.33, 320, 16, BLACK); // Clear titles 747 | lcd.startWrite(); 748 | } 749 | 750 | /* Meaning of various codes 751 | Moderate/heavy rain RERA 752 | Moderate/heavy snow RESN 753 | Moderate/heavy small hail REGS 754 | Moderate/heavy snow pellets REGS Moderate/heavy ice pellets REPL 755 | Moderate/heavy hail REGR 756 | Moderate/heavy snow grains RESG 757 | 758 | Intensity Description Precipitation Obscuration Other 759 | - Light MI Shallow DZ Drizzle BR Mist PO Well developed dust / sand whirls 760 | Moderate PR Partial RA Rain FG Fog SQ Squalls 761 | + Heavy BC Patches SN Snow FU Smoke FC Funnel clouds inc tornadoes or waterspouts 762 | VC Vicinity DR Low drifting SG Snow grains VA Volcanic ash SS Sandstorm 763 | BL Blowing IC Ice crystals DU Widespread dust DS Duststorm 764 | SH Showers PL Ice pellets SA Sand 765 | TS Thunderstorm GR Hail HZ Haze 766 | FZ Freezing GS Small hail PY Spray 767 | UP Unknown 768 | 769 | e.g. -SHRA - Light showers of rain 770 | TSRA - Thunderstorms and rain. 771 | */ 772 | 773 | 774 | 775 | /* Returns the following METAR data from the server address 776 | 777 | - 778 | 18793217 779 | 780 | 781 | 782 | 783 | 4 784 | - 785 | - 786 | EGLL 241950Z AUTO 07009KT 9999 NCD 03/M02 Q1023 787 | EGLL 788 | 2018-02-24T19:50:00Z 789 | 51.48 790 | -0.45 791 | 3.0 792 | -2.0 793 | 70 794 | 9 795 | 6.21 796 | 30.206694 797 | - 798 | TRUE 799 | 800 | 801 | VFR 802 | METAR 803 | 24.0 804 | 805 | - 806 | EGLL 241920Z AUTO 07008KT 9999 NCD 03/M03 Q1023 807 | EGLL 808 | 2018-02-24T19:20:00Z 809 | 51.48 810 | -0.45 811 | 3.0 812 | -3.0 813 | 70 814 | 8 815 | 6.21 816 | 30.206694 817 | - 818 | TRUE 819 | 820 | 821 | VFR 822 | METAR 823 | 24.0 824 | 825 | 826 | 827 | 828 | EGDM 121621Z 35005KT 9999 5000SW BR FEW001 BKN002 11/11 Q1014 AMB 829 | EGDM 830 | 2016-11-12T16:21:00Z 831 | 51.17 832 | -1.75 833 | 11.0 834 | 11.0 835 | 350 836 | 5 837 | 6.21 838 | 29.940945 839 | BR 840 | 841 | 842 | LIFR 843 | METAR 844 | 124.0 845 | 846 | */ 847 | --------------------------------------------------------------------------------