Microcontroller projects

Neo Clock 2: NeoPixel ring using NTP

last updated: 2021-07-05

Introduction

Before building the Neo clock, I bought a NeoPixel ring with 60 LED's in 4 parts from Adafruit to build that clock. So this could be Neo clock 0 (before 1), but as fate would have it, the ring became one of those "when I have better time" projects, an the LED stripe won for the Neo Clock.

This year I needed a simple clock without sound an I wanted a minimalistic (or no-) design.

I wanted to recycle the software from the Neo clock, but first I found a better way to run an internal clock on ESP8266 without RTC and second the FastLED library would not work with the RGBW LED's used in the NeoPixel rings from Adafruit. So I use here the Adafruit Neopixel library and did not encounter the watchdog problems, that made me back than change to the FastLED lib.

11h58 and 57seconds                              12 noon

neo clock 2 11h58:57 neo clock 2 12h00:00

Hardware

The microcontroller is a Wemos/Lolin D1 mini pro. An I²C light sensor from Adafruit (TSL2561) helps to adjust the brightness of the LED's. Naturally the light sensor can be omitted or replaced with a light-dependent resistor.

The I²C board is connected to SDA (4) and SCL (5) and powered with 3.3 V. The LED's are powered with 5 V. As max. 3 LED's are on simultaneously there is no special power concern (and no Elkos needed). The ring data line is connected to pin 13. A series resistor is already contained on the ring.

As my connection from the ring to the microcontroller has 2 meter in length, I had to add a logic-level converter to the data line to get a 5 V level. My first solution with an 2N7000 transistor and 10k resistors did not work as expected, because of the high frequency of the data line. So I used an 74HCT125 instead. More infos on this page: http://weigu.lu/microcontroller/tips_tricks/level_shifter_tts/index.html.

Circuit

neopixel clock circuit

PCB and housing

As there are not many connections to make, everything was soldered on a remaining piece of PCB. An old handy power supply was recycled, and to avoid electrical shock, a housing was printed on my 3D printer.

neo clock 2 hardware front neo clock 2 hardware back neo clock 2 hardware light sensor

neo clock 2 housing 1neo clock 2 housing 1

Software

The programming can be done over OTA and the debugging over UDP. For this I use my ESPToolbox library.

NTP is included in the ESP Core beginning with version 2.6.0 (2019). So we don't need an extra library any more. Everything is included in the time.h core library, even time zones (TZ) and daylight saving time (DST, summer/winter time). For more infos on this look here: http://weigu.lu/microcontroller/tips_tricks/esp_NTP_tips_tricks/index.html

I had some problems with the colours, as I wanted to mix them. The RGBW LED's are coded as WRGB in an unsigned long integer. The code is on github.

    /*
      neo_clock_2.ino
      weigu.lu
    */

    #define STATIC        // if static IP needed (no DHCP)
    #define OTA           // if Over The Air update needed (security risk!)
    // The file "secrets.h" has to be placed in the sketchbook libraries folder
    // in a folder named "Secrets" and must contain your secrets e.g.:
    // const char *MY_WIFI_SSID = "mySSID"; const char *MY_WIFI_PASSWORD = "myPASS"; ...
    #define USE_SECRETS
    #define LIGHT_SENSOR

    #include <coredecls.h>  // ! optional settimeofday_cb() callback to check on server
    #include <Adafruit_NeoPixel.h>
    #include "ESPToolbox.h"

    /****** Lightsensor settings ******/
    #ifdef LIGHT_SENSOR
      #include <Adafruit_Sensor.h>
      #include <Adafruit_TSL2561_U.h>
      Adafruit_TSL2561_Unified TSL = Adafruit_TSL2561_Unified(TSL2561_ADDR_FLOAT, 12345);
    #endif //ifdef LIGHT_SENSOR

    /****** WiFi and network settings ******/
    #ifdef USE_SECRETS
      #include <secrets.h>
      const char *WIFI_SSID = MY_WIFI_SSID;           // ssid     
      const char *WIFI_PASSWORD = MY_WIFI_PASSWORD;   // password
      #ifdef OTA                                      // Over The Air update settings
        const char *OTA_NAME = "neo_clock_2";
        const char *OTA_PASS_HASH = MY_OTA_PASS_HASH; // md5 hash for OTA
      #endif // ifdef OTA  
    #else
      const char *WIFI_SSID = mySSID;           // if no secrets file, add your SSID here
      const char *WIFI_PASSWORD = myPASSWORD;   // if no secrets file, add your PASS here
      #ifdef OTA                                // Over The Air update settings
        const char *OTA_NAME = "neo_clock_2";
        const char *OTA_PASS_HASH = myOTAHASH;  // if no secrets file, add your OTA HASH here
      #endif // ifdef OTA      
    #endif  // ifdef USE_SECRETS
    const char *NET_MDNSNAME = "NeoClock2";     // optional (access with myESP.local)
    const char *NET_HOSTNAME = "NeoClock2";     // optional
    #ifdef STATIC
      IPAddress NET_LOCAL_IP (192,168,1,77);    // 3x optional for static IP
      IPAddress NET_GATEWAY (192,168,1,20);
      IPAddress NET_MASK (255,255,255,0);  
    #endif // ifdef STATIC*/
    const word UDP_LOG_PORT = 6668;             // UDP logging settings
    IPAddress UDP_LOG_PC_IP(192,168,1,50);
    const char *NTP_SERVER = "lu.pool.ntp.org"; // NTP settings
    // your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php)
    const char *TZ_INFO    = "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00";


    /****** NeoPixel settings ******/
    const byte LED_PIN = 13;
    const byte LED_COUNT = 60;
    Adafruit_NeoPixel NeoPix(LED_COUNT, LED_PIN, NEO_GRBW);
    const uint32_t long COLOR_SEC  = 0x00000001; // blue
    const uint32_t long COLOR_MIN  = 0x00000100; // green
    const uint32_t long COLOR_HOUR = 0x00010000; // red 
    const uint32_t long COLOR_OFF  = 0x00000000; // black
    byte neopix_sec,neopix_min, neopix_hour, neopix_sec_prev,neopix_min_prev, neopix_hour_prev;
    uint32_t color_sec, color_min, color_hour, color_sm, color_sh, color_mh, color_smh;
    byte brightness = 100;

    ESPToolbox Tb;

    time_t now = 0;
    tm timeinfo;                      // time structure

    struct My_Timeinfo {
      byte second;
      byte minute;
      byte hour;
      byte day;
      byte month;
      unsigned int year;
      byte weekday;
      unsigned int yearday;
      bool daylight_saving_flag;
      String name_of_day;
      String name_of_month;
      String date;
      String time;
      String datetime;
    } my;

    // ! optional  callback function to check if NTP server called
    void time_is_set(bool from_sntp /* <= this parameter is optional */) {  
      Tb.log_ln("The NTP server was called!");
    }

    // ! optional change here if you want another NTP polling interval (default 1h)
    uint32_t sntp_update_delay_MS_rfc_not_less_than_15000 () {
      return 4 * 60 * 60 * 1000UL; // every 4 hours
    }

    void setup() {
      Tb.set_led_log(true);                 // use builtin LED for debugging
      Tb.set_serial_log(true,0);            // 2 parameter = interface (1 = Serial1)
      Tb.set_udp_log(true, UDP_LOG_PC_IP, UDP_LOG_PORT); // use "nc -kulw 0 6666"  
      NeoPix.begin();
      settimeofday_cb(time_is_set); // ! optional  callback function to check if NTP server called
      init_ntp_time();
      init_wifi(); 
      get_time();
      Tb.log_ln("Epoch time: " + String(now));
      #ifdef LIGHT_SENSOR
        init_light_sensor();
      #endif //ifdef LIGHT_SENSOR
      #ifdef OTA
        Tb.init_ota(OTA_NAME, OTA_PASS_HASH);
      #endif // ifdef OTA
      Tb.blink_led_x_times(3);
      Tb.log_ln("Setup done!");
    }

    void loop() {  
      #ifdef OTA
        ArduinoOTA.handle();
      #endif // ifdef OTA
      get_time();
      neo_clock();
      if (Tb.non_blocking_delay(5000)) {
        Tb.log_ln(my.datetime);
        delay(50);    
        #ifdef LIGHT_SENSOR
          set_brightness_from_light();
        #endif //ifdef LIGHT_SENSOR
      }
      delay(20);
    }

    /********** INIT FUNCTIONS ****************************************************/
    // init WiFi (static or dhcp)
    void init_wifi() {
      #ifdef STATIC
        Tb.init_wifi_sta(WIFI_SSID, WIFI_PASSWORD, NET_HOSTNAME, NET_LOCAL_IP, 
                      NET_GATEWAY, NET_MASK);
      #else                  
        Tb.init_wifi_sta(WIFI_SSID, WIFI_PASSWORD, NET_MDNSNAME, NET_HOSTNAME);                       
      #endif // ifdef STATIC
    }

    #ifdef LIGHT_SENSOR
      // init light sensor
      void init_light_sensor() {
        Wire.begin(4, 5); // D3 and D4 on ESP8266
        Wire.setClock(400000L);
        Wire.status();
        if(!TSL.begin()) {
          Tb.log("Ooops, no TSL2561 detected ... Check your wiring or I2C ADDR!");
          while(1);
        }
        TSL.enableAutoRange(true);                              // Auto-gain
        TSL.setIntegrationTime(TSL2561_INTEGRATIONTIME_13MS);   // fast but low resolution  
      }
    #endif //ifdef LIGHT_SENSOR

    // init NTP time: call this before the WiFi connects!
    void init_ntp_time() { 
      #ifdef ESP8266
        configTime(TZ_INFO, NTP_SERVER);    
      #else  
        configTime(0, 0, NTP_SERVER); // 0, 0 because we will use TZ in the next line
        setenv("TZ", TZ_INFO, 1);     // set environment variable with your time zone
        tzset();
      #endif
    }

    /********** TIME FUNCTION ****************************************************/

    // epoch to tm structure and update global struct
    void get_time() {
      time(&now);                     // this function calls the NTP server only every hour
      localtime_r(&now, &timeinfo);   // converts epoch time to tm structure
      my.second  = timeinfo.tm_sec;
      my.minute  = timeinfo.tm_min;
      my.hour  = timeinfo.tm_hour;
      my.day  = timeinfo.tm_mday;
      my.month  = timeinfo.tm_mon + 1;    // beer (Andreas video)
      my.year  = timeinfo.tm_year + 1900; // beer
      my.weekday = timeinfo.tm_wday; 
      if (my.weekday == 0) {              // beer
        my.weekday = 7;
      }
      my.yearday = timeinfo.tm_yday + 1;  // beer
      my.daylight_saving_flag  = timeinfo.tm_isdst;
      char buffer[25];  
      strftime(buffer, 25, "%A", localtime(&now));
      my.name_of_day = String(buffer);
      strftime(buffer, 25, "%B", localtime(&now));
      my.name_of_month = String(buffer);
      strftime(buffer, 25, "20%y-%m-%d", localtime(&now));
      my.date = String(buffer);
      strftime(buffer, 25, "%H:%M:%S", localtime(&now));
      my.time = String(buffer);  
      strftime(buffer, 25, "20%y-%m-%dT%H:%M:%S", localtime(&now));
      my.datetime = String(buffer);  
    }

    /******* CLOCK FUNCTIONS ******************************************************/
    void neo_clock() {  
      neopix_sec = my.second;
      neopix_min = my.minute;
      neopix_hour = my.hour;
      if (neopix_sec == neopix_sec_prev) {
        return;
      }
      color_sec  = COLOR_SEC * brightness;  
      color_min  = COLOR_MIN * brightness;  
      color_hour = COLOR_HOUR * brightness;  
      color_sm   = color_sec + color_min;
      color_sh   = color_sec + color_hour;
      color_mh   = color_min + color_hour;
      color_smh  = color_sec + color_min + color_hour; 
      //Clear previous LEDs
      NeoPix.setPixelColor(neopix_sec_prev, COLOR_OFF);
      NeoPix.setPixelColor(neopix_min_prev, COLOR_OFF);
      NeoPix.setPixelColor(neopix_hour_prev, COLOR_OFF);  


      if (neopix_hour >= 12) {
        neopix_hour -= 12;
      }
      neopix_hour *= 5;
      neopix_hour += (neopix_min/12);
      neopix_sec_prev = neopix_sec;
      neopix_min_prev = neopix_min;
      neopix_hour_prev = neopix_hour;
      if ((neopix_sec == neopix_min) && (neopix_sec == neopix_hour)) {
        NeoPix.setPixelColor(neopix_sec, color_smh); 
        NeoPix.setPixelColor(neopix_min, color_smh);
        NeoPix.setPixelColor(neopix_hour, color_smh);
      }
      else if (neopix_sec == neopix_min) {
        NeoPix.setPixelColor(neopix_sec, color_sm); 
        NeoPix.setPixelColor(neopix_min, color_sm); 
        NeoPix.setPixelColor(neopix_hour, color_hour); 
      }
      else if (neopix_sec == neopix_hour) {
        NeoPix.setPixelColor(neopix_sec, color_sh); 
        NeoPix.setPixelColor(neopix_min, color_min);
        NeoPix.setPixelColor(neopix_hour, color_sh); 
      }
      else if (neopix_min == neopix_hour) {
        NeoPix.setPixelColor(neopix_sec, color_sec);  
        NeoPix.setPixelColor(neopix_min, color_mh); 
        NeoPix.setPixelColor(neopix_hour, color_mh);
      }
      else {          
        NeoPix.setPixelColor(neopix_sec, color_sec); 
        NeoPix.setPixelColor(neopix_min, color_min); 
        NeoPix.setPixelColor(neopix_hour, color_hour); 
      }
      NeoPix.show(); // Send the updated pixel colors to the hardware.    
    }

    #ifdef LIGHT_SENSOR
      void set_brightness_from_light() {
        sensors_event_t event;
        TSL.getEvent(&event);
        if (event.light) {    
          if (event.light > 255) {    
            event.light = 255;
          }
          brightness = map(event.light,1,255,20,150);
          Tb.log_ln("Light: " + String(event.light) +" lux, brightness: " + String(brightness));
        }
        else {
          Tb.log_ln("Sensor overload");
          brightness = 9;
        }
      }
    #endif //ifdef LIGHT_SENSOR  

Downloads