Simple digital clock

Posted on
  • My 7 years old daughter have made (with my help) big digital clock. We use 8x32 matrix of ws8212b LED and small ESP8266 board with 5-to-3.3 V LDO on board. LED matrix have separate 2-wire power connector and 3-wire control connector, so we can connect 5V power adaptor to first one and ESP8266 to second one only (using 3 jumper wires - to GND, МСС and GPIO2).

    Then very simple program convert the matrix to digital clock

    var neopixel = require("neopixel");
    E.setTimeZone(3);
    require("Wifi").setSNTP("0.pool.ntp.org"­,0);
    
    require("Font6x8").add(Graphics);
    var g = Graphics.createArrayBuffer(8,32,24,{zigz­ag:true,color_order:'grb'});
    g.setRotation(1,1);
    
    var att=6.9;
    var d=Math.pow(2,att);
    g.setColor(0,0,1/d);
    
    var oldTime, newTime;
    g.setFont6x8();
    
    function drawTime() {
      newTime=Date().toString().substr(16,5);
      if(oldTime != newTime) {
        oldTime=newTime;
        g.clear();
        g.drawString(newTime);
        neopixel.write(D2, g.buffer);
      }
    }
    
    setInterval(drawTime,1000);
    
    neopixel.write(D2, g.buffer);
    

    Some comments:
    Last line are to fix state of LED matrix. It is enough to write first LED (even first logical LED - 1 byte) only but it is harder - we need separate (very small) buffer to do that. So we write all matrix. The line may be removed but first ouput will be incorrect.
    We have found that LED matrix is in fact 8x32 zigzag (first line is right-to-left, second one is left-to-right etc.) So we have added g.setRotation() to use it as 32x8 display.
    I have added att and d vars to control LED brightness more intuitively - now brightness looks to be changed linearly with att change.
    It need a few seconds to get correct time from Internet, so time displayed is strange after power on.

  • Nice - thanks for posting it up! That first write for neopixels is a pain - it's caused by the pin having been floating before neopixel.write was called.

    You might like this function for use with setColor: http://www.espruino.com/Reference#t_l_E_­HSBtoRGB

    It'd be really neat to change the hue by a little bit every second :)

  • Hi - could you also post a picture of it?

  • Sorry, I do not understand how to upload a picture - 'Upload a file' in the message form allows to choose a file but nothing happens after.

    By the way, I have assembled bigger clock with 4 16x16 LED panels and ESP32 as controller. Code was rewritten for the clock:

    var neopixel = require("neopixel");
    E.setTimeZone(3);
    var wifi=require("Wifi");
    
    //--------------------------------------­------------------------
    //--------------------- NTP client -----------------------------
    //--------------------------------------­------------------------
    
        var dgram = require('dgram');
    
        defaultNtpPort = 123;
        defaultNtpServer = "pool.ntp.org";
    
        /**
         * Amount of acceptable time to await for a response from the remote server.
         * Configured default to 10 seconds.
         */
        ntpReplyTimeout = 10000;
    
        /**
         * Fetches the current NTP Time from the given server and port.
         * @param {string} server IP/Hostname of the remote NTP Server
         * @param {number} port Remote NTP Server port number
         * @param {function(Object, Date)} callback(err, date) Async callback for
         * the result date or eventually error.
         */
        getNetworkTime = function (server, port, callback) {
            if (callback === null || typeof callback !== "function") {
                return;
            }
    
            server = server || defaultNtpServer;
            port = port || defaultNtpPort;
    
            var client = dgram.createSocket("udp4"),
                ntpData = new Uint8Array(48);
    
            // RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte
            // -> rtol(LI, 6) ^ rotl(VN, 3) ^ rotl(Mode, 0)
            // -> = 0x00 ^ 0x18 ^ 0x03
            ntpData[0] = 0x1B;
    
    //        for (var i = 1; i < 48; i++) {
    //            ntpData[i] = 0;
    //        }
    
            var timeout = setTimeout(function () {
                client.close();
                callback("Timeout waiting for NTP response.", null);
            }, ntpReplyTimeout);
    
            // Some errors can happen before/after send() or cause send() to was impossible.
            // Some errors will also be given to the send() callback.
            // We keep a flag, therefore, to prevent multiple callbacks.
            // NOTE : the error callback is not generalised, as the client has to lose the connection also, apparently.
            var errorFired = false;
            client.bind(8215+((Math.random()*100)|0)­, function(res){
                res.on('error', function (err) {
                    if (errorFired) {
                        return;
                    }
    
                    callback(err, null);
                    errorFired = true;
    
                    clearTimeout(timeout);
                });
    
                res.on('message', function (msg) {
                    clearTimeout(timeout);
                    client.close();
    
                    // Offset to get to the "Transmit Timestamp" field (time at which the reply
                    // departed the server for the client, in 64-bit timestamp format."
                    var offsetTransmitTime = 40,
                        intpart = 0,
                        fractpart = 0;
    
                    msg=E.toUint8Array(msg);
                    // Get the seconds part
                    for (var i = 0; i <= 3; i++) {
                        intpart = 256 * intpart + msg[offsetTransmitTime + i];
                    }
    
                    // Get the seconds fraction
                    for (i = 4; i <= 7; i++) {
                        fractpart = 256 * fractpart + msg[offsetTransmitTime + i];
                    }
    
                    var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000);
                    milliseconds+=-2208988800000;
                    // **UTC** time
                    var date = new Date(milliseconds);
    console.log('Date received', date.toString());
                    callback(null, date);
                });
                res.on('close', function(error) {
                    console.log('drgam is closed, error',error);
                });
            });
    //        client.on('error', function (err) {
    //            if (errorFired) {
    //                return;
    //            }
    
    //            callback(err, null);
    //            errorFired = true;
    
    //            clearTimeout(timeout);
    //        });
    
            client.send(E.toString(ntpData), 0, ntpData.length, port, server, function (err) {
                if (err) {
                    if (errorFired) {
                        return;
                    }
                    clearTimeout(timeout);
                    callback(err, null);
                    errorFired = true;
                    client.close();
                    return;
                }
    
            });
        };
    
    //--------------------------------------­------------------------
    //-- change font6x8 to add a few icons and some Russian chars --
    //--------------------------------------­------------------------
    
    //require("Font6x8").add(Graphics);
    var gt = Graphics.createArrayBuffer(16,64,24,{zig­zag:true,color_order:'grb'});
    gt.setRotation(3,1);
    var font = atob("AAAAAPoAwADAAFhw2HDQAGSS/5JMAGCW+L­zSDAxSolIMEsAAPEKBAIFCPABIMOAwSAAQEHwQEA­ABBgAQEBAQAAIAAwwwwAB8ipKifABA/gBChoqSYg­CEkrLSjAAYKEj+CADkoqKinAA8UpKSDACAgI6wwA­BskpKSbABgkpKUeAAiAAEmABAoRAAoKCgoKABEKB­AAQIqQYAA8WqW9RDgOOMg4DgD+kpKSbAB8goKCRA­D+goJEOAD+kpKCAP6QkIAAfIKCklwA/hAQEP4A/g­AMAgIC/AD+EChEggD+AgICAP5AIED+AP7AMAz+AH­yCgoJ8AP6QkJBgAHyChoN8AP6QmJRiAGSSkpJMAI­CA/oCAAPwCAgL8AOAYBhjgAPAOMA7wAMYoECjGAM­AgHiDAAI6SosIA/4EAwDAMAwCB/wBAgEAAAQEBAQ­EBEn6SggQABCoqHgD+IiIcABwiIhQAHCIi/gAcKi­oYACB+oIAAGCUlPgD+ICAeAL4AAQG+AP4IFCIA/A­IAPiAeIB4APiAgHgAcIiIcAD8kJBgAGCQkPwA+EC­AgABIqKiQAIPwiADwCAjwAIBgGGCAAOAYIBjgAIh­QIFCIAIRkGGCAAJioyIgAQboEA5wCBbhAAQIDAQI­AAPFqlpUI8");
    var widths = atob("BAIEBgYGBgIEBAYGAwUCBQYDBgYGBgYGBg­YCAwQGBAUGBgYGBgUFBgYCBgYFBgYGBgYGBgYGBg­YGBgUDBQMEBgYFBQUFBQUFBQIEBQMGBQUFBQUFBA­UGBgYGBQQCBAYG");
    font+='\x08\x1E\x32\x52\x32\x1E\x08';
    font+='\x41\x93\xAF\x79\xA1\x91\x41';
    widths+='\x07\x07';
    font+='\x62\x94\x98\x90\xFE\x00';
    font+='\x3E\x08\x08\x3E\x00';
    font+='\x3E\x2A\x2A\x14\x00';
    font+='\x78\x84\xFE\x84\x78\x00';
    font+='\x3E\x20\x20\x3E\x00';
    font+='\xFE\x0C\x30\x40\xFE\x00';
    font+='\x3E\x08\x1C\x22\x1C\x00';
    font+='\x06\x18\x20\x3E\x00';
    font+='\x3E\x20\x20\x20\x00';
    font+='\x3E\x08\x14\x22\x00';
    font+='\x20\x3E\x20\x00';
    font+='\x12\x2C\x28\x3E\x00';
    font+='\x03\x7E\x82\x82\xFE\x03\x00';
    widths+='\x06\x05\x05\x06\x05\x06\x06\x0­5\x05\x05\x04\x05\x07';
    
    //8  NNNN            NNN       N   N                                 NNN
    //4 N   N           N N N      N  NN                                N  N
    //2 N   N N  N NNN  N N N NNNN N N N N  N    NN NNNN N  N NNN  NNN  N  N
    //1  NNNN N  N N  N N N N N  N N N N N N N  N N N    N N   N  N  N  N  N
    //8   N N NNNN NNN  N N N N  N NN  N NNN N  N N N    NN    N   NNN  N  N
    //4  N  N N  N N  N  NNN  N  N NN  N N N N N  N N    N N   N   N N  N  N
    //2 N   N N  N NNN    N   N  N N   N N  N  N  N N    N  N  N  N  N NNNNNN
    //1                                                                N    N
    //    82   83   84    85   86    87   88    89   8A   8B   8C  8D    8E
    
    // Lookup table to convert English names of months to Russian using added symbols 
    var MC={'Jan':'\x82\x83\x84', 'Feb':'\x85e\x84', 'Mar':'Map', 'Apr':'A\x86p', 'May':'Ma\x8D',
            'Jun':'\x87\x88\x83', 'Jul':'\x87\x88\x89', 'Aug':'A\x84\x8A', 'Sep':'Ce\x83',
            'Oct':'O\x8B\x8C', 'Nov':'Ho\x8D', 'Dec':'\x8Ee\x8B'};
    
    //--------------------------------------­-----------
    //-------- Main program ---------------------------
    //--------------------------------------­-----------
    
    
    Graphics.prototype.setFont6x8 = function() {
        this.setFontCustom(font, 32, widths, 8);
    };
    
    Graphics.prototype.convertColorFromHTML = function(HTMLColor) {
        return parseInt(HTMLColor.substr(5,2)+HTMLColor­.substr(1,2)+HTMLColor.substr(3,2),16);
    };
    
    gt.setFont6x8();
    
    var br,d;
    
    var ClockBrightness=2,LampBrightness=0;
    var ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
    var LampB =(Math.pow(2,(LampBrightness -1)/9*8)+LampBrightness /4)/256;//add linear component to be better near brightness=1
    
    var TempIn='---', TempOut='---';
    
    var fields=[
      {x:0, y:0, w:32,hs:{h:0.6,  s:1}, drawField:0},
      {x:32,y:0, w:32,hs:{h:0.5,  s:1}, drawField:0},
      {x:0, y:8, w:32,hs:{h:0,    s:0}, drawField:0},
      {x:32,y:8, w:32,hs:{h:0.334,s:1}, drawField:0}
    ];
    
    var updateCounter=0;
    function updateField(fieldNum) {
      updateCounter++;
      setTimeout(function() {
        if(updateCounter) {
          var clr=E.HSBtoRGB(0,0.01,LampB,true);
          gt.setBgColor(clr[0]/255,clr[1]/255,clr[­2]/255);
          gt.clear();
    
          for(let i=0;i<fields.length;i++)
            fields[i].drawField(i);
          neopixel.write(D2, gt.buffer);
          updateCounter=0;
        }
      },0);
    }
    
    function updateSettings() {
      //ugly hack because updateField does not really use fieldNum
      updateField(0);
    }
    
    var oldTime, newTime, newDate;
    
    function drawDate(fieldNum) {
      var clr=E.HSBtoRGB(fields[fieldNum].hs.h,fie­lds[fieldNum].hs.s,ClockB,true);
      gt.setColor(clr[0]/255,clr[1]/255,clr[2]­/255);
      var text=newDate;
      gt.drawString(text,((fields[fieldNum].w-­gt.stringWidth(text))/2|0)+fields[fieldN­um].x,fields[fieldNum].y);
    }
    fields[0].drawField=drawDate;
    
    function drawTime(fieldNum) {
      var clr=E.HSBtoRGB(fields[fieldNum].hs.h,fie­lds[fieldNum].hs.s,ClockB,true);
      gt.setColor(clr[0]/255,clr[1]/255,clr[2]­/255);
      var text=newTime;
      gt.drawString(text,((fields[fieldNum].w-­gt.stringWidth(text))/2|0)+fields[fieldN­um].x,fields[fieldNum].y);
    }
    fields[1].drawField=drawTime;
    
    function prepareTime() {
      var ds=Date().toString();
      newTime=ds.substr((ds[14]==' ')?15:16,5);
      if(oldTime != newTime) {
        oldTime=newTime;
        newDate = Date().getDate()+' '+MC[Date().toString().substr(4,3)];
        updateField(0);
        updateField(1);
      }
    }
    
    function drawTempIn(fieldNum) {
      var clr=E.HSBtoRGB(fields[fieldNum].hs.h,fie­lds[fieldNum].hs.s,ClockB,true);
      gt.setColor(clr[0]/255,clr[1]/255,clr[2]­/255);
      var text='\x80'+TempIn;
      gt.drawString(text,((fields[fieldNum].w-­gt.stringWidth(text))/2|0)+fields[fieldN­um].x,fields[fieldNum].y);
    }
    fields[2].drawField=drawTempIn;
    
    function drawTempOut(fieldNum) {
      var clr=E.HSBtoRGB(fields[fieldNum].hs.h,fie­lds[fieldNum].hs.s,ClockB,true);
      gt.setColor(clr[0]/255,clr[1]/255,clr[2]­/255);
      var text='\x81'+TempOut;
      gt.drawString(text,((fields[fieldNum].w-­gt.stringWidth(text))/2|0)+fields[fieldN­um].x,fields[fieldNum].y);
    }
    fields[3].drawField=drawTempOut;
    
    //updateSettings();
    setInterval(prepareTime,1000);
    
    var tnum;
    function getNTP() {
      getNetworkTime(defaultNtpServer, defaultNtpPort, function (err, date) {
        if (err) {
          console.log(err);
          tnum=setTimeout(getNTP,120000);
          return;
        }
        console.log(date);
        setTime(date.valueOf()/1000);
        tnum=setTimeout(getNTP,24*3600*1000);
      });
    }
    
    wifi.on('connected', ()=>setTimeout(getNTP,30000));
    
    setInterval(()=>{
      var status=wifi.getDetails();
      if(!status.status || status.status != "connected")
        gti.drawString(status.status,0,0);
      if(!status.status || status.status != "connected")
    //  if(["off", "wrong_password", "bad_password", "no_ap_found", "connect_fail"].indexOf(status.status)!=­-1)
      {
        if(tnum) {
          clearTimeout(tnum);
          tnum=undefined;
        }
        wifi.restore();
      }
    },300000);
    
    neopixel.write(D2, gt.buffer);
    neopixel.write(D2, gt.buffer);
    
    var http=require("http");
    
    var toTempIn,toTempOut;
    
    function onPageRequest(req, res) {
    //  console.log('pr',req.url,Date().toString­());
      var a = url.parse(req.url, true);
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('<html><body>');
      res.write('<script src="https://cdnjs.cloudflare.com/ajax/l­ibs/jscolor/2.0.4/jscolor.min.js"></scri­pt>');
      res.write('<script>function update(hsv) { location.search="?time_color_h="+hsv[0]+­"&time_color_s="+hsv[1];}</script>'); 
      res.write('<button class="jscolor {mode:\'HS\',valueElement:null,value:\'6­6ccff\'}" style="border:2px solid black" onchange="update(this.jscolor.hsv)">Pick­ a color</button>');
    //  ,value:\'66ccff\'
    //  res.write('<p>Pin is '+(BTN.read()?'on':'off')+'</p>');
    //  res.write('<div>Mode: <a href="?mode=lamp">Lamp</a> <a href="?mode=clock">clock</a></div>');
      res.write('<div>Clock brightness: ');
      for(let i=0; i<=10; i++)
        res.write(' <a href="?clock_brightness='+i+'">'+i+'</a>­');
      res.write('</div>');
      res.write('<div>Lamp brightness: ');
      for(let i=0; i<=10; i++)
        res.write(' <a href="?lamp_brightness='+i+'">'+i+'</a>'­);
      res.write('</div>');
      res.end('</body></html>');
      if (a.query) {
        if("temp_in" in a.query) {
          TempIn=parseFloat(a.query.temp_in);
          TempIn=((TempIn>0)?'+':'')+TempIn.toFixe­d(1);
    //      console.log(toTempIn);
          if(toTempIn) clearTimeout(toTempIn);
          toTempIn=setTimeout(()=>{TempIn='---'; toTempIn=undefined;updateField(1);},3000­00);
          updateField(2);
        }
        if("temp_out" in a.query) {
          TempOut=parseFloat(a.query.temp_out);
          TempOut=((TempOut>0)?'+':'')+TempOut.toF­ixed(1);
          if(toTempOut) clearTimeout(toTempOut);
          toTempOut=setTimeout(()=>{TempOut='---';­ toTempOut=undefined;updateField(2);},300­000);
          updateField(3);
        }
        if("lamp_brightness" in a.query) {
          LampBrightness=E.clip(parseInt(a.query.l­amp_brightness),0,10);
          LampB =(Math.pow(2,(LampBrightness -1)/9*8)+LampBrightness /4)/256;//add linear component to be better near brightness=1
          updateSettings();
        }
        if("clock_brightness" in a.query) {
          ClockBrightness=E.clip(parseInt(a.query.­clock_brightness),0,10);
          ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
          updateSettings();
        }
      }
    //  console.log('pr2',Date().toString());
    }
    
    require("http").createServer(onPageReque­st).listen(80);
    
    if(wifi.getDetails().status=="connected"­) getNTP();
    
    

    Clock screen is divided to 4 fields - date, time and 2 temperatures which can be set by http now, but I am going to change it to MQTT client.
    Clock brightness may be set by http too. Field colors too, but it does not fully work still.
    ESP32 v1.97 (last known to work with neopixel) looks like does not have SNTP included. So I have added NTP client from https://github.com/moonpyk/node-ntp-clie­nt. I have modified the code to it work in Espruino environment. I hope its MIT license allows to do that. I am not sure what should I write here about the client.
    I have found strange behavior of Espruino Date() while I have worked on the library: dates about 1900-1905 are incorrecly converted to milliseconds. So I use exact number of milliseconds but not Date("01.01.1900") or something similar in NTP time conversion. I have not found the effect in node.js so may be it is a bug in Espruino.

  • dates about 1900-1905 are incorrecly converted to milliseconds

    So for example (new Date('1901-10-20')).getTime() isn't -2152224000000?

  • (new Date('1901-10-20')).getTime()
    =-2025993600000

  • One more example:

    (new Date('1906-12-31')).getTime()
    =-1988236800000
    (new Date('1907-01-01')).getTime()
    =-1861920000000
    (new Date('1910-12-31')).getTime()
    =-1862006400000

  • Mon 2018.11.19

    Hello @SergeP,

    'Sorry, I do not understand how to upload a picture'

    The image, usually a .jpg must be deployed on a publicly accessible host server, or your own remote server. Then click the 'Image' button in row of buttons above. Then copy-n-paste your URL. The link will then render as an image and show up in the body of your post.

    'Upload a file in the message form allows to choose a file but nothing happens after'

    What PC O/S are you running, and what browser & ver? I may be able to provide some insight on PC running Win10 - a modal dialog appears that allows the file manager to enable file selection from the local hard drive.

  • Mon 2018.11.19

    ref #4 'ESP32 v1.97 (last known to work with neopixel) '

    ref: 'http://www.espruino.com/Reference#l_Date­'

    Just tried my luck on a Pico running 1v99

    From #6

    (new Date('1901-10-20')).getTime()

    The result:

    =-2025993600000

    I did not get the value -2152224000000 as suggested in #5

    I can not confirm if that negative value is a correct value for the date Oct 20, 1901 though.

    Also agree with the three dates in #7

    'dates about 1900-1905 are incorrecly converted to milliseconds'

    Would you mind @SergeP posting a link to the maths used to create the dates that are felt incorrect please?

  • @Robin -2025993600000 is the WRONG date. If you try running that code in your browser or node.js you'll see the right one

  • Just fixed.

  • @Gordon Great!
    @Robin Thank you for explaination of image adding. Photo of my old smaller clock:

  • Tue 2018.11.20

    'Thank you for explaination of image adding'

    Hint: Were you aware of the 'Formatting help' link adjacent to the 'Cancel' button?



    It is always neat to see the results of end user project successes. I'm glad you were able to post your image of your project which I'm sure you are proud.

    Can't wait to see the results of your 4 16x16 LED panels version with the added date and temp.

    Are you considering a tutorial with images or a YouTube video perhaps?


    Also curious why two identical lines of write instruction?

    L300 neopixel.write(D2, gt.buffer);
    L301 neopixel.write(D2, gt.buffer);
    

    Was there an issue with flicker or something of the sort? Hadn't had the need to do something like that with an authentic Espruino Pico. Just curious. . . .

  • Newer clock:

    @Robin I am a dinosaur who did not use file hosting services before. So after you explain an idea it is easy to use. About to identical lines - ws2812 can be in any state at start. Sometimes (rarely) it is in the middle of byte receiving. And one write is not enough to blank display. I think it is enough to send 1 byte (or may be 3 bytes) to sync, but it was easier to send all the buffer instead.

  • @SergeP nicely done!

    'And one write is not enough to blank display'

    Have you considered creating an array/buffer of dark/black, say all zeros for every Neopixel, and write that just on start up, or during 'middle of byte receiving' as in a screen clear?

  • @Robin sorry, I am not sure I understand your question.
    I've created global buffer to use. At startup it is blank, and I send it to display twice (one to synchronize and one to blank display while startup proceed). Then I use the buffer to write time etc.
    If display is not synchronized it can show some noise after first data out, sometimes the noise is too bright.

  • Fri 2018.11.23

    Hi @SergeP,

    I'm not trying to rain on your parade here, as what you have shown demonstrates the willingness to understand, divide and conquer, until the working project emerged. I know you are proud of what you have done, and so you should be. If what you have is working to your satisfaction, then go with it.

    My only reason to suggest an alternative, was as I noticed, several interval timers and several timeout periods were being used along with multiple Neopixel writes. The 'flicker' or 'noise' as you put it, could occur, when those intervals or timeouts overlap while attempting to update the output within the Graphics buffer.

    I had recently been playing with lengthy Neopixel strips, but using a FlatString and linear array to format the data, but with the requirement of only one Neopixel.write() statement. I haven't noticed any 'noise' even with timings around 20-50msec, which I believe is still faster than those in your project. For the clock, it is quite possible that the data is becoming corrupt, an overlapping non cleared array for instance, and that might explain the flicker.

    As I believe the matrix panels you have are just a zig-zag connected continuous strip of Neopixels, (correct me if I'm wrong here) it would be possible to create a 'blank' array outside of the Graphics buffer that could be used in place of gt.buffer to 'clear' the panel before the actual desired data to write there. At a minimum, this would/might prove if the data within the Graphics buffer is not stable, which is resulting in the 'noise.'

    Just a thought, but if you are satisfied, go with what is there now. You have completed quite an impressive project indeed.

  • @Robin My English is not good enough, so sometimes I am not sure I see that you write. And that is the only I mean in my previous post. (And I am also not sure I write exactly that I think because of the same reason, so I am sorry :) )
    I call neopixel.write twice only at startup. Besides that there is only one point where it called. Moreover, the code contains some trivial event aggregator to minimize number of calls.
    When I write about synchronization I mean that WS1812 may be in any state when the code starts. Particularly, it may be still in process of byte receiving. For example, if we press reset button on ESP32 module when LED is in the process. In the state LED will interpret new data incorrectly and we will see number of bright pixels on display until next write. First regular write (current time etc) will be only after about 1 second after power on or reset, and display will be ill all the time. To minimize time of the effect I call neopixel.write twice at start.

  • @SergeP, I think that for a clock the startup noise is not really a problem, because a clock is usually started rarely and tried to be kept up as long as possible. To avoid noise, you could think of adding some power control of the neopixels.

    According to NeoPixel WS2812 data sheet (as attached as .pdf), control logic and LEDs have separate power supply. That would be ideal to blank the LEDs. For clean start, power the control and keep the LEDs unpowered until after sending reset followed by complete string of desired data.

    It may not be possible with the boards you have, because most follow the simple approach and power the control logic locally for each pixel locally. This is reflected in the sample circuit in the data sheet.

    To prevent a reset - or other blocking tasks - while a send is going on, make the reset application controlled - and delay it until after all data has been sent.

    Since most neopixel products do not use the two power lines approach, you may not power the neopixels until all other things have completed the start up, then power the neopixels and send right away a reset and (blanking) data stream that you have prepared beforehand. This way you can minimize the noise moment.

  • @allObjects. You are right, 1 second noise at reset or power up (and even at not every reset or power up) is really not a big problem. So I wanted to kill it easy or to leave it as is. double write at startup seems to be easy enough to implement it. Sorry, the code looks strange and confusing. If you remove the double write (both of them) you will see no any changes most times even at power up/reset.
    But if you send new code to clock every minute :) in dark room :)) it may be interesting to use the writes to remove bright noise.

  • I've cleaned my code for big clock, now it is more regular. At the same time @Wilberforce and @JumJum have improved ESP32 build. So I've just cleaned my code even more by removing unnecessary MQTT and NTP implementations.
    Now the code works on newest Travis build for ESP32. I think it will work on v2.01 as well.
    If you do not need Russian months names just remove var MC and its usage - it is just names mapping.
    Additional fields on display may get data from http requests or MQTT channel bigclock/in/temp_1 ... temp_4 (because I use the fields to show temperatures collected by other sensors). If first symbol of the message is ', field icon is omitted. If there is no new data, '---' is shown after timeout.
    The program may be easily extended to show more fields on larger display or with multiplexing.
    Some ugly code is still present, mostly because I don't understand internal implementation of some components (for example, I have not found sntp_init() implementation and so do not know SNTP renewal interval and also SNTP timezone and E.setTimeZone() interaction).

    var DevName='bigclock';
    var DevNum=1;
    var FullName=DevName+'_'+DevNum;
    
    var neopixel = require("neopixel");
    E.setTimeZone(3);
    var wifi=require("Wifi");
    
    //var tnum;
    function getNTP() {
      wifi.setSNTP("pool.ntp.org",0);
    //  tnum=setTimeout(getNTP,24*3600*1000);
    }
    wifi.on('connected', ()=>setTimeout(getNTP,30000));
    
    //--------------------------------------­-------------------------------
    //----------------------- Additions to font ---------------------------
    //--------------------------------------­-------------------------------
    
    //require("Font6x8").add(Graphics);
    var gt = Graphics.createArrayBuffer(16,64,24,{zig­zag:true,color_order:'grb'});
    gt.setRotation(3,1);
    var font = atob("AAAAAPoAwADAAFhw2HDQAGSS/5JMAGCW+L­zSDAxSolIMEsAAPEKBAIFCPABIMOAwSAAQEHwQEA­ABBgAQEBAQAAIAAwwwwAB8ipKifABA/gBChoqSYg­CEkrLSjAAYKEj+CADkoqKinAA8UpKSDACAgI6wwA­BskpKSbABgkpKUeAAiAAEmABAoRAAoKCgoKABEKB­AAQIqQYAA8WqW9RDgOOMg4DgD+kpKSbAB8goKCRA­D+goJEOAD+kpKCAP6QkIAAfIKCklwA/hAQEP4A/g­AMAgIC/AD+EChEggD+AgICAP5AIED+AP7AMAz+AH­yCgoJ8AP6QkJBgAHyChoN8AP6QmJRiAGSSkpJMAI­CA/oCAAPwCAgL8AOAYBhjgAPAOMA7wAMYoECjGAM­AgHiDAAI6SosIA/4EAwDAMAwCB/wBAgEAAAQEBAQ­EBEn6SggQABCoqHgD+IiIcABwiIhQAHCIi/gAcKi­oYACB+oIAAGCUlPgD+ICAeAL4AAQG+AP4IFCIA/A­IAPiAeIB4APiAgHgAcIiIcAD8kJBgAGCQkPwA+EC­AgABIqKiQAIPwiADwCAjwAIBgGGCAAOAYIBjgAIh­QIFCIAIRkGGCAAJioyIgAQboEA5wCBbhAAQIDAQI­AAPFqlpUI8");
    var widths = atob("BAIEBgYGBgIEBAYGAwUCBQYDBgYGBgYGBg­YCAwQGBAUGBgYGBgUFBgYCBgYFBgYGBgYGBgYGBg­YGBgUDBQMEBgYFBQUFBQUFBQIEBQMGBQUFBQUFBA­UGBgYGBQQCBAYG");
    font+='\x08\x1E\x32\x52\x32\x1E\x08';
    font+='\x41\x93\xAF\x79\xA1\x91\x41';
    widths+='\x07\x07';
    font+='\x62\x94\x98\x90\xFE\x00';
    font+='\x3E\x08\x08\x3E\x00';
    font+='\x3E\x2A\x2A\x14\x00';
    font+='\x78\x84\xFE\x84\x78\x00';
    font+='\x3E\x20\x20\x3E\x00';
    font+='\xFE\x0C\x30\x40\xFE\x00';
    font+='\x3E\x08\x1C\x22\x1C\x00';
    font+='\x06\x18\x20\x3E\x00';
    font+='\x3E\x20\x20\x20\x00';
    font+='\x3E\x08\x14\x22\x00';
    font+='\x20\x3E\x20\x00';
    font+='\x12\x2C\x28\x3E\x00';
    font+='\x03\x7E\x82\x82\xFE\x03\x00';
    widths+='\x06\x05\x05\x06\x05\x06\x06\x0­5\x05\x05\x04\x05\x07';
    
    //8  NNNN            NNN       N   N                                 NNN
    //4 N   N           N N N      N  NN                                N  N
    //2 N   N N  N NNN  N N N NNNN N N N N  N    NN NNNN N  N NNN  NNN  N  N
    //1  NNNN N  N N  N N N N N  N N N N N N N  N N N    N N   N  N  N  N  N
    //8   N N NNNN NNN  N N N N  N NN  N NNN N  N N N    NN    N   NNN  N  N
    //4  N  N N  N N  N  NNN  N  N NN  N N N N N  N N    N N   N   N N  N  N
    //2 N   N N  N NNN    N   N  N N   N N  N  N  N N    N  N  N  N  N NNNNNN
    //1                                                                N    N
    //    82   83   84    85   86    87   88    89   8A   8B   8C  8D    8E
    var MC={'Jan':'\x82\x83\x84', 'Feb':'\x85e\x84', 'Mar':'Map', 'Apr':'A\x86p', 'May':'Ma\x8D',
            'Jun':'\x87\x88\x83', 'Jul':'\x87\x88\x89', 'Aug':'A\x84\x8A', 'Sep':'Ce\x83',
            'Oct':'O\x8B\x8C', 'Nov':'Ho\x8D', 'Dec':'\x8Ee\x8B'};
    
    
    Graphics.prototype.setFont6x8 = function() {
        this.setFontCustom(font, 32, widths, 8);
    };
    
    Graphics.prototype.convertColorFromHTML = function(HTMLColor) {
        return parseInt(HTMLColor.substr(5,2)+HTMLColor­.substr(1,2)+HTMLColor.substr(3,2),16);
    };
    
    //--------------------------------------­--------------------
    //-------------------- main code ---------------------------
    //--------------------------------------­--------------------
    
    gt.setFont6x8();
    
    var br,d;
    
    var ClockBrightness=2,LampBrightness=0;
    var ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
    var LampB =(Math.pow(2,(LampBrightness -1)/9*8)+LampBrightness /4)/256;//add linear component to be better near brightness=1
    
    //var TempIn='---', TempOut='---', TempX1='---', TempX2='---';
    
    function drawFieldDefault(fieldNum) {
      var clr=E.HSBtoRGB(fields[fieldNum].hs.h,fie­lds[fieldNum].hs.s,ClockB,true);
      gt.setColor(clr[0]/255,clr[1]/255,clr[2]­/255);
      var text=(fields[fieldNum].text[0]=="'")?
        fields[fieldNum].text.substr(1,20): //No icon if started with '
        (fields[fieldNum].icon+fields[fieldNum].­text);
      gt.drawString(text,((fields[fieldNum].w-­gt.stringWidth(text))/2|0)+fields[fieldN­um].x,fields[fieldNum].y);
    }
    
    var fields=[
      {x:0, y:0, w:32,hs:{h:0.6,  s:1  }, view:[0,1], drawField:drawFieldDefault, icon:''    , text:'---'},
      {x:32,y:0, w:32,hs:{h:0.5,  s:1  }, view:[0,1], drawField:drawFieldDefault, icon:''    , text:'---'},
      {x:0, y:8, w:32,hs:{h:0,    s:0  }, view:[0],   drawField:drawFieldDefault, icon:'\x80', text:'---', timeout:undefined},
      {x:32,y:8, w:32,hs:{h:0.334,s:1  }, view:[0],   drawField:drawFieldDefault, icon:'\x81', text:'---', timeout:undefined},
      {x:0, y:8, w:32,hs:{h:0.26, s:0.6}, view:[1],   drawField:drawFieldDefault, icon:'\x81', text:'---', timeout:undefined},
      {x:32,y:8, w:32,hs:{h:0.1  ,s:0.6}, view:[1],   drawField:drawFieldDefault, icon:'\x81', text:'---', timeout:undefined}
    ];
    var currentView=0;
    var updateCounter=0;
    function updateField(fieldNum) {
      updateCounter++;
      if(updateCounter==1) setTimeout(function() {
        if(updateCounter) {
          var clr=E.HSBtoRGB(0,0.01,LampB,true);
          gt.setBgColor(clr[0]/255,clr[1]/255,clr[­2]/255);
          gt.clear();
    
          for(let i=0;i<fields.length;i++)
            if(fields[i].view.indexOf(currentView)!=­-1) fields[i].drawField(i);
          neopixel.write(D2, gt.buffer);
          updateCounter=0;
        }
      },1200);
    }
    
    function updateSettings() {
      //ugly hack because updateField does not really use fieldNum
      updateField(-1);
    }
    
    function setView(vnum) {
      currentView=vnum;
      updateField(-1);
    }
    
    var vcnt=0;
    setInterval(function() {
      if(++vcnt==10){ vcnt=0; setView(0); }
      if(vcnt==7) setView(1);
    },1000);
    
    var oldTime, newTime;
    
    function prepareTime() {
      var ds=Date().toString();
      newTime=ds.substr((ds[14]==' ')?15:16,5);
      if(oldTime != newTime) {
        oldTime=newTime;
        fields[1].text = newTime;
        fields[0].text = Date().getDate()+' '+MC[Date().toString().substr(4,3)];
        updateField(0);
        updateField(1);
      }
    }
    setInterval(prepareTime,1000);
    
    var mqtt_server = "192.168.1.40"; // the ip of your MQTT broker
    var mqtt_options = { // ALL OPTIONAL - the defaults are below
      client_id : FullName,   // the client ID sent to MQTT - it's a good idea to define your own static one based on `getSerial()`
    };
    
    var mqtt=require("MQTT").create(mqtt_server,­mqtt_options);
    
    mqtt.on('disconnected', function() {
      console.log("MQTT disconnected... reconnecting.");
      setTimeout(function() {
        var status=wifi.getDetails();
        if( status.status && 
            status.status=="connected" && 
            !mqtt.connected 
          ) mqtt.connect();
      }, 30000);
    });
    
    wifi.on('connected', ()=>{
      mqtt.connect();
      setTimeout(getNTP,30000);
    });
    
    setInterval(()=>{
      var status=wifi.getDetails();
      if(!status.status || status.status != "connected")
      {
        if(tnum) {
          clearTimeout(tnum);
          tnum=undefined;
        }
        wifi.restore();
      }
    },60000);
    
    function prepareTemp(msg,i) {
      if(msg[0] != "'") {
        let Temp=parseFloat(msg);
        fields[1+i].text=((Temp>0)?'+':'')+Temp.­toFixed(1);
      }
      else
        fields[1+i].text=msg.substr(0,20);
      if(fields[1+i].timeout) clearTimeout(fields[1+i].timeout);
      fields[1+i].timeout=setTimeout((i)=>{fie­lds[1+i].text='---'; fields[1+i].timeout=undefined;updateFiel­d(1+i);},300000,i);
      updateField(1+i);
    }
    
    neopixel.write(D2, gt.buffer);
    neopixel.write(D2, gt.buffer);
    
    var http=require("http");
    
    function onPageRequest(req, res) {
    //  console.log('pr',req.url,Date().toString­());
      var a = url.parse(req.url, true);
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('<html><body>');
      res.write('<script src="https://cdnjs.cloudflare.com/ajax/l­ibs/jscolor/2.0.4/jscolor.min.js"></scri­pt>');
      res.write('<script>function update(hsv) { location.search="?time_color_h="+hsv[0]+­"&time_color_s="+hsv[1];}</script>'); 
      res.write('<button class="jscolor {mode:\'HS\',valueElement:null,value:\'6­6ccff\'}" style="border:2px solid black" onchange="update(this.jscolor.hsv)">Pick­ a color</button>');
    //  ,value:\'66ccff\'
    //  res.write('<p>Pin is '+(BTN.read()?'on':'off')+'</p>');
    //  res.write('<div>Mode: <a href="?mode=lamp">Lamp</a> <a href="?mode=clock">clock</a></div>');
      res.write('<div>Clock brightness: ');
      for(let i=0; i<=10; i++)
        res.write(' <a href="?clock_brightness='+i+'">'+i+'</a>­');
      res.write('</div>');
      res.write('<div>Lamp brightness: ');
      for(let i=0; i<=10; i++)
        res.write(' <a href="?lam p_brightness='+i+'">'+i+'</a>');
      res.write('</div>');
      res.end('</body></html>');
      if (a.query) {
        for(let i=1;i<=fields.length-2;i++)
          if("temp_"+i in a.query) {
            prepareTemp(a.query["temp_"+i],i);
          }
        if("lamp_brightness" in a.query) {
          LampBrightness=E.clip(parseInt(a.query.l­amp_brightness),0,10);
          LampB =(Math.pow(2,(LampBrightness -1)/9*8)+LampBrightness /4)/256;//add linear component to be better near brightness=1
          updateSettings();
        }
        if("clock_brightness" in a.query) {
          ClockBrightness=E.clip(parseInt(a.query.­clock_brightness),0,10);
          ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
          updateSettings();
        }
      }
    //  console.log('pr2',Date().toString());
    }
    
    require("http").createServer(onPageReque­st).listen(80);
    
    mqtt.on('connected', function() {
      mqtt.subscribe([
        DevName+"/in/#",
        FullName+"/in/#"
      ]);
    });
    
    mqtt.on('publish', function (pub) {
      console.log("topic: "+pub.topic);
      console.log("message: "+pub.message);
      var command=pub.topic.split("/",3)[2];
      var i;
      for(i=1;i<=fields.length-2;i++)
        if(command == "temp_"+i)
          prepareTemp(pub.message,i);
      if(command == "lamp_brightness") {
        LampBrightness=E.clip(parseInt(pub.messa­ge),0,10);
        LampB =(Math.pow(2,(LampBrightness -1)/9*8)+LampBrightness /4)/256;//add linear component to be better near brightness=1
        updateSettings();
      }
      else if(command == "clock_brightness") {
        ClockBrightness=E.clip(parseInt(pub.mess­age),0,10);
        ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
        updateSettings();
      }
      else if(command == "eval") {
        setTimeout( function (s) {
          var res=eval(s);
          console.log(res);
          mqtt.publish(DevName+'/out/eval', JSON.stringify(res));
          updateSettings();
        },0,pub.message);
      }
    });
    
    
    //---------- auto brightness ----------
    setInterval(function() {
      var newCB=ClockBrightness;
      if(newTime=="07:00")
          newCB=3;
      if(newTime=="22:00")
          newCB=1;
      if(newCB != ClockBrightness) {
        ClockBrightness=newCB;
        ClockB=(Math.pow(2,(ClockBrightness-1)/9­*8)+ClockBrightness/4)/256;//add linear component to be better near brightness=1
        updateSettings();
      }  
    },60000);
    
    //---------------- init ---------------
    
    if(wifi.getDetails().status=="connected"­) {
      getNTP();
      mqtt.connect();
    }
    
  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Simple digital clock

Posted by Avatar for SergeP @SergeP

Actions