nodemcu-firmware/docs/lua-modules/sntp.md

8.8 KiB

SNTP Module

Since Origin / Contributor Maintainer Source
2019-07-01 nwf nwf sntp.lua

This is a user-friendly, Lua wrapper around the sntppkt module to facilitate the use of SNTP.

!!! note

   This `sntp` module is expected to live in LFS, and so all the documentation
   uses `node.flashindex()` to find the module.  If the module is instead
   loaded as a file, use, for example, `(require "sntp").go()` instead, but
   note that this may consume a large amount of heap.

Default Constructor Wrapper

The simplest use case is to use the go function with no arguments like this:

node.flashindex("sntp")().go()

This will...

  • use rtctime to interface with the local clock;

  • use a default list of servers, including the server provided by the local DHCP server, if any;

  • re-synchronize the clock every half-hour in the steady state;

  • attempt to discipline the local oscillator to improve timing accuracy (but note that this may take a while to converge; advanced users may wish to manually persist drift rate across reboots using the rtctime interface); and

  • rapidly re-discipline the local oscillator when a large deviance is observed (e.g., at cold boot)

The sntp object encapsulating the state machine is returned, but this may be safely ignored in simple use cases. (This object is augmented with tmr, the timer used to manage resychronization, should you wish to dynamically start, stop, or delay synchronization.)

You can provide your own list of servers, too, overriding the default:

node.flashindex("sntp")().go({ "ntp-server-1", "ntp-server-2" })

Or, to just use the server provided by DHCP and no others, when one is certain that the DHCP server will provide one,

do
  local ifi = net.ifinfo(0)
  local srv = ifi and ifi.dhcp and ifi.dhcp.ntp_server
  if srv then node.flashindex("sntp")().go({srv}) end
end

Similarly, the frequency of synchronization can be changed:

-- use default servers and synchronize every ten minutes
node.flashindex("sntp")().go(nil, 600000)

Success and failure callbacks can be given as well, for advanced use or increased reporting:

node.flashindex("sntp")().go(nil, 1200000,
  function(res, serv, self)
    print("SNTP OK", serv, res.theta_s, res.theta_us, rtctime.get())
  end,
  function(err, srv, rply)
    if     err == "all"    then print("SNTP FAIL", srv)
    elseif err == "kod"    then print("SNTP server kiss of death", srv, rply)
    elseif err == "goaway" then print("SNTP server rejected us", srv, rply)
    else                        print("SNTP server unreachable", srv, err)
    end
  end)

Details of the parameters to the callbacks are given below. The remainder of this document details increasingly internal details, and is likely of decreasing interest to general audiences.

Syntax

node.flashindex("sntp")().go([servers, [frequency, [success_cb, [failure_cb]]]])

Notes

Interaction with the Garbage Collector

As our network stack does not capture the time of received packets (nor does it know how to timestamp egressing NTP packets as dedicated hardware does), there is a fair bit of local processing delay, as we have to come into Lua to get the local timestamps. For higher-precision time keeping, if possible, it may help to move the device to a (E)GC mode which has more slop than the default, which prioritizes prompt reclaim of memory. Consider, for example, something like node.egc.setmode(node.egc.ON_MEM_LIMIT, -4096) to permit the SNTP logic to run (more often) without interference of the GC. (The go function above defaults to collecting garbage before triggering SNTP synchronization.)

SNTP Object Constructor

sntp = node.flashindex("sntp")().new(servers, success_cb, [failure_cb], [clock])

where

  • servers specifies the name(s) of the (S)NTP server(s) to use; it may be...

    • a string, either a DNS name or an IPv4 address in dotted quad form,
    • an array of the above
    • nil to use any DHCP-provided NTP server and some default *.nodemcu.pool.ntp.org servers.
  • success_cb is called back at the end of a synchronization when at least one server replied to us. It will be given three arguments:

    • the preferred SNTP result converted to a a table; see sntppkt.res.totable below.
    • the name of the server whence that result came
    • the sntp object itself
  • failure_cb may be nil but, otherwise, is called back in two circumstances:

    • at the end of a pass during which no server could be reached. In this case, the first argument will be the string "all", the second will be the number of servers tried, and the third will be the sntp object itself.

    • an individual server has failed in some way. In this case, the first argument will be one of:

      • "dns" (if name resolution failed),
      • "timeout" (if the server failed to reply in time),
      • "goaway" (if the server refused to answer), or
      • "kod" ("kiss of death", if the server told us to stop contacting it entirely).

      In all cases, the name of the server is the second argument and the sntp object itself is the third; in the "goaway" case, the fourth argument will contain the refusal string (e.g., "RATE" for rate-limiting or "DENY" for kiss-of-death warnings.

      In the case of kiss-of-death packets, the server will be removed from all subsequent syncs. This may result in there eventually being no servers to contact. Paranoid applications should therefore monitor failures!

  • clock, if given, should return two values describing the local clock in seconds and microseconds (between 0 and 1000000). If not given, the module will fall back on rtctime.get; if rtctime is not available, a clock must be provided. Using function() return 0, 0 end will result in the "clock offset" (theta) reported by the success callback being a direct estimate of the true time.

Other module methods

The module contains some other utility functions beyond the SNTP object constructor and the go utility function detailed above.

update_rtc()

Given a result from a SNTP sync pass, update the local RTC through rtctime. Attempting to use this function without rtctime support will raise an error.

sntpobj is used to track state between syncs and should be the "sntp object" given in the success and failure callbacks. (In principle, any Lua table will do, but that is the most convenient one. All data is passed using fields whose keys are strings with prefix rtc_.)

Syntax

node.flashindex("sntp")().update_rtc(res, sntpobj)

SNTP object methods

sync()

Run a pass through the specified servers and call back as described above.

Syntax

sntpobj:sync()

stop()

Abort any pass in progress; no more callbacks will be called. The current preferred response and server name (i.e., the arguments to the success callback, should the pass end now) are returned.

Syntax

sntpobj:stop()

servers

The table of NTP servers currently being used. Please treat this as read-only. This may be investigated to see if kiss-of-death processing has removed any servers, but one is probably better off listening for the failure callback notifications.

Internal Details: sntppkt response object methods

sntppkt.resp.totable()

Expose a sntppkt.resp result as a Lua table with the following fields:

  • theta_s: Local clock offset, seconds component

  • theta_us: Local clock offset, microseconds component

  • delta: An estimate of the error, in 65536ths of a second (i.e., approx 15.3 microseconds)

  • delta_r: The server's estimate of its error, in 65536ths of a second

  • epsilon_r: The server's estimate of its dispersion, in 65536ths of a second

  • leapind: The leap-second indicator

  • stratum: The server's stratum

  • rx_s: Packet reception timestamp, seconds component

  • rx_us: Packet reception timestamp, microseconds component

  • raw: The sntppkt.resp itself, so that we can pass this table around to user Lua code and still retain access to the raw internals in, for example, drift_compensate, below. See the use in update_rtc.

Note that negative offsets will be represented with a negative theta_s and a positive theta_us. For example, -200 microseconds would be -1 seconds and 999800 microseconds.

Syntax

res:totable()

sntppkt.resp.pick()

Used internally to select among multiple responses; see source for usage.

sntppkt.resp.drift_compensate()

Encapsulates a Proportional-Integral (PI) controller update step for use in disciplining the local oscillator. Used internally to update_rtc; see source for usage.