adafruit_epd_weather.ino
1 // SPDX-FileCopyrightText: 2019 Dan Cogliano for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 #include <time.h> 6 #include <Adafruit_GFX.h> // Core graphics library 7 #include <Adafruit_ThinkInk.h> 8 #include <Adafruit_NeoPixel.h> 9 #include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson 10 #include <SPI.h> 11 #include <WiFiNINA.h> 12 13 #include "secrets.h" 14 #include "OpenWeatherMap.h" 15 16 #include "Fonts/meteocons48pt7b.h" 17 #include "Fonts/meteocons24pt7b.h" 18 #include "Fonts/meteocons20pt7b.h" 19 #include "Fonts/meteocons16pt7b.h" 20 21 #include "Fonts/moon_phases20pt7b.h" 22 #include "Fonts/moon_phases36pt7b.h" 23 24 #include <Fonts/FreeSans9pt7b.h> 25 #include <Fonts/FreeSans12pt7b.h> 26 #include <Fonts/FreeSans18pt7b.h> 27 #include <Fonts/FreeSansBold12pt7b.h> 28 #include <Fonts/FreeSansBold24pt7b.h> 29 30 #define SRAM_CS 8 31 #define EPD_CS 10 32 #define EPD_DC 9 33 #define EPD_RESET -1 34 #define EPD_BUSY -1 35 36 #define NEOPIXELPIN 40 37 38 // This is for the 2.7" tricolor EPD 39 ThinkInk_270_Tricolor_C44 gfx(EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY); 40 41 AirliftOpenWeatherMap owclient(&Serial); 42 OpenWeatherMapCurrentData owcdata; 43 OpenWeatherMapForecastData owfdata[3]; 44 45 Adafruit_NeoPixel neopixel = Adafruit_NeoPixel(1, NEOPIXELPIN, NEO_GRB + NEO_KHZ800); 46 47 const char *moonphasenames[29] = { 48 "New Moon", 49 "Waxing Crescent", 50 "Waxing Crescent", 51 "Waxing Crescent", 52 "Waxing Crescent", 53 "Waxing Crescent", 54 "Waxing Crescent", 55 "Quarter", 56 "Waxing Gibbous", 57 "Waxing Gibbous", 58 "Waxing Gibbous", 59 "Waxing Gibbous", 60 "Waxing Gibbous", 61 "Waxing Gibbous", 62 "Full Moon", 63 "Waning Gibbous", 64 "Waning Gibbous", 65 "Waning Gibbous", 66 "Waning Gibbous", 67 "Waning Gibbous", 68 "Waning Gibbous", 69 "Last Quarter", 70 "Waning Crescent", 71 "Waning Crescent", 72 "Waning Crescent", 73 "Waning Crescent", 74 "Waning Crescent", 75 "Waning Crescent", 76 "Waning Crescent" 77 }; 78 79 int8_t readButtons(void) { 80 uint16_t reading = analogRead(A3); 81 //Serial.println(reading); 82 83 if (reading > 600) { 84 return 0; // no buttons pressed 85 } 86 if (reading > 400) { 87 return 4; // button D pressed 88 } 89 if (reading > 250) { 90 return 3; // button C pressed 91 } 92 if (reading > 125) { 93 return 2; // button B pressed 94 } 95 return 1; // Button A pressed 96 } 97 98 bool wifi_connect(){ 99 100 Serial.print("Connecting to WiFi... "); 101 102 WiFi.setPins(SPIWIFI_SS, SPIWIFI_ACK, ESP32_RESETN, ESP32_GPIO0, &SPIWIFI); 103 104 // check for the WiFi module: 105 if(WiFi.status() == WL_NO_MODULE) { 106 Serial.println("Communication with WiFi module failed!"); 107 displayError("Communication with WiFi module failed!"); 108 while(true); 109 } 110 111 String fv = WiFi.firmwareVersion(); 112 if (fv < "1.0.0") { 113 Serial.println("Please upgrade the firmware"); 114 } 115 116 neopixel.setPixelColor(0, neopixel.Color(0, 0, 255)); 117 neopixel.show(); 118 if(WiFi.begin(WIFI_SSID, WIFI_PASSWORD) == WL_CONNECT_FAILED) 119 { 120 Serial.println("WiFi connection failed!"); 121 displayError("WiFi connection failed!"); 122 return false; 123 } 124 125 int wifitimeout = 15; 126 int wifistatus; 127 while ((wifistatus = WiFi.status()) != WL_CONNECTED && wifitimeout > 0) { 128 delay(1000); 129 Serial.print("."); 130 wifitimeout--; 131 } 132 if(wifitimeout == 0) 133 { 134 Serial.println("WiFi connection timeout with error " + String(wifistatus)); 135 displayError("WiFi connection timeout with error " + String(wifistatus)); 136 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 137 neopixel.show(); 138 return false; 139 } 140 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 141 neopixel.show(); 142 Serial.println("Connected"); 143 return true; 144 } 145 146 void wget(String &url, int port, char *buff) 147 { 148 int pos1 = url.indexOf("/",0); 149 int pos2 = url.indexOf("/",8); 150 String host = url.substring(pos1+2,pos2); 151 String path = url.substring(pos2); 152 Serial.println("to wget(" + host + "," + path + "," + port + ")"); 153 wget(host, path, port, buff); 154 } 155 156 void wget(String &host, String &path, int port, char *buff) 157 { 158 //WiFiSSLClient client; 159 WiFiClient client; 160 161 neopixel.setPixelColor(0, neopixel.Color(0, 0, 255)); 162 neopixel.show(); 163 client.stop(); 164 if (client.connect(host.c_str(), port)) { 165 Serial.println("connected to server"); 166 // Make a HTTP request: 167 client.println(String("GET ") + path + String(" HTTP/1.0")); 168 client.println("Host: " + host); 169 client.println("Connection: close"); 170 client.println(); 171 172 uint32_t bytes = 0; 173 int capturepos = 0; 174 bool capture = false; 175 int linelength = 0; 176 char lastc = '\0'; 177 while(true) 178 { 179 while (client.available()) { 180 char c = client.read(); 181 //Serial.print(c); 182 if((c == '\n') && (lastc == '\r')) 183 { 184 if(linelength == 0) 185 { 186 capture = true; 187 } 188 linelength = 0; 189 } 190 else if(capture) 191 { 192 buff[capturepos++] = c; 193 //Serial.write(c); 194 } 195 else 196 { 197 if((c != '\n') && (c != '\r')) 198 linelength++; 199 } 200 lastc = c; 201 bytes++; 202 } 203 204 // if the server's disconnected, stop the client: 205 if (!client.connected()) { 206 //Serial.println(); 207 Serial.println("disconnecting from server."); 208 client.stop(); 209 buff[capturepos] = '\0'; 210 Serial.println("captured " + String(capturepos) + " bytes"); 211 break; 212 } 213 } 214 } 215 else 216 { 217 Serial.println("problem connecting to " + host + ":" + String(port)); 218 buff[0] = '\0'; 219 } 220 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 221 neopixel.show(); 222 } 223 224 int getStringLength(String s) 225 { 226 int16_t x = 0, y = 0; 227 uint16_t w, h; 228 gfx.getTextBounds(s, 0, 0, &x, &y, &w, &h); 229 return w + x; 230 } 231 232 /* 233 return value is percent of moon cycle ( from 0.0 to 0.999999), i.e.: 234 235 0.0: New Moon 236 0.125: Waxing Crescent Moon 237 0.25: Quarter Moon 238 0.375: Waxing Gibbous Moon 239 0.5: Full Moon 240 0.625: Waning Gibbous Moon 241 0.75: Last Quarter Moon 242 0.875: Waning Crescent Moon 243 244 */ 245 float getMoonPhase(time_t tdate) 246 { 247 248 time_t newmoonref = 1263539460; //known new moon date (2010-01-15 07:11) 249 // moon phase is 29.5305882 days, which is 2551442.82048 seconds 250 float phase = abs( tdate - newmoonref) / (double)2551442.82048; 251 phase -= (int)phase; // leave only the remainder 252 if(newmoonref > tdate) 253 phase = 1 - phase; 254 return phase; 255 } 256 257 void displayError(String str) 258 { 259 // show error on display 260 neopixel.setPixelColor(0, neopixel.Color(255, 0, 0)); 261 neopixel.show(); 262 263 Serial.println(str); 264 265 gfx.setTextColor(EPD_BLACK); 266 gfx.powerUp(); 267 gfx.clearBuffer(); 268 gfx.setTextWrap(true); 269 gfx.setCursor(10,60); 270 gfx.setFont(&FreeSans12pt7b); 271 gfx.print(str); 272 gfx.display(); 273 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 274 neopixel.show(); 275 } 276 277 void displayHeading(OpenWeatherMapCurrentData &owcdata) 278 { 279 280 time_t local = owcdata.observationTime + owcdata.timezone; 281 struct tm *timeinfo = gmtime(&local); 282 char datestr[80]; 283 // date 284 //strftime(datestr,80,"%a, %d %b %Y",timeinfo); 285 strftime(datestr,80,"%a, %b %d",timeinfo); 286 gfx.setFont(&FreeSans18pt7b); 287 gfx.setCursor((gfx.width()-getStringLength(datestr))/2,30); 288 gfx.print(datestr); 289 290 // city 291 strftime(datestr,80,"%A",timeinfo); 292 gfx.setFont(&FreeSansBold12pt7b); 293 gfx.setCursor((gfx.width()-getStringLength(owcdata.cityName))/2,60); 294 gfx.print(owcdata.cityName); 295 } 296 297 void displayForecastDays(OpenWeatherMapCurrentData &owcdata, OpenWeatherMapForecastData owfdata[], int count = 3) 298 { 299 for(int i=0; i < count; i++) 300 { 301 // day 302 303 time_t local = owfdata[i].observationTime + owcdata.timezone; 304 struct tm *timeinfo = gmtime(&local); 305 char strbuff[80]; 306 strftime(strbuff,80,"%I",timeinfo); 307 String datestr = String(atoi(strbuff)); 308 strftime(strbuff,80,"%p",timeinfo); 309 // convert AM/PM to lowercase 310 strbuff[0] = tolower(strbuff[0]); 311 strbuff[1] = tolower(strbuff[1]); 312 datestr = datestr + " " + String(strbuff); 313 gfx.setFont(&FreeSans9pt7b); 314 gfx.setCursor(i*gfx.width()/3 + (gfx.width()/3-getStringLength(datestr))/2,94); 315 gfx.print(datestr); 316 317 // weather icon 318 String wicon = owclient.getMeteoconIcon(owfdata[i].icon); 319 gfx.setFont(&meteocons20pt7b); 320 gfx.setCursor(i*gfx.width()/3 + (gfx.width()/3-getStringLength(wicon))/2,134); 321 gfx.print(wicon); 322 323 // weather main description 324 gfx.setFont(&FreeSans9pt7b); 325 gfx.setCursor(i*gfx.width()/3 + (gfx.width()/3-getStringLength(owfdata[i].main))/2,154); 326 gfx.print(owfdata[i].main); 327 328 // temperature 329 int itemp = (int)(owfdata[i].temp + .5); 330 int color = EPD_BLACK; 331 if((OWM_METRIC && itemp >= METRIC_HOT)|| (!OWM_METRIC && itemp >= ENGLISH_HOT)) 332 color = EPD_RED; 333 gfx.setTextColor(color); 334 gfx.setFont(&FreeSans9pt7b); 335 gfx.setCursor(i*gfx.width()/3 + (gfx.width()/3-getStringLength(String(itemp)))/2,172); 336 gfx.print(itemp); 337 gfx.drawCircle(i*gfx.width()/3 + (gfx.width()/3-getStringLength(String(itemp)))/2 + getStringLength(String(itemp)) + 6,163,3,color); 338 gfx.drawCircle(i*gfx.width()/3 + (gfx.width()/3-getStringLength(String(itemp)))/2 + getStringLength(String(itemp)) + 6,163,2,color); 339 gfx.setTextColor(EPD_BLACK); 340 } 341 } 342 343 void displayForecast(OpenWeatherMapCurrentData &owcdata, OpenWeatherMapForecastData owfdata[], int count = 3) 344 { 345 gfx.powerUp(); 346 gfx.clearBuffer(); 347 neopixel.setPixelColor(0, neopixel.Color(0, 255, 0)); 348 neopixel.show(); 349 350 gfx.setTextColor(EPD_BLACK); 351 displayHeading(owcdata); 352 353 displayForecastDays(owcdata, owfdata, count); 354 gfx.display(); 355 gfx.powerDown(); 356 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 357 neopixel.show(); 358 } 359 360 void displayAllWeather(OpenWeatherMapCurrentData &owcdata, OpenWeatherMapForecastData owfdata[], int count = 3) 361 { 362 gfx.powerUp(); 363 gfx.clearBuffer(); 364 neopixel.setPixelColor(0, neopixel.Color(0, 255, 0)); 365 neopixel.show(); 366 367 gfx.setTextColor(EPD_BLACK); 368 369 // date string 370 time_t local = owcdata.observationTime + owcdata.timezone; 371 struct tm *timeinfo = gmtime(&local); 372 char datestr[80]; 373 // date 374 //strftime(datestr,80,"%a, %d %b %Y",timeinfo); 375 strftime(datestr,80,"%a, %b %d %Y",timeinfo); 376 gfx.setFont(&FreeSans9pt7b); 377 gfx.setCursor((gfx.width()-getStringLength(datestr))/2,14); 378 gfx.print(datestr); 379 380 // weather icon 381 String wicon = owclient.getMeteoconIcon(owcdata.icon); 382 gfx.setFont(&meteocons24pt7b); 383 gfx.setCursor((gfx.width()/3-getStringLength(wicon))/2,56); 384 gfx.print(wicon); 385 386 // weather main description 387 gfx.setFont(&FreeSans9pt7b); 388 gfx.setCursor((gfx.width()/3-getStringLength(owcdata.main))/2,72); 389 gfx.print(owcdata.main); 390 391 // temperature 392 gfx.setFont(&FreeSansBold24pt7b); 393 int itemp = owcdata.temp + .5; 394 int color = EPD_BLACK; 395 if((OWM_METRIC && (int)itemp >= METRIC_HOT)|| (!OWM_METRIC && (int)itemp >= ENGLISH_HOT)) 396 color = EPD_RED; 397 gfx.setTextColor(color); 398 gfx.setCursor(gfx.width()/3 + (gfx.width()/3-getStringLength(String(itemp)))/2,58); 399 gfx.print(itemp); 400 gfx.setTextColor(EPD_BLACK); 401 402 // draw temperature degree as a circle (not available as font character 403 gfx.drawCircle(gfx.width()/3 + (gfx.width()/3 + getStringLength(String(itemp)))/2 + 8, 58-30,4,color); 404 gfx.drawCircle(gfx.width()/3 + (gfx.width()/3 + getStringLength(String(itemp)))/2 + 8, 58-30,3,color); 405 406 // draw moon 407 // draw Moon Phase 408 float moonphase = getMoonPhase(owcdata.observationTime); 409 int moonage = 29.5305882 * moonphase; 410 //Serial.println("moon age: " + String(moonage)); 411 // convert to appropriate icon 412 String moonstr = String((char)((int)'A' + (int)(moonage*25./30))); 413 gfx.setFont(&moon_phases20pt7b); 414 // font lines look a little thin at this size, drawing it a few times to thicken the lines 415 gfx.setCursor(2*gfx.width()/3 + (gfx.width()/3-getStringLength(moonstr))/2,56); 416 gfx.print(moonstr); 417 gfx.setCursor(2*gfx.width()/3 + (gfx.width()/3-getStringLength(moonstr))/2+1,56); 418 gfx.print(moonstr); 419 gfx.setCursor(2*gfx.width()/3 + (gfx.width()/3-getStringLength(moonstr))/2,56-1); 420 gfx.print(moonstr); 421 422 // draw moon phase name 423 int currentphase = moonphase * 28. + .5; 424 gfx.setFont(); // system font (smallest available) 425 gfx.setCursor(2*gfx.width()/3 + max(0,(gfx.width()/3 - getStringLength(moonphasenames[currentphase]))/2),62); 426 gfx.print(moonphasenames[currentphase]); 427 428 429 displayForecastDays(owcdata, owfdata, count); 430 gfx.display(); 431 gfx.powerDown(); 432 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 433 neopixel.show(); 434 435 } 436 437 void displayCurrentConditions(OpenWeatherMapCurrentData &owcdata) 438 { 439 gfx.powerUp(); 440 gfx.clearBuffer(); 441 neopixel.setPixelColor(0, neopixel.Color(0, 255, 0)); 442 neopixel.show(); 443 444 gfx.setTextColor(EPD_BLACK); 445 displayHeading(owcdata); 446 447 // weather icon 448 String wicon = owclient.getMeteoconIcon(owcdata.icon); 449 gfx.setFont(&meteocons48pt7b); 450 gfx.setCursor((gfx.width()/2-getStringLength(wicon))/2,156); 451 gfx.print(wicon); 452 453 // weather main description 454 gfx.setFont(&FreeSans9pt7b); 455 gfx.setCursor(gfx.width()/2 + (gfx.width()/2-getStringLength(owcdata.main))/2,160); 456 gfx.print(owcdata.main); 457 458 // temperature 459 gfx.setFont(&FreeSansBold24pt7b); 460 int itemp = owcdata.temp + .5; 461 int color = EPD_BLACK; 462 if((OWM_METRIC && (int)itemp >= METRIC_HOT)|| (!OWM_METRIC && (int)itemp >= ENGLISH_HOT)) 463 color = EPD_RED; 464 gfx.setTextColor(color); 465 gfx.setCursor(gfx.width()/2 + (gfx.width()/2-getStringLength(String(itemp)))/2,130); 466 gfx.print(itemp); 467 gfx.setTextColor(EPD_BLACK); 468 469 // draw temperature degree as a circle (not available as font character 470 gfx.drawCircle(gfx.width()/2 + (gfx.width()/2 + getStringLength(String(itemp)))/2 + 10, 130-26,4,color); 471 gfx.drawCircle(gfx.width()/2 + (gfx.width()/2 + getStringLength(String(itemp)))/2 + 10, 130-26,3,color); 472 473 gfx.display(); 474 gfx.powerDown(); 475 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 476 neopixel.show(); 477 } 478 479 void displaySunMoon(OpenWeatherMapCurrentData &owcdata) 480 { 481 482 gfx.powerUp(); 483 gfx.clearBuffer(); 484 neopixel.setPixelColor(0, neopixel.Color(0, 255, 0)); 485 neopixel.show(); 486 487 gfx.setTextColor(EPD_BLACK); 488 displayHeading(owcdata); 489 490 // draw Moon Phase 491 float moonphase = getMoonPhase(owcdata.observationTime); 492 int moonage = 29.5305882 * moonphase; 493 // convert to appropriate icon 494 String moonstr = String((char)((int)'A' + (int)(moonage*25./30))); 495 gfx.setFont(&moon_phases36pt7b); 496 gfx.setCursor((gfx.width()/3-getStringLength(moonstr))/2,140); 497 gfx.print(moonstr); 498 499 // draw moon phase name 500 int currentphase = moonphase * 28. + .5; 501 gfx.setFont(&FreeSans9pt7b); 502 gfx.setCursor(gfx.width()/3 + max(0,(gfx.width()*2/3 - getStringLength(moonphasenames[currentphase]))/2),110); 503 gfx.print(moonphasenames[currentphase]); 504 505 // draw sunrise/sunset 506 507 // sunrise/sunset times 508 // sunrise 509 510 time_t local = owcdata.sunrise + owcdata.timezone + 30; // round to nearest minute 511 struct tm *timeinfo = gmtime(&local); 512 char strbuff[80]; 513 strftime(strbuff,80,"%I",timeinfo); 514 String datestr = String(atoi(strbuff)); 515 strftime(strbuff,80,":%M %p",timeinfo); 516 datestr = datestr + String(strbuff) + " - "; 517 // sunset 518 local = owcdata.sunset + owcdata.timezone + 30; // round to nearest minute 519 timeinfo = gmtime(&local); 520 strftime(strbuff,80,"%I",timeinfo); 521 datestr = datestr + String(atoi(strbuff)); 522 strftime(strbuff,80,":%M %p",timeinfo); 523 datestr = datestr + String(strbuff); 524 525 gfx.setFont(&FreeSans9pt7b); 526 int datestrlen = getStringLength(datestr); 527 int xpos = (gfx.width() - datestrlen)/2; 528 gfx.setCursor(xpos,166); 529 gfx.print(datestr); 530 531 // draw sunrise icon 532 // sun icon is "B" 533 String wicon = "B"; 534 gfx.setFont(&meteocons16pt7b); 535 gfx.setCursor(xpos - getStringLength(wicon) - 12,174); 536 gfx.print(wicon); 537 538 // draw sunset icon 539 // sunset icon is "A" 540 wicon = "A"; 541 gfx.setCursor(xpos + datestrlen + 12,174); 542 gfx.print(wicon); 543 544 gfx.display(); 545 gfx.powerDown(); 546 neopixel.setPixelColor(0, neopixel.Color(0, 0, 0)); 547 neopixel.show(); 548 } 549 550 void setup() { 551 neopixel.begin(); 552 neopixel.show(); 553 554 gfx.begin(); 555 Serial.println("ePaper display initialized"); 556 gfx.setRotation(2); 557 gfx.setTextWrap(false); 558 559 } 560 561 void loop() { 562 char data[4000]; 563 static uint32_t timer = millis(); 564 static uint8_t lastbutton = 1; 565 static bool firsttime = true; 566 567 int button = readButtons(); 568 569 // update weather data at specified interval or when button 4 is pressed 570 if((millis() >= (timer + 1000*60*UPDATE_INTERVAL)) || (button == 4) || firsttime) 571 { 572 Serial.println("getting weather data"); 573 firsttime = false; 574 timer = millis(); 575 int retry = 6; 576 while(!wifi_connect()) 577 { 578 delay(5000); 579 retry--; 580 if(retry < 0) 581 { 582 displayError("Can not connect to WiFi, press reset to restart"); 583 while(1); 584 } 585 } 586 String urlc = owclient.buildUrlCurrent(OWM_KEY,OWM_LOCATION); 587 Serial.println(urlc); 588 retry = 6; 589 do 590 { 591 retry--; 592 wget(urlc,80,data); 593 if(strlen(data) == 0 && retry < 0) 594 { 595 displayError("Can not get weather data, press reset to restart"); 596 while(1); 597 } 598 } 599 while(strlen(data) == 0); 600 Serial.println("data retrieved:"); 601 Serial.println(data); 602 retry = 6; 603 while(!owclient.updateCurrent(owcdata,data)) 604 { 605 retry--; 606 if(retry < 0) 607 { 608 displayError(owclient.getError()); 609 while(1); 610 } 611 delay(5000); 612 } 613 614 String urlf = owclient.buildUrlForecast(OWM_KEY,OWM_LOCATION); 615 Serial.println(urlf); 616 wget(urlf,80,data); 617 Serial.println("data retrieved:"); 618 Serial.println(data); 619 if(!owclient.updateForecast(owfdata[0],data,0)) 620 { 621 displayError(owclient.getError()); 622 while(1); 623 } 624 if(!owclient.updateForecast(owfdata[1],data,2)) 625 { 626 displayError(owclient.getError()); 627 while(1); 628 } 629 if(!owclient.updateForecast(owfdata[2],data,4)) 630 { 631 displayError(owclient.getError()); 632 while(1); 633 } 634 635 switch(lastbutton) 636 { 637 case 1: 638 displayAllWeather(owcdata,owfdata,3); 639 break; 640 case 2: 641 displayCurrentConditions(owcdata); 642 break; 643 case 3: 644 displaySunMoon(owcdata); 645 break; 646 } 647 } 648 649 if (button == 0) { 650 return; 651 } 652 653 Serial.print("Button "); Serial.print(button); Serial.println(" pressed"); 654 655 if (button == 1) { 656 displayAllWeather(owcdata,owfdata,3); 657 lastbutton = button; 658 } 659 if (button == 2) { 660 //displayForecast(owcdata,owfdata,3); 661 displayCurrentConditions(owcdata); 662 lastbutton = button; 663 } 664 if (button == 3) { 665 displaySunMoon(owcdata); 666 lastbutton = button; 667 } 668 669 // wait until button is released 670 while (readButtons()) { 671 delay(10); 672 } 673 674 }