diff --git a/app/modules/sntppkt.c b/app/modules/sntppkt.c new file mode 100644 index 0000000000..7d943b57b5 --- /dev/null +++ b/app/modules/sntppkt.c @@ -0,0 +1,388 @@ +/* + * Copyright 2015 Dius Computing Pty Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * - Neither the name of the copyright holders nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Johny Mattsson + * @author Nathaniel Wesley Filardo + */ + +// Module for Simple Network Time Protocol (SNTP) packet processing; +// see lua_modules/sntp/sntp.lua for the user-friendly bits of this. + +#include "module.h" +#include "lauxlib.h" +#include "lmem.h" +#include "os_type.h" +#include "osapi.h" +#include "lwip/udp.h" +#include +#include "user_modules.h" +#include "lwip/dns.h" +#include "task/task.h" +#include "user_interface.h" + +#define max(a,b) ((a < b) ? b : a) + +#define NTP_PORT 123 +#define NTP_ANYCAST_ADDR(dst) IP4_ADDR(dst, 224, 0, 1, 1) + +#if 0 +# define sntppkt_dbg(...) dbg_printf(__VA_ARGS__) +#else +# define sntppkt_dbg(...) +#endif + +typedef struct +{ + uint32_t sec; + uint32_t frac; +} ntp_timestamp_t; + +static const uint32_t NTP_TO_UNIX_EPOCH = 2208988800ul; + +typedef struct +{ + uint8_t mode : 3; + uint8_t ver : 3; + uint8_t LI : 2; + uint8_t stratum; + uint8_t poll; + uint8_t precision; + uint32_t delta_r; + uint32_t epsilon_r; + uint32_t refid; + ntp_timestamp_t ref; + ntp_timestamp_t origin; + ntp_timestamp_t recv; + ntp_timestamp_t xmit; +} __attribute__((packed)) ntp_frame_t; + +#define NTP_RESPONSE_METATABLE "sntppkt.resp" +typedef struct { + /* Copied from incoming packet */ + uint32_t delta_r; + uint32_t epsilon_r; + uint8_t LI; + uint8_t stratum; + + /* Computed as per RFC 5905; units are 2^(-32) seconds */ + int64_t theta; + int64_t delta; + + /* Local computation */ + uint32_t rx_s; + uint32_t rx_us; + uint32_t cached_delta; +} ntp_response_t; + +static uint64_t +sntppkt_div1m(uint64_t n) { + uint64_t q1 = (n >> 5) + (n >> 10); + uint64_t q2 = (n >> 12) + (q1 >> 1); + uint64_t q3 = (q2 >> 11) - (q2 >> 23); + + uint64_t q = n + q1 + q2 - q3; + + q = q >> 20; + + // Ignore the error term -- it is measured in pico seconds + return q; +} + +static uint32_t +sntppkt_us_to_frac(uint64_t us) { + return sntppkt_div1m(us << 32); +} + +static const uint32_t MICROSECONDS = 1000000; + +static uint32_t +sntppkt_frac16_to_us(uint64_t frac) { + return (frac * MICROSECONDS) >> 16; +} + +/* + * Convert sec/usec to a Lua string suitable for depositing into a SNTP packet + * buffer. This is a little gross, but it's not the worst thing a C + * programmer's ever done, I'm sure. + */ +static int +sntppkt_make_ts(lua_State *L) { + ntp_timestamp_t ts; + uint32_t usec; + + ts.sec = htonl(luaL_checkinteger(L, 1) + NTP_TO_UNIX_EPOCH) ; + usec = luaL_checkinteger(L, 2) ; + ts.frac = htonl(sntppkt_us_to_frac(usec)); + + lua_pushlstring(L, (const char *)&ts, sizeof(ts)); + return 1; +} + +/* + * Process a SNTP packet as contained in a Lua string, given a cookie timestamp + * and local clock second*usecond pair. Generates a ntp_response_t userdata + * for later processing or a string if the server is telling us to go away. + * + * :: string (packet) + * -> string (cookie) + * -> int (local clock, sec component) + * -> int (local clock, usec component) + * -> sntppkt.resp + * + */ +static int +sntppkt_proc_pkt(lua_State *L) { + const char *pkts; + size_t pkts_len; + + uint32_t now_sec; + uint32_t now_usec; + + ntp_timestamp_t *cookie; + size_t cookie_len; + + ntp_response_t *ntpr; + + // make sure we have an aligned copy to work from + // XXX nwf: is this necessary? + ntp_frame_t pktb; + + now_usec = luaL_checkinteger(L, 4); + now_sec = luaL_checkinteger(L, 3); + + luaL_checktype(L, 2, LUA_TSTRING); + cookie = (ntp_timestamp_t*) lua_tolstring(L, 2, &cookie_len); + if (cookie_len != sizeof(*cookie)) { + luaL_error(L, "Bad expected cookie"); + } + + luaL_checktype(L, 1, LUA_TSTRING); + pkts = lua_tolstring(L, 1, &pkts_len); + if (pkts_len != sizeof(pktb)) { + luaL_error(L, "Bad packet length"); + } + os_memcpy (&pktb, pkts, sizeof(pktb)); + + if (memcmp((const char *)cookie, (const char *)&pktb.origin, sizeof (*cookie))) { + /* bad cookie; return nil */ + return 0; + } + + /* KOD? */ + if (pktb.LI == 3) { + lua_pushlstring(L, (const char *)&pktb.refid, 4); + return 1; + } + + ntpr = lua_newuserdata(L, sizeof(ntp_response_t)); + luaL_getmetatable(L, NTP_RESPONSE_METATABLE); + lua_setmetatable(L, -2); + + ntpr->rx_s = now_sec; + ntpr->rx_us = now_usec; + ntpr->LI = pktb.LI; + ntpr->stratum = pktb.stratum; + + // NTP Short Format: 16 bit seconds, 16 bit fraction + ntpr->delta_r = ntohl(pktb.delta_r); + ntpr->epsilon_r = ntohl(pktb.epsilon_r); + + /* Heavy time lifting time */ + + // NTP Long Format: 32 bit seconds, 32 bit fraction + pktb.origin.sec = ntohl(pktb.origin.sec); + pktb.origin.frac = ntohl(pktb.origin.frac); + pktb.recv.sec = ntohl(pktb.recv.sec); + pktb.recv.frac = ntohl(pktb.recv.frac); + pktb.xmit.sec = ntohl(pktb.xmit.sec); + pktb.xmit.frac = ntohl(pktb.xmit.frac); + + // When we sent it (our clock) + uint64_t ntp_origin = (((uint64_t) pktb.origin.sec ) << 32) + + pktb.origin.frac; + // When they got it (their clock) + uint64_t ntp_recv = (((uint64_t) pktb.recv.sec ) << 32) + + pktb.recv.frac; + // When they replied (their clock) + uint64_t ntp_xmit = (((uint64_t) pktb.xmit.sec ) << 32) + + pktb.xmit.frac; + // When we got it back (our clock) + uint64_t ntp_dest = (((uint64_t) now_sec + NTP_TO_UNIX_EPOCH ) << 32) + + sntppkt_us_to_frac(now_usec); + + // | outgoing offset | | incoming offset | + ntpr->theta = (int64_t)(ntp_recv - ntp_origin) / 2 + (int64_t)(ntp_xmit - ntp_dest) / 2; + + // | our clock delta | | their clock delta | + ntpr->delta = (int64_t)(ntp_dest - ntp_origin) / 2 + (int64_t)(ntp_xmit - ntp_recv) / 2; + + ntpr->cached_delta = ntpr->delta_r + (ntpr->delta >> 17); + + sntppkt_dbg("SNTPPKT PROC n_r=%llx n_o=%llx n_x=%llx n_d=%llx th=%llx " + "d=%llx + %lx = cd=%lx\n", + ntp_recv, ntp_origin, ntp_xmit, ntp_dest, ntpr->theta, + ntpr->delta, ntpr->delta_r, ntpr->cached_delta); + + return 1; +} + +/* + * Left-biased selector of a "preferred" NTP response. Note that preference + * is rather subjective! + * + * Returns true iff we'd prefer the second response to the first. + * + * :: sntppkt.resp -> sntppkt.resp -> boolean + */ + +static int +sntppkt_resp_pick(lua_State *L) { + + ntp_response_t *a = luaL_checkudata(L, 1, NTP_RESPONSE_METATABLE); + ntp_response_t *b = luaL_checkudata(L, 2, NTP_RESPONSE_METATABLE); + int biased = 0; + + biased = lua_toboolean(L, 3); + + /* + * If we're "biased", prefer the second structure if the delay less than + * 3/4ths of the delay in the first. An unbiased comparison just uses + * the raw delay values. + */ + if (biased) { + lua_pushboolean(L, a->cached_delta * 3 > b->cached_delta * 4); + } else { + lua_pushboolean(L, a->cached_delta > b->cached_delta ); + } + return 1; +} + +static void +field_from_number(lua_State *L, const char * field_name, lua_Number value) { + lua_pushnumber(L, value); + lua_setfield(L, -2, field_name); +} + +/* + * Inflate a NTP response into a Lua table + * + * :: sntppkt.resp -> { } + */ +static int +sntppkt_resp_totable(lua_State *L) { + ntp_response_t *r = luaL_checkudata(L, 1, NTP_RESPONSE_METATABLE); + + lua_createtable(L, 0, 6); + + sntppkt_dbg("SNTPPKT READ th=%llx\n", r->theta); + + field_from_number(L, "theta_s", r->theta >> 32); + field_from_number(L, "theta_us", ((r->theta & 0xFFFFFFFF) * MICROSECONDS) >> 32); + + field_from_number(L, "delta", r->delta >> 16); + field_from_number(L, "delta_r", r->delta_r); + field_from_number(L, "epsilon_r", r->epsilon_r); + + field_from_number(L, "leapind", r->LI); + field_from_number(L, "stratum", r->stratum); + field_from_number(L, "rx_s" , r->rx_s); + field_from_number(L, "rx_us" , r->rx_us); + + return 1; +} + +/* + * Compute local RTC drift rate given a SNTP response, previous sample time, + * and error integral value. Returns new rate and integral value. + * + * Results are only sensible if resp->theta is sufficiently small (i.e., less + * than a second) and the inter-sample duration must, of course, be positive. + * + * :: sntppkt.resp + * -> int (prior sample time, seconds component) + * -> int (prior sample time, microseconds component) + * -> int (integral) + * -> int (rate), int (integral) + * + */ +static int +sntppkt_resp_drift_compensate(lua_State *L) { + ntp_response_t *resp = luaL_checkudata(L, 1, NTP_RESPONSE_METATABLE); + + int32_t theta32 = resp->theta >> 32; + if (theta32 != 0 && theta32 != -1) { + return luaL_error(L, "Large deviation"); + } + + uint32_t prior_s = luaL_checkinteger(L, 2); + uint32_t prior_us = luaL_checkinteger(L, 3); + int32_t err_int = luaL_checkinteger(L, 4); + + uint64_t prior = ((uint64_t)prior_s << 32) + sntppkt_us_to_frac( prior_us); + uint64_t rx = ((uint64_t)resp->rx_s << 32) + sntppkt_us_to_frac(resp->rx_us); + + int64_t isdur = rx - prior; + if (isdur <= 0) { + return luaL_error(L, "Negative time base"); + } + + /* Compute our drift rate over 2 */ + int32_t drift2 = ((resp->theta << 31) / isdur) >> 32; + + int32_t newrate = drift2 * 2 + err_int; + err_int += drift2 >> 1; + + lua_pushnumber(L, newrate); + lua_pushnumber(L, err_int); + return 2; +} + +LROT_BEGIN(sntppkt_resp) + LROT_FUNCENTRY( pick, sntppkt_resp_pick ) + LROT_FUNCENTRY( totable, sntppkt_resp_totable ) + LROT_FUNCENTRY( drift_compensate, sntppkt_resp_drift_compensate ) + + LROT_TABENTRY( __index, sntppkt_resp ) +LROT_END(sntppkt_resp, sntppkt_resp, 0) + +static int +sntppkt_init(lua_State *L) +{ + luaL_rometatable(L, NTP_RESPONSE_METATABLE, LROT_TABLEREF(sntppkt_resp)); + return 0; +} + +// Module function map +LROT_BEGIN(sntppkt) + LROT_FUNCENTRY( make_ts , sntppkt_make_ts ) + LROT_FUNCENTRY( proc_pkt , sntppkt_proc_pkt ) +LROT_END( sntppkt, NULL, 0 ) + +NODEMCU_MODULE(SNTPPKT, "sntppkt", sntppkt, sntppkt_init); diff --git a/docs/lua-modules/sntp.md b/docs/lua-modules/sntp.md new file mode 100644 index 0000000000..eb3a58316e --- /dev/null +++ b/docs/lua-modules/sntp.md @@ -0,0 +1,129 @@ +# 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. + +## Constructor +```lua +sntp = (require "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 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, as an opaque `sntppkt.resp` userdata + * 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 + +* `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. + +## SNTP object methods + +### sntp.sync() +#### Syntax +`sntp:sync()` + +Run a pass through the specified servers and call back as described above. + +### sntp.stop() +#### Syntax +`sntp:stop()` + +Abort any pass in progress; no more continuations 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. + +## sntppkt response object methods + +### sntppkt.resp.totable() +#### Syntax +`res: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 16ths of a second +* `delta_r`: The server's estimate of its error +* `epsilon_r`: The server's estimate of its dispersion + +* `leapind`: The leap-second indicator +* `stratum`: The server's stratum + +* `rx_s`: Packet reception timestamp, seconds component +* `rx_us`: Packet reception timestamp, microseconds component + +### sntppkt.resp.pick() + +Used internally to select among multiple responses. + +### sntppkt.resp.drift_compensate() + +Used internally to `update_rtc` to compute the RTC drift rate. + +## Other module functions + +The module contains some other utility functions beyond the SNTP object +constructor. + +### update_rtc() +#### Syntax +`update_rtc(res)` + +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. + +## Example usage + +```lua +sntpm = require "sntp" +sntp = sntpm.new(nil, + function(res, serv, self) + print("SNTP OK", serv) + sntpm.update_rtc(res, res:totable(), self) + end, + function(err, srv, rply) + if err == "all" then print("SNTP FAIL", #srv) + elif err == "goaway" then print("SNTP server rejected us", srv, rply) + else print("SNTP server unreachable", srv, err) + end + end) + +-- Every five minutes, re-run SNTP +sntptmr = tmr.create() +sntptmr:alarm(300000, tmr.ALARM_AUTO, sntp.sync) +``` diff --git a/lua_modules/sntp/sntp.lua b/lua_modules/sntp/sntp.lua new file mode 100644 index 0000000000..0734db5e8b --- /dev/null +++ b/lua_modules/sntp/sntp.lua @@ -0,0 +1,267 @@ +local MAX_SERVER_ATTEMPTS = 2 +local SNTP_TIMEOUT = 5000 + + -- sk and fk are our Success and Failure Kontinuations, resp. +return { +new = function(serv, sk, fk, now) + + if type(serv) == "string" then serv = {serv} + elseif serv == nil then serv = + { + "0.nodemcu.pool.ntp.org", + "1.nodemcu.pool.ntp.org", + "2.nodemcu.pool.ntp.org", + "3.nodemcu.pool.ntp.org", + } + elseif type(serv) ~= "table" then error "Bad server table" + end + + if type(sk) ~= "function" and type(sk) ~= "lightfunction" then + error "Bad success continuation type" + end + if fk ~= nil and type(fk) ~= "function" and type(fk) ~= "lightfunction" then + error "Bad failure continuation type" + end + if now ~= nil and type(now) ~= "function" and type(now) ~= "lightfunction" then + error "Bad clock type" + end + now = now or (rtctime and rtctime.get) + if now == nil then error "Need clock function" end + + local self = {} + + local _tmr -- contains the currently running timer, if any + local _udp -- the socket we're using to talk to the world + + local _kod = {} -- kiss of death flags accumulated accoss syncs + local _pbest -- best server from prior pass + + local _six -- index of the server in serv to whom we are speaking + local _sat -- number of times we've tried to reach this server + local _res -- the best result we've got so far + local _best -- best server this pass, for updating _pbest + + -- Shut down the state machine + -- + -- upvals: _tmr, _udp, _six, _sat, _res, _best + local function _stop() + -- print("sntp", "stop") + + _six = nil + _sat = nil + _res = nil + _best = nil + + -- stop any time-based callbacks and drop tmr + if _tmr then + _tmr:unregister() + _tmr = nil + end + + -- stop any UDP callbacks and drop the socket + if _udp then + _udp:on("receive", nil) + _udp:on("sent" , nil) + _udp:on("dns" , nil) + _udp:close() + _udp = nil + end + end + + local nextServer + local doserver + + -- Try communicating with the current server + -- + -- upvals: SNTP_TIMEOUT, now, _tmr, _udp, _best, _kod, _pbest, _res, _six + local function hail(ip) + -- print("sntp", "hail", ip) + + _tmr:alarm(SNTP_TIMEOUT, tmr.ALARM_SINGLE, function() + -- print("sntp", "hail-tmr") + _udp:on("sent", nil) + _udp:on("receive", nil) + return doserver("timeout") + end) + + -- _udp:on("sent", function() + -- print("sntp", "udp-on-sent") + -- _udp:on("sent", nil) + -- end) + + local txts = sntppkt.make_ts(now()) + + _udp:on("receive", + -- upvals: now, ip, _tmr, _best, _kod, _pbest, _res, _six + function(skt, d, port, rxip) + -- print("sntp", "udp-on-recv", rxip, port) + + -- many things constitute bad packets; drop with tmr running + if rxip ~= ip and rxip ~= "224.0.1.1" then return end -- wrong peer + if port ~= 123 then return end -- wrong port + if #d < 48 then return end -- too short + + local pok, pkt = pcall(sntppkt.proc_pkt, d, txts, now()) + + if not pok or pkt == nil then + -- sntppkt can also reject the packet for having a bad cookie; + -- this is important to prevent processing spurious or delayed responses + return + elseif type(pkt) == "string" then + -- print("sntp", "udp-on-recv", "goaway", pkt) + if pkt == "DENY" then -- KoD packet + if _kod[_six] then + if fk then fk("kod", serv[_six], self) end + _kod[_six] = nil + table.remove(serv, _six) + _six = _six - 1 -- nextServer will add one + else + _kod[_six] = 1 + if fk then fk("goaway", serv[_six], self, pkt) end + end + else + if fk then fk("goaway", serv[_six], self, pkt) end + end + return nextServer() + end + + _kod[_six] = nil + + if _pbest == serv[_six] then + -- this was our favorite server last time; if we don't have a + -- result or if we'd rather this one than the result we have... + if not _res or not pkt:pick(_res, true) then + _res = pkt + _best = _pbest + end + else + -- this was not our favorite server; take this result if we have no + -- other option or if it compares favorably to the one we have, which + -- might be from our favorite from last pass. + if not _res or _res:pick(pkt, _pbest == _best) then + _res = pkt + _best = serv[_six] + end + end + + _tmr:unregister() + skt:on("receive", nil) -- skt == _udp + skt:on("sent", nil) + return nextServer() + end) + + return _udp:send(123, ip, + -- '#' == 0x23: version 4, mode 3 (client), no LI + "#\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + .. txts) + end + + -- upvals: _sat, _six, _udp + function doserver(err) + if _sat == MAX_SERVER_ATTEMPTS then + if fk then fk(err, serv[_six], self) end + return nextServer() + end + _sat = _sat + 1 + + return _udp:dns(serv[_six], function(skt, ip) + + -- XXX Because stop() always drops the socket, having nuked its DNS + -- callback, we should never attempt to resurrect a stale scan if DNS + -- results come back after stop()-ing the state machine. If that turns + -- out not to be true, then we can do something like this to check that + -- the callback is being run in the correct context. + -- + -- -- if skt ~= _udp then + -- -- print("sntp", "udp-on-dns", "stale socket") + -- -- return + -- -- end + + -- print("sntp", "udp-on-dns", ip) + skt:on("dns", nil) -- skt == _udp + if ip == nil then return doserver("dns") else return hail(ip) end + end) + end + + -- Move on to the next server or finish a pass + -- + -- upvals: fk, serv, sk, _best, _pbest, _res, _sat, _six + function nextServer() + if _six >= #serv then + if _res then + _pbest = _best + local res = _res + local best = _best + _stop() + return sk(res, best, self) + else + _stop() + if fk then return fk("all", #serv, self) else return end + end + end + + -- print("sntp", "next", _six) + + _six = _six + 1 + _sat = 0 + return doserver() + end + + -- Poke all the servers and invoke the user's callbacks + -- + -- upvals: _tmr, _udp, _six + function self.sync() + _stop() + _udp = net.createUDPSocket() + _tmr = tmr.create() + _udp:listen() -- on random port + _six = 0 + nextServer() + end + + function self.stop() + local res, best = _res, _best + _stop() + return res, best + end + + return self + +end, + +-- A utility function which applies a result to the rtc +update_rtc = function(res, rest, obj) + local rate = nil + if rest.theta_s > 2 then + obj.last_rx_s = nil -- too big, don't try drift compensation + else + if obj.last_rx_s ~= nil then + -- adjust drift compensation. We have three pieces of information: + -- + -- our idea of time at rx (res/rest.rx_*), + -- our idea of time at the last sync (obj.last_rx_*) + -- the measured theta now (res/rest.theta_us) + -- + -- Since we set theta to 0 as of the last sync (well, as best we + -- could, anyway), any theta now "must" be due to local clock + -- drift. We're going to integrate this signal over time and use + -- that to mediate the rate we set, making this a PI controller. + ok, rate, obj.err_int = pcall(res.drift_compensate, res, + obj.last_rx_s, obj.last_rx_us, + obj.err_int or 0) + -- print("SNTP RATE", rate, obj.err_int) + if not ok then rate = nil end + end + obj.last_rx_s = rest.rx_s + obj.last_rx_us = rest.rx_us + end + local now_s, now_us = rtctime.get() + local new_s, new_us = now_s + rest.theta_s, now_us + rest.theta_us + if new_us > 1000000 then + new_s = new_s + 1 + new_us = new_us - 1000000 + end + rtctime.set(new_s, new_us, rate) +end + +}