Преглед изворни кода

Improve SNTP module: Add list of servers and auto-sync [SNTP module only] (#1596)

Philip Gladstone пре 7 година
родитељ
комит
79013ae79a
2 измењених фајлова са 493 додато и 108 уклоњено
  1. 446 100
      app/modules/sntp.c
  2. 47 8
      docs/en/modules/sntp.md

+ 446 - 100
app/modules/sntp.c

@@ -47,6 +47,8 @@
 #include "rtc/rtctime.h"
 #endif
 
+#define max(a,b) ((a < b) ? b : a)
+
 #define NTP_PORT 123
 #define NTP_ANYCAST_ADDR(dst)  IP4_ADDR(dst, 224, 0, 1, 1)
 
@@ -58,6 +60,11 @@
 # define sntp_dbg(...)
 #endif
 
+#define US_TO_FRAC(us)          ((((uint64_t) (us)) << 32) / 1000000)
+#define SUS_TO_FRAC(us)         ((((int64_t) (us)) << 32) / 1000000)
+#define US_TO_FRAC16(us)        ((((uint64_t) (us)) << 16) / 1000000)
+#define FRAC16_TO_US(frac)      ((((uint64_t) (frac)) * 1000000) >> 16)
+
 typedef enum {
   NTP_NO_ERR = 0,
   NTP_DNS_ERR,
@@ -96,13 +103,47 @@ typedef struct
   os_timer_t timer;
   int sync_cb_ref;
   int err_cb_ref;
-  uint8_t attempts;
+  uint8_t attempts;    // Number of repeats of each entry
+  uint8_t server_index;   // index into server table
+  uint8_t lookup_pos;
+  bool is_on_timeout;
+  int server_pos;
+  int list_ref;
+  struct {
+    uint32_t delay_frac;
+    uint32_t root_maxerr;
+    uint32_t root_delay;
+    uint32_t root_dispersion;
+    uint8_t LI;
+    uint8_t stratum;
+    int when;
+    int64_t delta;
+    ip_addr_t server;
+  } best;
 } sntp_state_t;
 
-static sntp_state_t *state;
-static ip_addr_t server;
+typedef struct {
+  int32_t sync_cb_ref;
+  int32_t err_cb_ref;
+  os_timer_t timer;
+} sntp_repeat_t;
 
-static void on_timeout (void *arg);
+static sntp_state_t *state;
+static sntp_repeat_t *repeat;
+static ip_addr_t *serverp;
+static uint8_t server_count;
+static uint8_t using_offset;
+static uint8_t the_offset;
+static uint8_t pending_LI;
+static int32_t next_midnight;
+static uint64_t pll_increment;
+
+#define PLL_A   (1 << (32 - 11))
+#define PLL_B   (1 << (32 - 11 - 2))
+
+static void on_timeout(void *arg);
+static void on_long_timeout(void *arg);
+static void sntp_dolookups(lua_State *L);
 
 static void cleanup (lua_State *L)
 {
@@ -110,62 +151,194 @@ static void cleanup (lua_State *L)
   udp_remove (state->pcb);
   luaL_unref (L, LUA_REGISTRYINDEX, state->sync_cb_ref);
   luaL_unref (L, LUA_REGISTRYINDEX, state->err_cb_ref);
+  luaL_unref (L, LUA_REGISTRYINDEX, state->list_ref);
   os_free (state);
   state = 0;
 }
 
+static ip_addr_t* get_free_server() {
+  ip_addr_t* temp = (ip_addr_t *) c_malloc((server_count + 1) * sizeof(ip_addr_t));
+
+  if (server_count > 0) {
+    memcpy(temp, serverp, server_count * sizeof(ip_addr_t));
+  }
+  if (serverp) {
+    c_free(serverp);
+  }
+  serverp = temp;
+
+  return serverp + server_count;
+}
 
-static void handle_error (lua_State *L, ntp_err_t err)
+static void handle_error (lua_State *L, ntp_err_t err, const char *msg)
 {
   sntp_dbg("sntp: handle_error\n");
   if (state->err_cb_ref != LUA_NOREF)
   {
     lua_rawgeti (L, LUA_REGISTRYINDEX, state->err_cb_ref);
     lua_pushinteger (L, err);
+    lua_pushstring (L, msg);
     cleanup (L);
-    lua_call (L, 1, 0);
+    lua_call (L, 2, 0);
   }
   else
     cleanup (L);
 }
 
+#ifdef LUA_USE_MODULES_RTCTIME
+static void get_zero_base_timeofday(struct rtc_timeval *tv) {
+  uint32_t now = system_get_time();
+
+  tv->tv_sec = now / 1000000;
+  tv->tv_usec = now % 1000000;
+}
+#endif
+
+static void sntp_handle_result(lua_State *L) {
+  const uint32_t MICROSECONDS = 1000000;
+
+  if (state->best.stratum == 0) {
+    handle_error(L, NTP_TIMEOUT_ERR, NULL);
+    return;
+  }
+
+  bool have_cb = (state->sync_cb_ref != LUA_NOREF);
+
+  // if we have rtctime, do higher resolution delta calc, else just use
+  // the transmit timestamp
+#ifdef LUA_USE_MODULES_RTCTIME
+  struct rtc_timeval tv;
+  rtctime_gettimeofday (&tv);
+  if (tv.tv_sec == 0) {
+    get_zero_base_timeofday(&tv);
+  }
+  tv.tv_sec += (int)(state->best.delta >> 32);
+  tv.tv_usec += (int) ((MICROSECONDS * (state->best.delta & 0xffffffff)) >> 32);
+  while (tv.tv_usec >= 1000000) {
+    tv.tv_usec -= 1000000;
+    tv.tv_sec++;
+  }
+  if (state->is_on_timeout && state->best.delta > SUS_TO_FRAC(-200000) && state->best.delta < SUS_TO_FRAC(200000)) {
+    // Adjust rate
+    // f is frequency -- f should be 1 << 32 for nominal
+    sntp_dbg("delta=%d, increment=%d, ", (int32_t) state->best.delta, (int32_t) pll_increment);
+    int64_t f = ((state->best.delta * PLL_A) >> 32) + pll_increment;
+    pll_increment += (state->best.delta * PLL_B) >> 32;
+    sntp_dbg("f=%d, increment=%d\n", (int32_t) f, (int32_t) pll_increment);
+    //rtctime_adjust_rate((int32_t) f);
+  } else {
+    rtctime_settimeofday (&tv);
+  }
+#endif
+
+  if (have_cb)
+  {
+    lua_rawgeti(L, LUA_REGISTRYINDEX, state->sync_cb_ref);
+#ifdef LUA_USE_MODULES_RTCTIME
+    lua_pushnumber(L, tv.tv_sec);
+    lua_pushnumber(L, tv.tv_usec);
+    lua_pushstring(L, ipaddr_ntoa (&state->best.server));
+    lua_newtable(L);
+    int d40 = state->best.delta >> 40;
+    if (d40 != 0 && d40 != -1) {
+      lua_pushnumber(L, state->best.delta >> 32);
+      lua_setfield(L, -2, "offset_s");
+    } else {
+      lua_pushnumber(L, (state->best.delta * MICROSECONDS) >> 32);
+      lua_setfield(L, -2, "offset_us");
+    }
+#else
+    int adjust_us = system_get_time() - state->best.when;
+    int tv_sec = state->best.delta >> 32;
+    int tv_usec = (int) (((state->best.delta & 0xffffffff) * MICROSECONDS) >> 32) + adjust_us;
+    while (tv_usec >= 1000000) {
+      tv_usec -= 1000000;
+      tv_sec++;
+    }
+    lua_pushnumber(L, tv_sec);
+    lua_pushnumber(L, tv_usec);
+    lua_pushstring(L, ipaddr_ntoa (&state->best.server));
+    lua_newtable(L);
+#endif
+    if (state->best.delay_frac > 0) {
+      lua_pushnumber(L, FRAC16_TO_US(state->best.delay_frac));
+      lua_setfield(L, -2, "delay_us");
+    }
+    lua_pushnumber(L, FRAC16_TO_US(state->best.root_delay));
+    lua_setfield(L, -2, "root_delay_us");
+    lua_pushnumber(L, FRAC16_TO_US(state->best.root_dispersion));
+    lua_setfield(L, -2, "root_dispersion_us");
+    lua_pushnumber(L, FRAC16_TO_US(state->best.root_maxerr + state->best.delay_frac / 2));
+    lua_setfield(L, -2, "root_maxerr_us");
+    lua_pushnumber(L, state->best.stratum);
+    lua_setfield(L, -2, "stratum");
+    lua_pushnumber(L, state->best.LI);
+    lua_setfield(L, -2, "leap");
+    lua_pushnumber(L, pending_LI);
+    lua_setfield(L, -2, "pending_leap");
+  }
+
+  cleanup (L);
+
+  if (have_cb)
+  {
+    lua_call (L, 4, 0);
+  }
+}
+
 
 static void sntp_dosend (lua_State *L)
 {
-  if (state->attempts == 0)
-  {
-    os_timer_disarm (&state->timer);
-    os_timer_setfn (&state->timer, on_timeout, NULL);
-    os_timer_arm (&state->timer, 1000, 1);
+  if (state->server_pos < 0) {
+    os_timer_disarm(&state->timer);
+    os_timer_setfn(&state->timer, on_timeout, NULL);
+    state->server_pos = 0;
+  } else {
+    ++state->server_pos;
+  }
+
+  if (state->server_pos >= server_count) {
+    state->server_pos = 0;
+    ++state->attempts;
+  }
+
+  if (state->attempts >= MAX_ATTEMPTS || state->attempts * server_count > 10) {
+    sntp_handle_result(L);
+    return;
   }
 
-  ++state->attempts;
-  sntp_dbg("sntp: attempt %d\n", state->attempts);
+  sntp_dbg("sntp: server %s (%d), attempt %d\n", ipaddr_ntoa(serverp + state->server_pos), state->server_pos, state->attempts);
 
   struct pbuf *p = pbuf_alloc (PBUF_TRANSPORT, sizeof (ntp_frame_t), PBUF_RAM);
   if (!p)
-    handle_error (L, NTP_MEM_ERR);
+    handle_error (L, NTP_MEM_ERR, NULL);
 
   ntp_frame_t req;
   os_memset (&req, 0, sizeof (req));
   req.ver = 4;
   req.mode = 3; // client
 #ifdef LUA_USE_MODULES_RTCTIME
+  const uint32_t NTP_TO_UNIX_EPOCH = 2208988800ul;
   struct rtc_timeval tv;
   rtctime_gettimeofday (&tv);
-  req.xmit.sec = htonl (tv.tv_sec);
-  req.xmit.frac = htonl (tv.tv_usec);
+  if (tv.tv_sec == 0) {
+    get_zero_base_timeofday(&tv);
+  }
+  req.xmit.sec = htonl (tv.tv_sec - the_offset + NTP_TO_UNIX_EPOCH);
+  req.xmit.frac = htonl (US_TO_FRAC(tv.tv_usec));
 #else
   req.xmit.frac = htonl (system_get_time ());
 #endif
   state->cookie = req.xmit;
 
   os_memcpy (p->payload, &req, sizeof (req));
-  int ret = udp_sendto (state->pcb, p, &server, NTP_PORT);
+  int ret = udp_sendto (state->pcb, p, serverp + state->server_pos, NTP_PORT);
   sntp_dbg("sntp: send: %d\n", ret);
   pbuf_free (p);
   if (ret != ERR_OK)
-    handle_error (L, NTP_SEND_ERR);
+    handle_error (L, NTP_SEND_ERR, NULL);
+
+  os_timer_arm (&state->timer, 1000, 0);
 }
 
 
@@ -177,12 +350,13 @@ static void sntp_dns_found(const char *name, ip_addr_t *ipaddr, void *arg)
   if (ipaddr == NULL)
   {
     sntp_dbg("DNS Fail!\n");
-    handle_error(L, NTP_DNS_ERR);
+    handle_error(L, NTP_DNS_ERR, name);
   }
   else
   {
-    server = *ipaddr;
-    sntp_dosend(L);
+    serverp[server_count] = *ipaddr;
+    server_count++;
+    sntp_dolookups(L);
   }
 }
 
@@ -192,16 +366,75 @@ static void on_timeout (void *arg)
   (void)arg;
   sntp_dbg("sntp: timer\n");
   lua_State *L = lua_getstate ();
-  if (state->attempts >= MAX_ATTEMPTS)
-    handle_error (L, NTP_TIMEOUT_ERR);
-  else
-    sntp_dosend (L);
+  sntp_dosend (L);
 }
 
+static void update_offset()
+{
+  // This may insert or remove an offset second -- i.e. a leap second
+  // This can only happen if it is at midnight UTC.
+#ifdef LUA_USE_MODULES_RTCTIME
+  struct rtc_timeval tv;
+
+  if (pending_LI && using_offset) {
+    rtctime_gettimeofday (&tv);
+    if (tv.tv_sec - the_offset >= next_midnight) {
+      next_midnight = tv.tv_sec + 86400 - the_offset - (tv.tv_sec - the_offset) % 86400;
+      // is this the first day of the month
+      int day = (tv.tv_sec - the_offset) / 86400 + 1975 * 365 + 1970 / 4 - 74;
+
+      int century = (4 * day + 3) / 146097;
+      day = day - century * 146097 / 4;
+      int year = (4 * day + 3) / 1461;
+      day = day - year * 1461 / 4;
+      int month = (5 * day + 2) / 153;
+      day = day - (153 * month + 2) / 5;
+
+      if (day == 0) {
+        if (pending_LI == 1) {
+          the_offset ++;
+        } else {
+          the_offset --;
+        }
+      }
+      pending_LI = 0;
+    }
+  }
+#endif
+}
+
+static void record_result(ip_addr_t *addr, int64_t delta, int stratum, int LI, uint32_t delay_frac, uint32_t root_maxerr, uint32_t root_dispersion, uint32_t root_delay) {
+  sntp_dbg("Recording %s: delta=%08x.%08x, stratum=%d, li=%d, delay=%dus, root_maxerr=%dus",
+      ipaddr_ntoa(addr), (uint32_t) (delta >> 32), (uint32_t) (delta & 0xffffffff), stratum, LI, (int32_t) FRAC16_TO_US(delay_frac), (int32_t) FRAC16_TO_US(root_maxerr));
+  // I want to favor close by servers as they probably have a more consistent clock,
+  if (!state->best.stratum || root_delay * 2 + delay_frac < state->best.root_delay * 2 + state->best.delay_frac) {
+    sntp_dbg("   --BEST\n");
+    state->best.server = *addr;
+    state->best.delay_frac = delay_frac;
+    state->best.root_maxerr = root_maxerr;
+    state->best.root_dispersion = root_dispersion;
+    state->best.root_delay = root_delay;
+    state->best.delta = delta;
+    state->best.stratum = stratum;
+    state->best.LI = LI;
+    state->best.when = system_get_time();
+  } else {
+    sntp_dbg("\n");
+  }
+}
 
 static void on_recv (void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *addr, uint16_t port)
 {
   (void)port;
+#ifdef LUA_USE_MODULES_RTCTIME
+  // Ideally this would be done when we receive the packet....
+  struct rtc_timeval tv;
+
+  rtctime_gettimeofday (&tv);
+  if (tv.tv_sec == 0) {
+    get_zero_base_timeofday(&tv);
+  }
+#endif
   sntp_dbg("sntp: on_recv\n");
 
   lua_State *L = lua_getstate();
@@ -232,7 +465,7 @@ static void on_recv (void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_a
   // sanity checks before we touch our clocks
   ip_addr_t anycast;
   NTP_ANYCAST_ADDR(&anycast);
-  if (server.addr != anycast.addr && server.addr != addr->addr)
+  if (serverp[state->server_pos].addr != anycast.addr && serverp[state->server_pos].addr != addr->addr)
     return; // unknown sender, ignore
 
   if (ntp.origin.sec  != state->cookie.sec ||
@@ -242,7 +475,14 @@ static void on_recv (void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_a
   if (ntp.LI == 3)
     return; // server clock not synchronized (why did it even respond?!)
 
-  server.addr = addr->addr;
+  os_timer_disarm(&state->timer);
+
+  if (ntp.LI) {
+    pending_LI = ntp.LI;
+  }
+
+  update_offset();
+
   ntp.origin.sec  = ntohl (ntp.origin.sec);
   ntp.origin.frac = ntohl (ntp.origin.frac);
   ntp.recv.sec  = ntohl (ntp.recv.sec);
@@ -250,135 +490,237 @@ static void on_recv (void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_a
   ntp.xmit.sec  = ntohl (ntp.xmit.sec);
   ntp.xmit.frac = ntohl (ntp.xmit.frac);
 
-  const uint32_t UINT32_MAXI = (uint32_t)-1;
   const uint64_t MICROSECONDS = 1000000ull;
   const uint32_t NTP_TO_UNIX_EPOCH = 2208988800ul;
 
-  bool have_cb = (state->sync_cb_ref != LUA_NOREF);
+  uint32_t root_maxerr = ntohl(ntp.root_dispersion) + ntohl(ntp.root_delay) / 2;
 
   // if we have rtctime, do higher resolution delta calc, else just use
   // the transmit timestamp
 #ifdef LUA_USE_MODULES_RTCTIME
-  struct rtc_timeval tv;
-
-  rtctime_gettimeofday (&tv);
   ntp_timestamp_t dest;
-  dest.sec = tv.tv_sec;
-  dest.frac = (MICROSECONDS * tv.tv_usec) / UINT32_MAXI;
+  dest.sec = tv.tv_sec + NTP_TO_UNIX_EPOCH - the_offset;
+  dest.frac = US_TO_FRAC(tv.tv_usec);
 
-  // Compensation as per RFC2030
-  int64_t delta_s = (((int64_t)ntp.recv.sec - ntp.origin.sec) +
-                     ((int64_t)ntp.xmit.sec - dest.sec)) / 2;
+  uint64_t ntp_recv = (((uint64_t) ntp.recv.sec) << 32) + (uint64_t) ntp.recv.frac;
+  uint64_t ntp_origin = (((uint64_t) ntp.origin.sec) << 32) + (uint64_t) ntp.origin.frac;
+  uint64_t ntp_xmit = (((uint64_t) ntp.xmit.sec) << 32) + (uint64_t) ntp.xmit.frac;
+  uint64_t ntp_dest = (((uint64_t) dest.sec) << 32) + (uint64_t) dest.frac;
 
-  int64_t delta_f = (((int64_t)ntp.recv.frac - ntp.origin.frac) +
-                     ((int64_t)ntp.xmit.frac - dest.frac)) / 2;
-
-  dest.sec += delta_s;
-  if (delta_f + dest.frac < 0)
-  {
-    delta_f += UINT32_MAXI;
-    --dest.sec;
-  }
-  else if (delta_f + dest.frac > UINT32_MAXI)
-  {
-    delta_f -= UINT32_MAXI;
-    ++dest.sec;
-  }
-  dest.frac += delta_f;
+  // Compensation as per RFC2030
+  int64_t delta = (int64_t) (ntp_recv - ntp_origin) / 2 + (int64_t) (ntp_xmit - ntp_dest) / 2;
 
-  tv.tv_sec = dest.sec - NTP_TO_UNIX_EPOCH;
-  tv.tv_usec = (MICROSECONDS * dest.frac) / UINT32_MAXI;
-  rtctime_settimeofday (&tv);
+  record_result(addr, delta, ntp.stratum, ntp.LI, ((int64_t)(ntp_dest - ntp_origin - (ntp_xmit - ntp_recv))) >> 16, root_maxerr, ntohl(ntp.root_dispersion), ntohl(ntp.root_delay));
 
-  if (have_cb)
-  {
-    lua_rawgeti (L, LUA_REGISTRYINDEX, state->sync_cb_ref);
-    lua_pushnumber (L, tv.tv_sec);
-    lua_pushnumber (L, tv.tv_usec);
-  }
 #else
-  if (have_cb)
-  {
-    lua_rawgeti (L, LUA_REGISTRYINDEX, state->sync_cb_ref);
-    lua_pushnumber (L, ntp.xmit.sec - NTP_TO_UNIX_EPOCH);
-    lua_pushnumber (L, (MICROSECONDS * ntp.xmit.frac) / UINT32_MAXI);
-  }
+  uint64_t ntp_xmit = (((uint64_t) ntp.xmit.sec - NTP_TO_UNIX_EPOCH) << 32) + (uint64_t) ntp.xmit.frac;
+  record_result(addr, ntp_xmit, ntp.stratum, ntp.LI, (((int64_t) (system_get_time() - ntp.origin.frac)) << 16) / MICROSECONDS, root_maxerr, ntohl(ntp.root_dispersion), ntohl(ntp.root_delay));
 #endif
 
-  cleanup (L);
+  sntp_dosend(L);
+}
 
-  if (have_cb)
-  {
-    lua_pushstring (L, ipaddr_ntoa (&server));
-    lua_call (L, 3, 0);
+#ifdef LUA_USE_MODULES_RTCTIME
+static int sntp_setoffset(lua_State *L)
+{
+  the_offset = luaL_checkinteger(L, 1);
+  if (!using_offset) {
+    struct rtc_timeval tv;
+    rtctime_gettimeofday (&tv);
+    next_midnight = tv.tv_sec + 86400 - the_offset - (tv.tv_sec - the_offset) % 86400;
   }
-}
+  using_offset = 1;
 
+  return 0;
+}
 
-// sntp.sync (server or nil, syncfn or nil, errfn or nil)
-static int sntp_sync (lua_State *L)
+static int sntp_getoffset(lua_State *L)
 {
-  // default to anycast address, then allow last server to stick
-  if (server.addr == IPADDR_ANY)
-    NTP_ANYCAST_ADDR(&server);
+  update_offset();
+  lua_pushnumber(L, the_offset);
 
-  const char *errmsg = 0;
-  #define sync_err(x) do { errmsg = x; goto error; } while (0)
+  return 1;
+}
+#endif
 
-  if (state)
-    return luaL_error (L, "sync in progress");
+static void sntp_dolookups (lua_State *L) {
+  // Step through each element of the table, converting it to an address
+  // at the end, start the lookups
+  //
+  if (state->list_ref == LUA_NOREF) {
+    sntp_dosend(L);
+  }
+
+  lua_rawgeti(L, LUA_REGISTRYINDEX, state->list_ref);
+  while (1) {
+    if (lua_objlen(L, -1) <= state->lookup_pos) {
+      // We reached the end
+      sntp_dosend(L);
+      break;
+    }
 
+    state->lookup_pos++;
+
+    lua_rawgeti(L, -1, state->lookup_pos);
+
+    int l;
+
+    const char *hostname = luaL_checklstring(L, -1, &l);
+    lua_pop(L, 1);
+
+    if (l>128 || hostname == NULL) {
+      handle_error(L, NTP_DNS_ERR, hostname);
+      break;
+    }
+    err_t err = dns_gethostbyname(hostname, get_free_server(), sntp_dns_found, state);
+    if (err == ERR_INPROGRESS)
+      break;  // Callback function sntp_dns_found will handle sntp_dosend for us
+    else if (err == ERR_ARG) {
+      handle_error(L, NTP_DNS_ERR, hostname);
+      break;
+    }
+    server_count++;
+  }
+  lua_pop(L, 1);
+}
+
+static char *state_init(lua_State *L) {
   state = (sntp_state_t *)c_malloc (sizeof (sntp_state_t));
   if (!state)
-    sync_err ("out of memory");
+    return ("out of memory");
 
   memset (state, 0, sizeof (sntp_state_t));
 
   state->sync_cb_ref = LUA_NOREF;
   state->err_cb_ref = LUA_NOREF;
+  state->list_ref = LUA_NOREF;
 
   state->pcb = udp_new ();
   if (!state->pcb)
-    sync_err ("out of memory");
+    return ("out of memory");
 
   if (udp_bind (state->pcb, IP_ADDR_ANY, 0) != ERR_OK)
-    sync_err ("no port available");
+    return ("no port available");
 
   udp_recv (state->pcb, on_recv, L);
 
+  state->server_pos = -1;
+
+  return NULL;
+}
+
+static char *set_repeat_mode(lua_State *L, bool enable)
+{
+  if (enable) {
+    set_repeat_mode(L, FALSE);
+    repeat = (sntp_repeat_t *) c_malloc(sizeof(sntp_repeat_t));
+    if (!repeat) {
+      return "no memory";
+    }
+    memset(repeat, 0, sizeof(repeat));
+    lua_rawgeti(L, LUA_REGISTRYINDEX, state->sync_cb_ref);
+    repeat->sync_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+    lua_rawgeti(L, LUA_REGISTRYINDEX, state->err_cb_ref);
+    repeat->err_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+    os_timer_setfn(&repeat->timer, on_long_timeout, NULL);
+    os_timer_arm(&repeat->timer, 1000 * 1000, 1);
+  } else {
+    if (repeat) {
+      os_timer_disarm (&repeat->timer);
+      luaL_unref (L, LUA_REGISTRYINDEX, repeat->sync_cb_ref);
+      luaL_unref (L, LUA_REGISTRYINDEX, repeat->err_cb_ref);
+      c_free(repeat);
+      repeat = NULL;
+    }
+  }
+  return NULL;
+}
+
+static void on_long_timeout (void *arg)
+{
+  (void)arg;
+  sntp_dbg("sntp: long timer\n");
+  lua_State *L = lua_getstate ();
+  if (!state) {
+    if (!state_init(L)) {
+      // Good.
+      lua_rawgeti(L, LUA_REGISTRYINDEX, repeat->sync_cb_ref);
+      state->sync_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+      lua_rawgeti(L, LUA_REGISTRYINDEX, repeat->err_cb_ref);
+      state->err_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+      state->is_on_timeout = 1;
+      sntp_dosend (L);
+    }
+  }
+}
+
+// sntp.sync (server or nil, syncfn or nil, errfn or nil)
+static int sntp_sync (lua_State *L)
+{
+  // default to anycast address, then allow last server to stick
+  if (server_count == 0) {
+    NTP_ANYCAST_ADDR(get_free_server());
+    server_count++;
+  }
+
+  set_repeat_mode(L, 0);
+
+  const char *errmsg = 0;
+#define sync_err(x) do { errmsg = x; goto error; } while (0)
+
+  if (state)
+    return luaL_error (L, "sync in progress");
+
+  char *state_err;
+  state_err = state_init(L);
+  if (state_err) {
+    sync_err(state_err);
+  }
+
   if (!lua_isnoneornil (L, 2))
   {
     lua_pushvalue (L, 2);
     state->sync_cb_ref = luaL_ref (L, LUA_REGISTRYINDEX);
   }
-  else
-    state->sync_cb_ref = LUA_NOREF;
 
   if (!lua_isnoneornil (L, 3))
   {
     lua_pushvalue (L, 3);
     state->err_cb_ref = luaL_ref (L, LUA_REGISTRYINDEX);
   }
-  else
-    state->err_cb_ref = LUA_NOREF;
-
-  state->attempts = 0;
 
   // use last server, unless new one specified
   if (!lua_isnoneornil (L, 1))
   {
-    size_t l;
-    const char *hostname = luaL_checklstring(L, 1, &l);
-    if (l>128 || hostname == NULL)
-      sync_err("need <128 hostname");
-    err_t err = dns_gethostbyname(hostname, &server, sntp_dns_found, state);
-    if (err == ERR_INPROGRESS)
-      return 0;  // Callback function sntp_dns_found will handle sntp_dosend for us
-    else if (err == ERR_ARG)
-      sync_err("bad hostname");
+    server_count = 0;
+    if (lua_istable(L, 1)) {
+      // Save a reference to the table
+      lua_pushvalue(L, 1);
+      state->list_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+      sntp_dolookups(L);
+      goto good_ret;
+    } else {
+      size_t l;
+      const char *hostname = luaL_checklstring(L, 1, &l);
+      if (l>128 || hostname == NULL)
+        sync_err("need <128 hostname");
+      err_t err = dns_gethostbyname(hostname, get_free_server(), sntp_dns_found, state);
+      if (err == ERR_INPROGRESS) {
+        goto good_ret;
+      } else if (err == ERR_ARG)
+        sync_err("bad hostname");
+
+      server_count++;
+    }
   }
 
   sntp_dosend (L);
+
+good_ret:
+  if (!lua_isnoneornil(L, 4)) {
+    set_repeat_mode(L, 1);
+  }
+
   return 0;
 
 error:
@@ -396,6 +738,10 @@ error:
 // Module function map
 static const LUA_REG_TYPE sntp_map[] = {
   { LSTRKEY("sync"),  LFUNCVAL(sntp_sync)  },
+#ifdef LUA_USE_MODULES_RTCTIME
+  { LSTRKEY("setoffset"),  LFUNCVAL(sntp_setoffset)  },
+  { LSTRKEY("getoffset"),  LFUNCVAL(sntp_getoffset)  },
+#endif
   { LNILKEY, LNILVAL }
 };
 

+ 47 - 8
docs/en/modules/sntp.md

@@ -13,31 +13,44 @@ When compiled together with the [rtctime](rtctime.md) module it also offers seam
 Attempts to obtain time synchronization. 
 
 For best results you may want to to call this periodically in order to compensate for internal clock drift. As stated in the [rtctime](rtctime.md) module documentation it's advisable to sync time after deep sleep and it's necessary to sync after module reset (add it to [`init.lua`](../upload.md#initlua) after WiFi initialization).
+Note that either a single server can be provided as an argument (name or address), or a list (table) of servers can be provided. 
 
 #### Syntax
-`sntp.sync([server_ip], [callback], [errcallback])`
+`sntp.sync([server_ip], [callback], [errcallback], [autorepeat])`
+`sntp.sync({ server1, server2, .. }, [callback], [errcallback], [autorepeat])`
 
 #### Parameters
 - `server_ip` if non-`nil`, that server is used. If `nil`, then the last contacted server is used. This ties in with the NTP anycast mode, where the first responding server is remembered for future synchronization requests. The easiest way to use anycast is to always pass nil for the server argument.
-- `callback` if provided it will be invoked on a successful synchronization, with three parameters: seconds, microseconds, and server. Note that when the [rtctime](rtctime.md) module is available, there is no need to explicitly call [`rtctime.set()`](rtctime.md#rtctimeset) - this module takes care of doing so internally automatically, for best accuracy.
-- `errcallback` failure callback with a single integer parameter describing the type of error. The module automatically performs a number of retries before giving up and reporting the error. Error codes:
-  - 1: DNS lookup failed
+- `server1`, `server2` these are either the ip address or dns name of one or more servers to try.
+- `callback` if provided it will be invoked on a successful synchronization, with four parameters: seconds, microseconds, server and info. Note that when the [rtctime](rtctime.md) module is available, there is no need to explicitly call [`rtctime.set()`](rtctime.md#rtctimeset) - this module takes care of doing so internally automatically, for best accuracy. The info parameter is a table of (semi) interesting values. These are described below.
+- `errcallback` failure callback with two parameters. The first is an integer describing the type of error. The module automatically performs a number of retries before giving up and reporting the error. The second is a string containing supplementary information (if any). Error codes:
+  - 1: DNS lookup failed (the second parameter is the failing DNS name)
   - 2: Memory allocation failure
   - 3: UDP send failed
   - 4: Timeout, no NTP response received
+- `autorepeat` if this is non-nil, then the synchronization will happen every 1000 seconds and try and condition the clock if possible. The callbacks will be called after each sync operation.
 
 #### Returns
 `nil`
 
+#### Info table
+This is passed to the success callback and contains useful information about the time synch that just completed. The keys in this table are:
+
+- `offset_s` This is an optional field and contains the number of seconds that the clock was adjusted. This is only present for large (many second) adjustments. Typically, this is only present on the initial sync call.
+- `offset_us` This is an optional field (but one of `offset_s` and `offset_us` will always be present). This contains the number of microseconds that the clock was adjusted. 
+- `delay_us` This is the round trip delay to the server in microseconds. Thie setting uncertainty is somewhat less than this value.
+- `stratum` This is the stratum of the server. 
+- `leap` This contains the leap bits from the NTP protocol. 0 means that no leap second is pending, 1 is a pending extra leap second at the end of the UTC month, and 2 is a pending leap second removal at the end of the UTC month.
+
 #### Example
 ```lua
--- Best effort, use the last known NTP server (or the NTP "anycast" address 224.0.1.1 initially)
+-- Best effort, use the last known NTP server(s) (or the NTP "anycast" address 224.0.1.1 initially)
 sntp.sync()
 ```
 ```lua
--- Sync time with 192.168.0.1 and print the result, or that it failed
-sntp.sync('192.168.0.1',
-  function(sec,usec,server)
+-- Sync time with some servers from the NTP pool and print the result, or that it failed
+sntp.sync({ '1.pool.ntp.org', '2.pool.ntp.org', '3.pool.ntp.org' },
+  function(sec, usec, server, info)
     print('sync', sec, usec, server)
   end,
   function()
@@ -47,3 +60,29 @@ sntp.sync('192.168.0.1',
 ```
 #### See also
 [`rtctime.set()`](rtctime.md#rtctimeset)
+
+## sntp.setoffset
+
+Sets the offset between the rtc clock and the NTP time. Note that NTP time has leap seconds in it and hence it runs slow when a leap second is 
+inserted. The `setoffset` call enables explicit leap second tracking and causes the rtc clock to tick more evenly -- but it gets out of step
+with wall clock time. The number of seconds is the offset.
+
+#### Syntax
+`sntp.setoffset([offset])`
+
+#### Parameters
+- `offset` The offset between NTP time and the rtc time. This can be omitted, and defaults to zero. This call enables the offset tracking.
+
+#### Returns
+nil
+
+## sntp.getoffset
+
+Gets the offset between the rtc clock and the NTP time. This value should be subtracted from the rtc time to get the NTP time -- which
+corresponds to wall clock time. If the offset returned has changed from the pervious call, then there has been a leap second inbetween.
+
+#### Syntax
+`offset = sntp.getoffset()`
+
+#### Returns
+The current offset.