├── ESP8266-Hobo-Clock.ino ├── LICENSE └── README.md /ESP8266-Hobo-Clock.ino: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | The Hobo's Animated IoT Clock 3 | 4 | Manifesto: giving trust to unreliable time providers 5 | isn't worse than trusting potentially unreliable 6 | access points with personal credentials. 7 | 8 | This clock has not RTC module and doesn't need to 9 | reach a NTP server to adjust its time. It uses WiFi 10 | access points openness in order to extract the 11 | date/time from the "Date" HTTP header that may be 12 | sent by some captive portals. So it will scan all 13 | nearby open WiFi AP until it finds a suitable response. 14 | 15 | Breakout: 16 | 17 | - Wemos Mini D1 (ESP8266) 18 | - SSD1306 OLED (128x64 Monochrome) 19 | - TP4056 LiPo Charger 20 | - 3.7v LiPo (270mAh, tiny size) 21 | 22 | Boot sequence: 23 | 24 | - Enumerate Open WiFi AP 25 | - Get an IP address 26 | - Connect to captive portal 27 | - Look for a "Date" HTTP header (and against common 28 | sense, trust its value) 29 | - Adjust the clock accordingly (at 0:40) 30 | 31 | Expecting unknown networks to provide a HTTP header 32 | value and relying on it to estimate time is like 33 | counting on other people's wealth to survive, hence 34 | the Hobo name. 35 | 36 | The exclusive use of open access points removes the 37 | hassle of hardcoding SSID/password into the sketch 38 | but also compensates its lack of auth plus the fact 39 | that the optional NTP connexion attempt will always 40 | fail, unless the AP acts as. 41 | 42 | The Pong animation with a bouncing rotating cube is 43 | there to cut on the boringness of the clock but also 44 | to demonstrate how this tiny OLED can animate fast 45 | (nearly 60fps). 46 | 47 | Since it has trust issues, don't trust this clock 48 | more than you would trust a stranger's watch! The 49 | available space and power consumption won't let it 50 | run more than a couple of hours on the LiPo anyway. 51 | 52 | Ported to NodeMCU by tobozo (c+) Nov 2016 53 | 54 | https://youtu.be/RZ90ruADrI4 55 | 56 | ****************************************************/ 57 | 58 | // add some verbosity 59 | #define DEBUG false 60 | #define DEBUGGYRO false 61 | #define DEBUGNTP false 62 | #define DEBUGHTTP false 63 | #define DEBUGWIFI false 64 | #define DEBUGOTA false 65 | 66 | // using a gyro for cube animation (enable this for wearable clock) 67 | // 68 | #define USE_GYRO false 69 | 70 | // hardcoding mac address, use this at home as if you're hosting your 71 | // own hobo. NTP will be used in this case. 72 | // Set to false if you encounter problems 73 | #define HC_MAC true 74 | 75 | #if USE_GYRO == true 76 | // https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050 77 | #include "MPU6050_6Axis_MotionApps20.h" 78 | #endif 79 | #include 80 | #include 81 | #include 82 | #include 83 | #include 84 | #include 85 | #include 86 | 87 | 88 | // hardcoded mac case 89 | #if HC_MAC == true 90 | extern "C" { 91 | #include "user_interface.h" 92 | } 93 | /* override init function to inject fake mac address */ 94 | void initVariant() { 95 | // WARN: hardcoded mac address could create cross-device identity issues 96 | uint8_t mac[] = {0x60, 0x01, 0x94, 0x17, 0x8b, 0xb1}; 97 | wifi_set_macaddr(STATION_IF, &mac[0]); 98 | } 99 | // also set hardcoded Hostname. 100 | const char* ESPName = "esp8266-178bb1"; 101 | const char* ssid = "myhomewifi"; // Set this only if you're fool enough to hardcode it into an IoT 102 | const char* password = "myhomewifipassword"; // Set this only if you're fool enough to hardcode it into an IoT 103 | #endif 104 | 105 | // regular case 106 | #if HC_MAC == false 107 | // automatic generation o 108 | const char* ESPName = "HOBO-CLOCK-" + String(ESP.getChipId(), HEX); 109 | const char* ssid = ""; // Set this only if you're fool enough to hardcode it into an IoT 110 | const char* password = ""; // Set this only if you're fool enough to hardcode it into an IoT 111 | #endif 112 | 113 | 114 | // OTA updatable as long as the bool is true 115 | bool otaready = true; 116 | int otawait = 20000; // how long to wait for OTA at boot (millisec) 117 | int otanow; // time counter for OTA update 118 | 119 | 120 | // Accel and gyro data 121 | int16_t ax, ay, az, gx, gy, gz; 122 | double MMPI = 512*M_PI; 123 | 124 | // used for cube animation loop 125 | long int timeLast = -100, period = 1; 126 | // Overall scale and perspective distance 127 | uint8_t sZ = 4, scale = 6, scaleMax = 16; 128 | // screen center 129 | uint8_t centerX = 64; 130 | uint8_t centerY = 32; 131 | // Initialize cube point arrays 132 | double C1[] = { 1, 1, 1 }; 133 | double C2[] = { 1, 1, -1 }; 134 | double C3[] = { 1, -1, 1 }; 135 | double C4[] = { 1, -1, -1 }; 136 | double C5[] = { -1, 1, 1 }; 137 | double C6[] = { -1, 1, -1 }; 138 | double C7[] = { -1, -1, 1 }; 139 | double C8[] = { -1, -1, -1 }; 140 | // Initialize cube points coords 141 | uint8_t P1[] = { 0, 0 }; 142 | uint8_t P2[] = { 0, 0 }; 143 | uint8_t P3[] = { 0, 0 }; 144 | uint8_t P4[] = { 0, 0 }; 145 | uint8_t P5[] = { 0, 0 }; 146 | uint8_t P6[] = { 0, 0 }; 147 | uint8_t P7[] = { 0, 0 }; 148 | uint8_t P8[] = { 0, 0 }; 149 | 150 | // wifi scan progress bar 151 | uint8_t progress; 152 | int n = 0; // wifiscan index 153 | int con = 0; // wifi connection indicator 154 | // a host to visit in order to get the "Date:" HTTP header response 155 | // in case the NTP query failed (most of the time it will) 156 | const char* host = "google.com"; 157 | const char* pass = ""; 158 | String dateString = ""; 159 | bool dateprinted = false; 160 | 161 | 162 | //Define Pins 163 | #define OLED_RESET 4 164 | #define BEEPER 3 165 | #define CONTROL_A D1 166 | #define CONTROL_B D2 167 | 168 | //Define Visuals 169 | #define FONT_SIZE 2 170 | #define SCREEN_WIDTH 127 //real size minus 1, because coordinate system starts with 0 171 | #define SCREEN_HEIGHT 63 //real size minus 1, because coordinate system starts with 0 172 | #define PADDLE_WIDTH 4 173 | #define PADDLE_HEIGHT 10 174 | #define PADDLE_PADDING 10 175 | #define BALL_SIZE 16 176 | #define SCORE_PADDING 10 177 | 178 | #define EFFECT_SPEED 1 179 | #define MIN_Y_SPEED 1 180 | #define MAX_Y_SPEED 2 181 | 182 | 183 | int nstars=32;// Number of Stars 184 | int ncrementer = 0; 185 | double star[512][5];// Data structure to hold the position of all the stars 186 | int w=127;// Width of the viewport (aka the body width) 187 | int h=63;// Height of the viewport (aka the body height) 188 | int x=64;// Center of the width of the viewport (width/2) 189 | int y=32;// Center of the height of the viewport (height/2) 190 | int z=5;// Hypothetical z-value representing where we are on the screen 191 | int starRatio=8;// Just a constant effecting the way stars appear 192 | int starSpeed=1;// The speed of the star. Yes, all star's have the same speed. 193 | // Play around with the values for star speed, I noticed a cool effect if we made the star speed 0. Hence, I created a variable to save the star speed in those cases 194 | int starSpeedPrev=0; 195 | int opacity = 1;// Just a constant to hold the opacity 196 | int cursorX=64;// Mouse Positions 197 | int cursorY=32;// Mouse Positions 198 | 199 | 200 | int paddleLocationA = 0; 201 | int paddleLocationB = 0; 202 | 203 | float ballX = SCREEN_WIDTH/2; 204 | float ballY = SCREEN_HEIGHT/2; 205 | float ballSpeedX = 2; 206 | float ballSpeedY = 1; 207 | 208 | int lastPaddleLocationA = 0; 209 | int lastPaddleLocationB = 0; 210 | 211 | int scoreA = 0; 212 | int scoreB = 0; 213 | 214 | 215 | // NTP variables 216 | time_t prevDisplay = 0; // when the digital clock was displayed 217 | int hours, minutes, seconds; // for the results 218 | int lastSecond = -1; // need an impossible value for comparison 219 | int offset = 1; // timezone offset (relative to the ntp pool and geoposition) 220 | unsigned int localPort = 2390; // local port to listen for UDP packets 221 | IPAddress timeServerIP; // time.nist.gov NTP server address 222 | const char* ntpServerName = "fr.pool.ntp.org"; 223 | const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message 224 | byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets 225 | 226 | 227 | // A UDP instance to let us send and receive packets over UDP 228 | WiFiUDP udp; 229 | SSD1306 display(0x3c, D3, D4); 230 | OLEDDisplayUi ui ( &display ); 231 | #if USE_GYRO == true 232 | MPU6050 mpu; 233 | #endif 234 | 235 | 236 | void setstar(int i) { 237 | /* Initialize the stars. 238 | Since the ship is in the middle, we assume 239 | Each star has the following properties: 240 | 1.[0] Actual X-coordinate of position in prespective of ship 241 | 2.[1] Actual Y-coordinate of position in prespective of ship 242 | 3.[2] Actual Z-coordinate of position in prespective of ship 243 | 4.[3] Calculated X (represents X-coordinate on screen) 244 | 5.[4] Calculated Y (represents Y-coordinate on screen) 245 | */ 246 | star[i][0]=(random(100)*w*2-x*2)/100; 247 | star[i][1]=(random(100)*h*2-y*2)/100; 248 | star[i][2]=round(random(1)*z); 249 | star[i][3]=0; 250 | star[i][4]=0; 251 | } 252 | 253 | 254 | void starAnimate(){ 255 | int mouseX=cursorX-x; 256 | int mouseY=cursorY-y; 257 | display.clear(); 258 | 259 | if(ncrementer 75) continue; 269 | 270 | bool test=true; 271 | /* Save the stars calculated position so we can use it for drawing */ 272 | int starXPrev=star[i][3]; 273 | int starYPrev=star[i][4]; 274 | /* Update the Star */ 275 | star[i][0]+=mouseX>>4; 276 | star[i][1]+=mouseY>>4; 277 | star[i][2]-=starSpeed; 278 | /* Check the boundary conditions to make sure stars aren't offscreen. */ 279 | if(star[i][0]>x<<1){ 280 | star[i][0]-=w<<1; 281 | test=false; 282 | } 283 | if(star[i][0]<-x<<1){ 284 | star[i][0]+=w<<1; 285 | test=false; 286 | } 287 | if(star[i][1]>y<<1){ 288 | star[i][1]-=h<<1; 289 | test=false; 290 | } 291 | if(star[i][1]<-y<<1){ 292 | star[i][1]+=h<<1; 293 | test=false; 294 | } 295 | if(star[i][2]>z){ 296 | star[i][2]-=z; 297 | test=false; 298 | } 299 | if(star[i][2]<0){ 300 | star[i][2]+=z; 301 | test=false; 302 | } 303 | // Our calculated position and where the star is going to be drawn on the screen 304 | star[i][3]=x + (star[i][0]/star[i][2]) * starRatio; 305 | star[i][4]=y + (star[i][1]/star[i][2]) * starRatio; 306 | // Actually draw the object, if the star isn't offscreen 307 | if( starXPrev>0 && starXPrev0 && starYPrev0 && star[i][4]0 && star[i][4]w || starYPrev<0 || starYPrev>h 317 | || star[i][3]<0 || star[i][4]>w || star[i][3]<0 || star[i][4]>h 318 | ) { 319 | setstar(i); 320 | } 321 | 322 | } 323 | 324 | } 325 | 326 | 327 | void swsetup() { 328 | 329 | // force stars out from screen center 330 | //x=round(w/2); 331 | //y=round(h/2); 332 | 333 | z=(w+h)/2; 334 | //starColorRatio=1/z; 335 | // Initially we set the mouse to point to the middle of the viewport 336 | cursorX=x; 337 | cursorY=y; 338 | 339 | for(int i=0;i 0) { 489 | ballSpeedY += EFFECT_SPEED; 490 | } else { 491 | ballSpeedY -= EFFECT_SPEED; 492 | } 493 | } 494 | 495 | //limit to minimum speed 496 | if (ballSpeedY < MIN_Y_SPEED && ballSpeedY > -MIN_Y_SPEED) { 497 | if (ballSpeedY > 0) ballSpeedY = MIN_Y_SPEED; 498 | if (ballSpeedY < 0) ballSpeedY = -MIN_Y_SPEED; 499 | if (ballSpeedY == 0) ballSpeedY = oldBallSpeedY; 500 | } 501 | 502 | //limit to maximum speed 503 | if (ballSpeedY > MAX_Y_SPEED) ballSpeedY = MAX_Y_SPEED; 504 | if (ballSpeedY < -MAX_Y_SPEED) ballSpeedY = -MAX_Y_SPEED; 505 | } 506 | 507 | void calculateMovement() { 508 | 509 | paddleLocationA = ballY - PADDLE_HEIGHT/2; //map(controlA, 0, 738, 0, SCREEN_HEIGHT - PADDLE_HEIGHT); 510 | paddleLocationB = ballY - PADDLE_HEIGHT/2; //map(controlB, 0, 738, 0, SCREEN_HEIGHT - PADDLE_HEIGHT); 511 | 512 | int paddleSpeedA = paddleLocationA - lastPaddleLocationA; 513 | int paddleSpeedB = paddleLocationB - lastPaddleLocationB; 514 | 515 | ballX += ballSpeedX; 516 | ballY += ballSpeedY; 517 | 518 | //bounce from top and bottom 519 | if (ballY >= SCREEN_HEIGHT - BALL_SIZE || ballY <= 0) { 520 | ballSpeedY *= -1; 521 | } 522 | 523 | //bounce from paddle A 524 | if (ballX >= PADDLE_PADDING && ballX <= PADDLE_PADDING+BALL_SIZE && ballSpeedX < 0) { 525 | if (ballY > paddleLocationA - BALL_SIZE && ballY < paddleLocationA + PADDLE_HEIGHT) { 526 | ballSpeedX *= -1; 527 | addEffect(paddleSpeedA); 528 | } 529 | } 530 | 531 | //bounce from paddle B 532 | if (ballX >= SCREEN_WIDTH-PADDLE_WIDTH-PADDLE_PADDING-BALL_SIZE && ballX <= SCREEN_WIDTH-PADDLE_PADDING-BALL_SIZE && ballSpeedX > 0) { 533 | if (ballY > paddleLocationB - BALL_SIZE && ballY < paddleLocationB + PADDLE_HEIGHT) { 534 | ballSpeedX *= -1; 535 | addEffect(paddleSpeedB); 536 | } 537 | } 538 | 539 | //score points if ball hits wall behind paddle 540 | if (ballX >= SCREEN_WIDTH - BALL_SIZE || ballX <= 0) { 541 | if (ballSpeedX > 0) { 542 | scoreA++; 543 | ballX = SCREEN_WIDTH / 4; 544 | } 545 | if (ballSpeedX < 0) { 546 | scoreB++; 547 | ballX = SCREEN_WIDTH / 4 * 3; 548 | } 549 | } 550 | 551 | //set last paddle locations 552 | lastPaddleLocationA = paddleLocationA; 553 | lastPaddleLocationB = paddleLocationB; 554 | } 555 | 556 | 557 | // utility function for digital clock display: prints leading 0 558 | String twoDigits(int digits) { 559 | if(digits < 10) { 560 | String i = '0'+String(digits); 561 | return i; 562 | } 563 | else { 564 | return String(digits); 565 | } 566 | } 567 | 568 | 569 | void draw() { 570 | display.clear(); 571 | 572 | starAnimate(); 573 | 574 | //draw paddle A 575 | display.fillRect(PADDLE_PADDING,paddleLocationA+ (scale/2),PADDLE_WIDTH,PADDLE_HEIGHT+ (scale/2)); 576 | 577 | //draw paddle B 578 | display.fillRect(SCREEN_WIDTH-PADDLE_WIDTH-PADDLE_PADDING,paddleLocationB+ scale/2,PADDLE_WIDTH,PADDLE_HEIGHT+ (scale/2)); 579 | 580 | //draw center line 581 | for (int i=0; i comtimeout) { 662 | progress = 0; 663 | #if DEBUGWIFI==true 664 | Serial.print("*"); 665 | Serial.println(" Timeout"); 666 | #endif 667 | delayAnimate(10); 668 | return; 669 | } 670 | } 671 | progress = 0; 672 | con = n; 673 | #if DEBUGWIFI==true 674 | Serial.println(""); 675 | Serial.println("WiFi connected"); 676 | Serial.println("IP address: "); 677 | Serial.println(WiFi.localIP()); 678 | #endif 679 | delayAnimate(10); 680 | } 681 | 682 | // send an NTP request to the time server at the given address 683 | unsigned long sendNTPpacket(IPAddress& address) { 684 | #if DEBUGNTP==true 685 | Serial.println("sending NTP packet..."); 686 | #endif 687 | // set all bytes in the buffer to 0 688 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 689 | // Initialize values needed to form NTP request 690 | // (see URL above for details on the packets) 691 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 692 | packetBuffer[1] = 0; // Stratum, or type of clock 693 | packetBuffer[2] = 6; // Polling Interval 694 | packetBuffer[3] = 0xEC; // Peer Clock Precision 695 | // 8 bytes of zero for Root Delay & Root Dispersion 696 | packetBuffer[12] = 49; 697 | packetBuffer[13] = 0x4E; 698 | packetBuffer[14] = 49; 699 | packetBuffer[15] = 52; 700 | 701 | // all NTP fields have been given values, now 702 | // you can send a packet requesting a timestamp: 703 | udp.beginPacket(address, 123); //NTP requests are to port 123 704 | udp.write(packetBuffer, NTP_PACKET_SIZE); 705 | udp.endPacket(); 706 | } 707 | 708 | 709 | void tryHttpCon() { 710 | #if DEBUGHTTP==true 711 | Serial.print("connecting to "); 712 | Serial.println(host); 713 | #endif 714 | // Use WiFiClient class to create TCP connections 715 | WiFiClient client; 716 | const int httpPort = 80; 717 | if (!client.connect(host, httpPort)) { 718 | #if DEBUGHTTP==true 719 | Serial.println("connection failed"); 720 | #endif 721 | con = 0; 722 | return; 723 | } 724 | // We now create a URI for the request 725 | String url = "/"; 726 | #if DEBUGHTTP==true 727 | Serial.print("Requesting URL: "); 728 | Serial.println(url); 729 | #endif 730 | // This will send the request to the server 731 | client.print(String("GET ") + url + " HTTP/1.1\r\n" + 732 | "Host: " + host + "\r\n" + 733 | "Connection: close\r\n\r\n"); 734 | unsigned long timeout = millis(); 735 | while (client.available() == 0) { 736 | if (millis() - timeout > 5000) { 737 | #if DEBUGHTTP==true 738 | Serial.println(">>> Client Timeout !"); 739 | #endif 740 | client.stop(); 741 | con = 0; 742 | return; 743 | } 744 | } 745 | // Read all the lines of the reply from server and print them to Serial 746 | while(client.available()){ 747 | String line = client.readStringUntil('\r'); 748 | 749 | if(line.indexOf("Date: ")>-1) { 750 | dateString = line;//.split("Date:"); 751 | dateString.replace("Date: ", ""); 752 | } 753 | #if DEBUGHTTP==true 754 | Serial.print(line); 755 | #endif 756 | } 757 | #if DEBUGHTTP==true 758 | Serial.println(); 759 | Serial.println("closing connection"); 760 | #endif 761 | con = 0; 762 | } 763 | 764 | 765 | time_t getNtpTime() { 766 | udp.begin(localPort); 767 | WiFi.hostByName(ntpServerName, timeServerIP); 768 | sendNTPpacket(timeServerIP); // send an NTP packet to a time server 769 | 770 | uint32_t beginWait = millis(); 771 | while (millis() - beginWait < 1500) { 772 | int size = udp.parsePacket(); 773 | if (size >= NTP_PACKET_SIZE) { 774 | #if DEBUGNTP==true 775 | Serial.println("Receive NTP Response"); 776 | #endif 777 | udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer 778 | unsigned long secsSince1900; 779 | // convert four bytes starting at location 40 to a long integer 780 | secsSince1900 = (unsigned long)packetBuffer[40] << 24; 781 | secsSince1900 |= (unsigned long)packetBuffer[41] << 16; 782 | secsSince1900 |= (unsigned long)packetBuffer[42] << 8; 783 | secsSince1900 |= (unsigned long)packetBuffer[43]; 784 | return secsSince1900 - 2208988800UL + offset * SECS_PER_HOUR; 785 | } 786 | delayAnimate(1); 787 | } 788 | #if DEBUGNTP==true 789 | Serial.println("No NTP Response :-("); 790 | #endif 791 | return 0; // return 0 if unable to get the time 792 | } 793 | 794 | 795 | void tryNtpCon() { 796 | time_t ntpTime = getNtpTime(); 797 | if(ntpTime!=0) { 798 | setTime((time_t) ntpTime); 799 | //adjustTime(offset * SECS_PER_HOUR); 800 | dateString = (String)dayShortStr(weekday()) + ". " + day() + " " + monthShortStr(month()) + " " + year(); 801 | con = 0; 802 | dateprinted = true; 803 | #if DEBUGNTP==true 804 | Serial.print("[UDP] New Date: "); 805 | Serial.println(dateString); 806 | #endif 807 | } 808 | } 809 | 810 | 811 | void printDigits(int digits) { 812 | // utility function for digital clock display: prints preceding colon and leading 0 813 | if(digits < 10) { 814 | Serial.print('0'); 815 | } 816 | Serial.print(digits); 817 | } 818 | 819 | 820 | int my_atoi(const char *s) { 821 | int sign=1; 822 | if(*s == '-') 823 | sign = -1; 824 | s++; 825 | int num=0; 826 | while(*s){ 827 | num=((*s)-'0')+num*10; 828 | s++; 829 | } 830 | return num*sign; 831 | } 832 | 833 | 834 | void adjustTimeFromHTTPHeaders() { 835 | // find the colons: 836 | int firstColon = dateString.indexOf(":"); 837 | int secondColon= dateString.lastIndexOf(":"); 838 | // get the substrings for hour, minute second: 839 | String hourString = dateString.substring(firstColon-2, firstColon); 840 | String minString = dateString.substring(firstColon+1, secondColon); 841 | String secString = dateString.substring(secondColon+1, secondColon+3); 842 | dateString = dateString.substring(0, firstColon-3)+" "; 843 | // convert to ints,saving the previous second: 844 | hours = hourString.toInt(); 845 | minutes = minString.toInt(); 846 | lastSecond = seconds; // save to do a time comparison 847 | seconds = secString.toInt(); 848 | // extract date info 849 | char str[] = "Sun. 10 nov 2016 "; // expected format 850 | static const char month_names[] = "ErrJanFebMarAprMayJunJulAugSepOctNovDec"; 851 | char dlm[] = " "; // space delimited 852 | // convert to char array for easier splitting 853 | dateString.toCharArray(str, dateString.length()+1); 854 | // get date chunks 855 | int cnt = 0; 856 | char* tab[10] = { "ddd", "dd", "mmm", "yyyy" }; 857 | // find chunks positions in array 858 | char *pch = strtok(str, dlm); 859 | // iterate and move chunks into tab 860 | while ( pch != NULL ) { 861 | if (cnt < 10) { 862 | tab[cnt++] = pch; 863 | } else { 864 | break; 865 | } 866 | pch = strtok(NULL, dlm); 867 | } 868 | // convert to ints 869 | int dday = ((String)tab[1]).toInt(); 870 | int mmonth = (strstr(month_names, tab[2])-month_names)/3; 871 | int yyear = ((String)tab[3]).toInt(); 872 | // set and adjust 873 | setTime(hours, minutes, seconds, dday, mmonth, yyear); 874 | adjustTime(offset * SECS_PER_HOUR); 875 | dateString = (String)dayShortStr(weekday()) + ". " + day() + " " + monthShortStr(month()) + " " + year(); 876 | con = 0; 877 | dateprinted = true; 878 | #if DEBUGHTTP==true 879 | Serial.print("New Date: "); 880 | Serial.println(dateString); 881 | #endif 882 | } 883 | 884 | 885 | 886 | void setupOta() { 887 | Serial.begin(115200); 888 | 889 | display.setFont(ArialMT_Plain_16); 890 | display.setTextAlignment(TEXT_ALIGN_CENTER); 891 | display.drawString(64, 0, "OTA Check"); 892 | display.drawProgressBar(14, 27, 100, 10, 0); 893 | display.display(); 894 | 895 | #if DEBUGOTA==true 896 | Serial.println("OTA Check"); 897 | Serial.print("Setting Hostname: "); 898 | Serial.println(ESPName); 899 | Serial.print("MAC Address:"); 900 | Serial.printf("%02x", WiFi.macAddress()[0]); 901 | Serial.print(":"); 902 | Serial.printf("%02x", WiFi.macAddress()[1]); 903 | Serial.print(":"); 904 | Serial.printf("%02x", WiFi.macAddress()[2]); 905 | Serial.print(":"); 906 | Serial.printf("%02x", WiFi.macAddress()[3]); 907 | Serial.print(":"); 908 | Serial.printf("%02x", WiFi.macAddress()[4]); 909 | Serial.print(":"); 910 | Serial.printf("%02x", WiFi.macAddress()[5]); 911 | Serial.println(); 912 | #endif 913 | 914 | //WiFi.hostname(ESPName); 915 | WiFi.mode(WIFI_STA); 916 | WiFi.begin(ssid, password); 917 | while (WiFi.waitForConnectResult() != WL_CONNECTED) { 918 | #if DEBUGOTA==true 919 | Serial.println("Connection Failed! Aborting..."); 920 | #endif 921 | display.clear(); 922 | display.drawString(64, 0, "OTA Fail, aborting"); 923 | display.display(); 924 | delay(1000); 925 | otaready = false; 926 | return; 927 | } 928 | 929 | display.drawProgressBar(14, 27, 100, 10, 10); 930 | display.display(); 931 | 932 | ArduinoOTA.onStart([]() { 933 | String type; 934 | if (ArduinoOTA.getCommand() == U_FLASH) 935 | type = "sketch"; 936 | else // U_SPIFFS 937 | type = "filesystem"; 938 | 939 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 940 | #if DEBUGOTA==true 941 | Serial.println("Start updating " + type); 942 | #endif 943 | }); 944 | ArduinoOTA.onEnd([]() { 945 | #if DEBUGOTA==true 946 | Serial.println("\nEnd"); 947 | #endif 948 | }); 949 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 950 | #if DEBUGOTA==true 951 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 952 | #endif 953 | display.clear(); 954 | display.drawString(64, 0, "OTA Flashing"); 955 | display.drawProgressBar(14, 27, 100, 10, 20); 956 | display.display(); 957 | }); 958 | ArduinoOTA.onError([](ota_error_t error) { 959 | #if DEBUGOTA==true 960 | Serial.printf("Error[%u]: ", error); 961 | #endif 962 | display.clear(); 963 | display.drawString(64, 0, "Error"); 964 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 965 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 966 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 967 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 968 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 969 | otaready = false; 970 | }); 971 | 972 | ArduinoOTA.setHostname(ESPName); 973 | ArduinoOTA.begin(); 974 | 975 | display.clear(); 976 | display.drawProgressBar(14, 27, 100, 10, 20); 977 | display.drawString(64, 0, "OTA Ready"); 978 | display.display(); 979 | #if DEBUGOTA==true 980 | Serial.println("Ready"); 981 | Serial.print("IP address: "); 982 | Serial.println(WiFi.localIP()); 983 | #endif 984 | otanow = millis(); 985 | } 986 | 987 | 988 | 989 | void wifiOff() { 990 | WiFi.disconnect(); 991 | WiFi.mode(WIFI_OFF); 992 | WiFi.forceSleepBegin(); 993 | } 994 | 995 | 996 | void handleOta() { 997 | int then = millis(); 998 | if(then - otawait < otanow) { 999 | int percent = 100-(ceil(then - otanow) / (otawait / 100))+1; 1000 | display.drawProgressBar(14, 27, 100, 10, percent); 1001 | display.display(); 1002 | ArduinoOTA.handle(); 1003 | } else { 1004 | otaready = false; 1005 | 1006 | tryNtpCon(); // probably useless but who knows ? 1007 | 1008 | if (timeStatus()== timeNotSet) { 1009 | 1010 | tryHttpCon(); // only useful on captive portals (hoping they return Date: header) 1011 | 1012 | if(dateString!="" && !dateprinted) { 1013 | // headers already received, no need to stay connected 1014 | wifiOff(); 1015 | // Serial.println(dateString); 1016 | adjustTimeFromHTTPHeaders(); 1017 | } 1018 | 1019 | } else { 1020 | 1021 | wifiOff(); 1022 | 1023 | } 1024 | } 1025 | } 1026 | 1027 | 1028 | 1029 | void setup() { 1030 | 1031 | Serial.begin(115200); 1032 | 1033 | ui.setTargetFPS(60); 1034 | display.init(); 1035 | display.clear(); // clears the screen and buffer 1036 | display.display(); 1037 | display.setColor(WHITE); 1038 | display.clear(); 1039 | display.flipScreenVertically(); 1040 | #if USE_GYRO == true 1041 | pinMode(A0, INPUT); 1042 | mpu.initialize(); 1043 | if (mpu.dmpInitialize() == 0) { 1044 | #if DEBUGGYRO==true 1045 | Serial.println("MPU initialized!"); 1046 | #endif 1047 | // turn on the DMP, now that it's ready 1048 | mpu.setDMPEnabled(true); 1049 | // supply your own gyro offsets here, scaled for min sensitivity 1050 | mpu.setXGyroOffset(62); 1051 | mpu.setYGyroOffset(100); 1052 | mpu.setZGyroOffset(82); 1053 | mpu.setZAccelOffset(-580); // 1688 factory default for my test chip 1054 | } 1055 | #endif 1056 | 1057 | delay(100); 1058 | 1059 | setupOta(); 1060 | swsetup(); 1061 | 1062 | Serial.println("Setup done"); 1063 | 1064 | } 1065 | 1066 | 1067 | void loop() { 1068 | 1069 | if(otaready==true) { 1070 | handleOta(); 1071 | return; 1072 | } 1073 | 1074 | calculateMovement(); 1075 | draw(); 1076 | 1077 | if (timeStatus()!= timeNotSet) { 1078 | if (now() != prevDisplay) { //update the display only if the time has changed 1079 | prevDisplay = now(); 1080 | // digital clock display of the time 1081 | #if DEBUGNTP == true 1082 | printDigits(hour()); 1083 | Serial.print(":"); 1084 | printDigits(minute()); 1085 | Serial.print(":"); 1086 | printDigits(second()); 1087 | Serial.print(" "); 1088 | Serial.print(day()); 1089 | Serial.print(" "); 1090 | Serial.print(month()); 1091 | Serial.print(" "); 1092 | Serial.print(year()); 1093 | Serial.println(); 1094 | #endif 1095 | } 1096 | // TODO: rescan every hour or so to calculate avg time leap 1097 | return; 1098 | } 1099 | 1100 | if(n<=0) { 1101 | // get Wifi neigbourhood 1102 | Serial.println("scan start"); 1103 | doScan(); 1104 | delayAnimate(5000); 1105 | Serial.println("scan done"); 1106 | return; 1107 | } 1108 | 1109 | while(con==0) { 1110 | // iterate over Wifi neighbourhood 1111 | tryWifiCon(); 1112 | n--; 1113 | if(n>0) return; 1114 | } 1115 | 1116 | if(con!=0) { 1117 | // got and IP address! 1118 | if(dateString=="") { 1119 | tryNtpCon(); // probably useless 1120 | if (timeStatus()== timeNotSet) { 1121 | tryHttpCon(); // only useful on captive portals (hoping they return Date: header) 1122 | } 1123 | } 1124 | } 1125 | 1126 | if(dateString!="" && !dateprinted) { 1127 | // Date+Time HTTP headers have been received, no need to stay connected 1128 | wifiOff(); 1129 | // Serial.println(dateString); 1130 | adjustTimeFromHTTPHeaders(); 1131 | } 1132 | 1133 | } 1134 | 1135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 tobozo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266-Hobo-Clock 2 | 3 | 4 | * The Hobo's Animated IoT Clock 5 | 6 | Manifesto: giving trust to unreliable time providers isn't worse than trusting potentially unreliable access points with personal credentials. 7 | 8 | This clock has not RTC module and doesn't need to reach a NTP server to adjust its time. It uses WiFi access points openness in order to extract the date/time from the "Date" HTTP header that may be sent by some captive portals. So it will scan all nearby open WiFi AP until it finds a suitable response. 9 | 10 | Breakout: 11 | - Wemos Mini D1 (ESP8266) 12 | - SSD1306 OLED (128x64 Monochrome) 13 | - TP4056 LiPo Charger 14 | - 3.7v LiPo (270mAh, tiny size) 15 | 16 | Boot sequence: 17 | - Enumerate Open WiFi AP 18 | - Get an IP address 19 | - Connect to captive portal 20 | - Look for a "Date" HTTP header (and against common 21 | sense, trust its value) 22 | - Adjust the clock accordingly (at 0:40) 23 | 24 | Expecting unknown networks to provide a HTTP header value and relying on it to estimate time is like counting on other people's wealth to survive, hence the Hobo name. 25 | 26 | The exclusive use of open access points removes the hassle of hardcoding SSID/password into the sketch but also compensates its lack of auth plus the fact that the optional NTP connexion attempt will always fail, unless the AP acts as. 27 | 28 | The Pong animation with a bouncing rotating cube is there to cut on the boringness of the clock but also to demonstrate how this tiny OLED can animate fast (nearly 60fps). 29 | 30 | Since it has trust issues, don't trust this clock more than you would trust a stranger's watch! The available space and power consumption won't let it run more than a couple of hours on the LiPo anyway. 31 | 32 | Ported to NodeMCU by tobozo (c+) Nov 2016 33 | 34 | [![The Hobo's Animated IoT Clock](https://img.youtube.com/vi/RZ90ruADrI4/0.jpg)](https://www.youtube.com/watch?v=RZ90ruADrI4) 35 | 36 | --------------------------------------------------------------------------------