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

240 lines
8.8 KiB
Markdown

# SNTP Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2019-07-01 | [nwf](https://github.com/nwf) | [nwf](https://github.com/nwf) | [sntp.lua](../../lua_modules/sntp/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:
```lua
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:
```lua
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,
```lua
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:
```lua
-- 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:
```lua
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
```lua
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.