Improvements - 20190129a

A project log for The Internet of Nixie Clocks

Wherein the tubes of Nix do bridge betwix the worlds of form and ether. I see no reason one should bridge, but that won't stop me, either.

ziggurat29ziggurat29 02/02/2019 at 03:500 Comments


Armed with the power of LFS, I begin to make improvements.  In this round, I improve resetting the clock to happen once a day, instead of every 17 minutes.


The NodeMCU will keep it's local rtc in sync by periodically performing an SNTP transaction.  When this succeeds (or fails), our code is notified.  As it was, we used the 'success' notification to update the date/time on the Nixie clock.

This works, however the updating process causes the display to go through a little flashy sequence because that's how it works.  Also, the SNTP module auto-syncs every 1000 sec (16 2/3 min), and this is not changeable by the caller.  So, the clock presently will go through the flashy sequence every 16 2/3 min.  Lastly, the SNTP sync will almost certainly not happen right at the stroke of 2 AM, when changing from daylight to standard time.  I wanted to improve this.

The first thing was to /not/ always set the Nixie upon SNTP sync.  But I definitely wanted to do that the first time, because that happens right after power up, and the Nixie is surely wrong then.  This was easily done by creating a global variable that is initially 'true', and then immediately setting it to 'false'.  Even better, we can leverage the fact that in Lua the absence of a value is logically the same as 'false', so we simply delete the variable, allowing it's RAM to be reclaimed once it has served it's purpose.

local bFirstSNTPrun = true -- so we can tell if we need to kickstart it

local function sntp_syncsuccess ( seconds, microseconds, server, info )

    local sec, usec, rate = rtctime.get()
    local utcTM = rtctime.epoch2cal(sec)
    local localTM = localtime ( utcTM, TZ )
    print ( "sntp succeeded; current local time is:  " .. 
            string.format("%04d-%02d-%02d %02d:%02d:%02d", 
            localTM.year, localTM.mon,, 
            localTM.hour, localTM.min, localTM.sec) )

    -- always update the clock on the first SNTP update, because the clock
    -- will be reset on power up and needs this asap.
    if ( bFirstSNTPrun ) then
        bFirstSNTPrun = nil -- delete it

So, the first time sntp_syncsuccess() is invoked, bFirstSNTPrun will be 'true'.  We then update the Nixie clock, and then delete that variable.  The next time sntp_syncsuccess() happens, the variable doesn't exist, which is logically 'false' and the update doesn't happen.  The method periodicClockUpdate() will update the clock and also manage the timer that will cause subsequent updates to happen on a schedule -- namely 'happen at 2 am'.

The periodicClockUpdate() takes a boolean indicating 'do update the Nixie' or 'don't update the Nixie', and I'll explain that forthwith, but for now obviously it should be 'true' in the one-time invocation coming from sntp_syncsuccess().

The implementation of periodicClockUpdate() will conditionally update the Nixie from the system RTC (and adjusted to local time), and then use a timer to schedule a one-shot event to cause a new invocation of itself at a later time.  The 'later time' is a relative time, so it is necessary to compute it as:  'the number of milliseconds to the next 2 AM from now'.  This has the added wrinkle that the next 2 AM from now could be sometime later today, or it might be tomorrow.

Lastly, it was discovered that the timer mechanism in NodeMCU has a maximum period of 6870947 milliseconds.  I don't know where this value comes from, but it is documented as such.  It's hex is 68D7A3, so it isn't a round binary value.  Who knows?  It's just what we have to deal with.  This is way too small for us, because it means a maximum delay of 1:54:30.947.  Typically we will want to delay 24 hours.  This is where the boolean parameter 'do update the Nixie' comes in.  If we compute the desired delay is greater than the maximum, then we saturate the delay to the maximum, and schedule a future invocation with 'do not update the Nixie'.  Otherwise if the delay is less than the maximum, we schedule a future invocation with 'do update the Nixie'.  Here in its glory:

local function periodicClockUpdate( bUpdate )

    if ( bUpdate ) then
        -- update the clock now

    -- determine time until another sync at the next 2 AM; either today or tomorrow
    local ueNow = rtctime.get()
    local utcTM = rtctime.epoch2cal(ueNow)
    local localTM = localtime ( utcTM, TZ )
    local nSecIntoToday = ( ( localTM.hour * 60 ) + localTM.min ) * 60 + localTM.sec
    local nSecToUpdate = 0
    if ( localTM.hour < 2 ) then
        -- will be later today at 2 am - nSecIntoToday
        nSecToNextUpdate = 7200 - nSecIntoToday
        -- will be tomorrow at (24*60*60 - nSecIntoToday) + 2*60*60
        nSecToNextUpdate = 93600 - nSecIntoToday
    nSecToNextUpdate = nSecToNextUpdate + 2 -- HHH 2 sec past 2 am

    if ( bUpdate ) then
        print ( "now:  "..ueNow..",  next update:  "..nSecToNextUpdate)
        print ( "(interim update check)  now:  " )

    -- OK, the timer has a maximum delay period 6870947 (1:54:30.947), so we 
    -- probably won't be able to reach it with one timeout.  If not, we will 
    -- have to schedule an alarm earlier, and have that skip the update until 
    -- the next try (which also may be too short).
    bUpdateNext = ( nSecToNextUpdate < 6870 )
    if ( not bUpdateNext ) then
        nSecToNextUpdate = 6870

    -- now set up a single-shot timer to call ourselves later
    local updateTimer = tmr.create()
    updateTimer:alarm(nSecToNextUpdate*1000, tmr.ALARM_SINGLE,
            function () periodicClockUpdate(bUpdateNext) end )


Once again, we're using a closure to bind the future invocation with its parameter's value to create a function which takes no parameters that will do the desired thing at a later time.

Even if the delay was long enough to reach over 24 hours we'd still want to use a one-shot timer such that we re-compute the delay to the next event instead of using a periodically recurring timer.  That way we will keep on schedule rather than compound timing errors over time, causing our update events to likely creep forward day by day.  By keeping them on schedule for 2 AM, the daily update will likely happen when folks are asleep, and also concur with the daylight/standard time switch.  As a fudge factor I added 2 seconds delay /after/ 2 AM to avoid the risk of isdst() ambiguously computing is/isn't on the day of the change.  I'm not sure I actually need to do that, but this makes me feel better, somehow.

I let it run overnight with some debugging print statements, and verified that it was working as expected.


Improve the call sequencer so that we can make additional calls, even if there is a call sequence currently in-progress.