Controlling the Clock Programmatically

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 01/29/2019 at 00:560 Comments


Clock control functions have been added, and SNTP synchronization is effected.


Back before my first attempt failed, I had managed to control the clock a little from the serial port.  First we needed to configure it for the correct bit rate.  This was added at the start of the section of the immediately executed code:

uart.setup(1, 9600, 8, uart.PARITY_NONE, uart.STOPBITS_1)

According to the Nixie clock's documentation, the following commands exist:

These commands have no termination (e.g. CR/LF, etc).  Experimenting, I found that the clock seems to depend on there being some sort of delay to infer that a command has been sent.  For instance, sending a partial display's worth:

uart.write(1, "5 4 3")
uart.write(1, " 2 1 ")

will result in "5 4 3 2 1 " being shown, but if there is a delay between those two writes, then the " 2 1 " will overwrite the prior "5 4 3".  How much delay?  Unknown.

Also, more annoyingly, you need to pace out the digits before doing updates.  For instance, to update the time, you are meant to load the display with the desired time, switch to time display via 't', and then apply the change with 'u'.  However you cannot send the digits followed by 'tu'.  It will ignore those commands.  You need to pace them out, with a little delay before the 't' and another delay before the 'u'.  How much delay?  Unknown; it's not documented as being needed at all.

As mentioned before, delays via a spin-wait are verboten in NodeMCU, so I will need to use timers in some way.  That's a pity.  (and a PITA; You can come to my PITA pity party!)

The documentation seems to suggest that you should send a full displays worth of digits at one time, though that doesn't seem to be completely true.  You can send a partial display so long as it starts on the left.

Also, the 'a' command seems to do nothing on my clock.  Fortunately, I am not going to use the alarm feature.

Documentation says that other characters are ignored, but you know I had to try.  I found some buried treasure in undocumented commands:

Making Clock Functions

I made clock functions in Lua.  Most are trivial, e.g.:

function clock_show_time()
    --switch to time display mode
    uart.write(1, "t")

and did the same sort of thing for:

function clock_show_date()
function clock_update()
function clock_blank()
function clock_zero()

I also created a clock_send_time() and clock_send_date() function.  The clock documentation did not specify how to send the digits to cause 'u' to update the date or time correctly, but by experimentation, I found that you are expected to place the digits into the expected spots where the clock or date normally have them, and then 'u' will work.  So I created those function by using Lua's string formatting capabilities (roughly similar to a printf()):

function clock_send_time ( tm )
    --format string for updating time
    local strTimeSet = string.format("  %02d %02d %02d", 
            tm.hour, tm.min, tm.sec)
    --send strTimeSet
    uart.write(1, strTimeSet)

function clock_send_date ( dt )
    --format string for updating time
    local strDateSet = string.format("    %02d%02d%02d", 
            dt.year%100, dt.mon,
    --send strDateSet
    uart.write(1, strDateSet)

OK, so I should explain why I used a struct (er, 'table') as a parameter, instead of the three values h, m, s (or y, m, d).  The reason is that this makes other things easier.  In particular, the way to get the time and date from the NodeMCU is like this:

local tm = rtctime.epoch2cal(rtctime.get())

and this returns a struct that has the hour, min, sec, year, mon, day in it.  So by making clock_send_time() and clock_send_date() expect that sort of struct as a parameter, it slightly simplifies the code that is ultimately going to be calling those methods, anyway.

All this works by way of testing with the interactive console, but I still have a problem with the programmatic control, because I need the delays between the various calls.

I started making some code that was a little like what was done in the connect_and_run() method, but it looked klunky, and required a bunch of cut-and-paste for the various command sequences.  Later, I got a different idea:  a generic call sequencer.  Then I could reuse that command pacing logic for arbitrary call sequences.

Call Sequencer

The gist of the call sequencer is that you pass it an 'array' (in the Lua sense) of arbitrary functions, and those functions will get invoked sequentially, with a specified delay between each invocation.

function run_sequence ( sequence, period_us )
    if ( not sequence or 0 == #sequence ) then return end --silly cases
    --do the first one immediately
    local nIdx = 1
    nIdx = nIdx + 1
    --if that was it, beat it
    if ( nIdx > #sequence ) then return end
    --remaining ones are to be paced out
    local pacingTimer = tmr.create()
    pacingTimer:alarm(period_us, tmr.ALARM_AUTO, function(timer)
        if ( nIdx <= #sequence ) then
            nIdx = nIdx + 1
        if ( nIdx > #sequence ) then
            -- we are completed
    end )

So, it will call the first function immediately, then if there are more, it will register a timer which will invoke the remaining functions.  Because Lua can create closures via it's upvalue mechanism, any parameters to the function are packaged along with it.

This allows me to create the setting functions like this:

function clock_set_time ( tm )
sequence = { function () clock_send_time(tm) end, 
        clock_show_time, clock_update }
run_sequence ( sequence, 250 )

function clock_set_date ( dt )
sequence = { function () clock_send_date(dt) end, 
        clock_show_date, clock_update, clock_show_time }
run_sequence ( sequence, 250 )

The first member of those sequences are examples of creating a closure.  The other functions do not take parameters, so a closure was not needed for them.

There is a wart in this design, though, in that the run_sequence() should not be called again until any prior sequences have completed.  I did not address this at this time, but instead punted with a clock_set_now() function that combines getting the current date/time, and setting the time, and the date, and switching back to time display:

function clock_set_now()
    --get current date and time
    local sec, usec, rate = rtctime.get()
    local tm = rtctime.epoch2cal(sec)
    --update the clock
    sequence = { function () clock_send_time(tm) end, 
            clock_show_time, clock_update,
            function () clock_send_date(tm) end, 
            clock_show_date, clock_update, 
            clock_show_time }
    run_sequence ( sequence, 250 )

I dreaded trying to figure out any contention solving mechanisms -- possibly using coroutines (Lua doesn't have preemptive threads or locks), but it occurred to me that I should simply have the run_sequence() mechanism post to a single, shared, queue, and subsequent calls to run_sequence() will simply append items to that queue, kickstarting it if needed, and not kickstarting if not needed.  Because NodeMCU is a single-threaded environment, it will not be necessary to lock access to the queue.  Any timer events are pended until our code has yielded to the system, anyway.

But for now, I am leaving it like this, since I want to move on.

For the last step, I simply added the call to clock_set_now() at the end of the exiting sntp_syncsuccess() method.  So the program flow is now:

  1. process configuration
  2. connect to WiFi
  3. start an SNTP sync operation
  4. when SNTP syncs successfully, call clock_set_now()
  5. handle the timer event that will sequence out calls to set the clock up

The SNTP sync is set to automatically resync, which is hard-coded in the library to be every 1000 sec, so about 16 2/3 min.  As presently written, I setup the clock every time this happens, however I will probably change that to do so less often -- maybe once per day.  The reason is that the clock updating has visible effects which might be distracting to the onlooker.  It's not critical, though, so I'll save that optimisation for later.

I uploaded the code to the board, and beheld with wonder the clock synchronising itself via the Internet!  Ha!  So this project wasn't quite as silly is I originally thought it would be.

I still need to make a 'server' so that I can fiddle with the clock via the network (probably just displaying digits, but maybe implementing stopwatches or something else more interesting).  But for now, a more pressing matter is apparent:  timezone.  The clock is showing UTC time, not local.  The NodeMCU libraries have nothing for dealing with timezones, so I'm on my own there.


Coding in timezone support.