nodemcu-firmware/app/modules/sntp.c

403 lines
9.7 KiB
C
Raw Normal View History

/*
* 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 <jmattsson@dius.com.au>
*/
// Module for Simple Network Time Protocol (SNTP)
#include "module.h"
#include "lauxlib.h"
#include "os_type.h"
#include "osapi.h"
#include "lwip/udp.h"
#include "c_stdlib.h"
#include "user_modules.h"
2015-11-19 21:51:33 +01:00
#include "lwip/dns.h"
#include "user_interface.h"
#ifdef LUA_USE_MODULES_RTCTIME
#include "rtc/rtctime.h"
#endif
#define NTP_PORT 123
#define NTP_ANYCAST_ADDR(dst) IP4_ADDR(dst, 224, 0, 1, 1)
#define MAX_ATTEMPTS 5
#if 0
Next 1.5.4.1 master drop (#1627) * add u8g.fb_rle display * move comm drivers to u8g_glue.c * disable fb_rle per default * implement file.size for spiffs (#1516) Another bug squashed! * Fix start-up race between UART & start_lua. (#1522) Input during startup (especially while doing initial filesystem format) ran the risk of filling up the task queue, preventing the start_lua task from being queued, and hence NodeMCU would not start up that time. * Reimplemented esp_init_data_default. To work around the pesky "rf_cal[0] !=0x05" hang when booting on a chip which doesn't have esp_init_data written to it. It is no longer possible to do the writing of the esp_init_data_default from within nodemcu_init(), as the SDK now hangs long before it gets there. As such, I've had to reimplement this in our user_start_trampoline and get it all done before the SDK has a chance to look for the init data. It's unfortunate that we have to spend IRAM on this, but I see no better alternative at this point. * Replace hardcoded init data with generated data from SDK The esp_init_data_default.bin is now extracted from the SDK (and its patch file, if present), and the contents are automatically embedded into user_main.o. * Rework flashing instructions Clarifies issues around SDK init data and hopefully clears up some confusion, when paired with the esp_init_data_default changes in NodeMCU. * Fix typo * Fixes the gpio.serout problem from #1534 (#1535) * Fix some issues in gpio.serout * Minor cleanup * fix dereferencing NULL pointer in vfs_errno() (#1539) * add map ids for flash sizes 32m-c2, 64m, 128m in user_rf_cal_sector_set() (#1529) * Somfy/TELIS driver (#1521) * Reduced LUAL_BUFFERSIZE to 256. Should free up some stack (#1530) * avoid task queue overrun for serial input (#1540) Thank you. * Increase irom0_0_seg size for PR build * Improve reliability of FS detection. (#1528) * Version to make filesystem detection more reliable * Improve bad fs detection * Version of printf that doesn't suffer from buffer overflows (#1564) * Small improvement to http client (#1558) * Remove luaL_buffer from file_g_read() (#1541) * remove luaL_buffer from file_g_read() - avoid memory leak when function gets terminated by lua_error - skip scanning for end_char when reading until EOF * attempt to free memory in any case * Change HTTP failures from debug to error messages (#1568) * Change HTTP failures from debug to error messages * Add tag to HTTP error messages * Create macro for error msg and improve dbg msg * Add ssd1306_128x32 for U8G (#1571) * Update CONTRIBUTING.md * Add support to mix ws2812.buffer objects. (#1575) * Add load/dump/mix/power operations on the buffer object * Calculate the pixel value in mix and then clip to the range. * Fixed the two wrong userdata types * Added a couple more useful methods * Add support for shifting a piece of the buffer. * Fix a minor bug with offset shifts * Update to the wifi module (#1497) * Removed inline documentation for several functions and update comments Since documentation is now part of the repository, the inline documentation just adds to the already huge wifi.c * Wifi module: add new functionality, update documentation Functions Added: wifi.getdefaultmode(): returns default wifi opmode wifi.sta.apchange(): select alternate cached AP wifi.sta.apinfo(): get cached AP list wifi.sta.aplimit(): set cached AP limit wifi.sta.getapindex(): get index of currently configured AP wifi.sta.getdefaultconfig(): get default station configuration wifi.ap.getdefaultconfig(): get default AP configuration functions modified: wifi.setmode: saving mode to flash is now optional wifi.sta.config: now accepts table as an argument and save config to flash is now optional wifi.sta.getconfig: added option to return table wifi.ap.config: save config to flash is now optional wifi.ap.getconfig: added option to return table Documentation changes: - Modified documentation to reflect above changes - Removed unnecessary inline documentation from `wifi.c` - Updated documentation for `wifi.sta.disconnect`to address issue #1480 - Fixed inaccurate documentation for function `wifi.sleeptype` - Added more details to `wifi.nullmodesleep()` * Move function `wifi.sleeptype()` to `wifi.sta.sleeptype()` * Fixed problem where wifi.x.getconfig() returned invalid strings when ssid or password were set to maximum length. * fix error in documentation for `wifi.sta.getapindex` * Renamed some wifi functions wifi.sta.apinfo -> getapinfo wifi.sta.aplimit -> setaplimit wifi.sta.apchange -> changeap also organized the wifi_station_map array * Make the MQTT PING functionality work better. (#1557) Deal with flow control stopped case * Implement object model for files (#1532) * Eus channelfix (#1583) Squashed commits included: Bug fixes and final implementation - Added Content-Length: 0 to all headers - Endpoint name checks not using trailing space so cache-busting techniques can be used (i.e., append a nonce to the URL) - Track when connecting so APList scan doesn't take place during (which changes the channel) - More debugging output added to assist in tracking down some issues Added /status.json endpoint for phone apps/XHR to get JSON response Station Status caching for wifi channel workaround + AJAX/CORS - During checkstation poll, cache the last station status - Shut down the station if status = 2,3,4 and channel is different than SoftAP - Add Access-Control-Allow-Origin: * to endpoint responses used by a service - Add a /setwifi GET endpoint for phone apps/XHR to use (same parameters as /update endpoint). Returns a JSON response containing chip id and status code. - Add handler for OPTIONS verb (needed for CORS support) Wi-Fi Channel Issue Workaround - Do a site survey upon startup, set SoftAP channel to the strongest rssi's channel - Compare successful station connect channel to SoftAP's. If different, then defer the Lua success callback to the end. Shut down Station and start the SoftAP back up with original channel. - After the 10 second shutdown timer fires, check to see if success callback was already called. If not, then call it while starting the Station back up. HTTP Response and DNS enhancements - If DNS's UDP buffer fills up, keep going as non-fatal. It's UDP and not guaranteed anyways. I've seen this occur when connecting a PC to the SoftAP and every open program tries to phone home at the same time, overwhelming the EUS DNS server. - Support for detecting/handling pre-gzipped `enduser_setup.html` (and `http_html_backup`) payload. Nice for keeping the size of the `state->http_payload_data` as small as possible (also makes minimization not as critical) - Corrected misuse of HTTP 401 response status (changed one occurrence to 400/Bad Request, and changed another to 405/Method Not Allowed) * Normalized formatting (tabs-to-spaces) * Added documentation * Corrected misuse of strlen for binary (gzip) data. * Added NULL check after malloc * fix vfs_lseek() result checking in enduser_setup and clarify SPIFFS_lseek() return value (#1570) * Fix link * Overhaul flashing docs once again (#1587) * Add chapter about determine flash size plus small fixes * Rewrite esptool.py chapter, move flash size chapter to end * i2c - allow slave stretching SCL (just loop and check) (#1589) * Add note on dev board usage of SPI bus 0 (#1591) * Turn SPI busses note to admonition note * support for custom websocket headers (#1573) Looks good to me. Thank you. Also: - allow for '\0's in received messages * add client:config for setting websocket headers Also: - headers are case-insensitive now * fix docs * fix typo * remove unnecessary luaL_argcheck calls * replace os_sprintf with simple string copy * Handle error condition in file.read() (#1599) * handle error condition in file.read() * simplify loop initialization * Fix macro as suggested in #1548 * Extract and hoist net receive callbacks This is done to avoid the accidental upval binding * Fix typo at rtctime.md rtctime.dsleep -> rtctime.dsleep_aligned
2016-12-01 21:37:24 +01:00
# define sntp_dbg(...) dbg_printf(__VA_ARGS__)
#else
# define sntp_dbg(...)
#endif
typedef enum {
NTP_NO_ERR = 0,
NTP_DNS_ERR,
NTP_MEM_ERR,
NTP_SEND_ERR,
NTP_TIMEOUT_ERR
} ntp_err_t;
typedef struct
{
uint32_t sec;
uint32_t frac;
} ntp_timestamp_t;
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 root_delay;
uint32_t root_dispersion;
uint32_t refid;
ntp_timestamp_t ref;
ntp_timestamp_t origin;
ntp_timestamp_t recv;
ntp_timestamp_t xmit;
} ntp_frame_t;
typedef struct
{
struct udp_pcb *pcb;
ntp_timestamp_t cookie;
os_timer_t timer;
int sync_cb_ref;
int err_cb_ref;
uint8_t attempts;
} sntp_state_t;
static sntp_state_t *state;
static ip_addr_t server;
2016-03-07 02:38:45 +01:00
static void on_timeout (void *arg);
static void cleanup (lua_State *L)
{
os_timer_disarm (&state->timer);
udp_remove (state->pcb);
luaL_unref (L, LUA_REGISTRYINDEX, state->sync_cb_ref);
luaL_unref (L, LUA_REGISTRYINDEX, state->err_cb_ref);
os_free (state);
state = 0;
}
static void handle_error (lua_State *L, ntp_err_t err)
{
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);
cleanup (L);
lua_call (L, 1, 0);
}
else
cleanup (L);
}
static void sntp_dosend (lua_State *L)
{
2016-03-07 02:38:45 +01:00
if (state->attempts == 0)
{
os_timer_disarm (&state->timer);
os_timer_setfn (&state->timer, on_timeout, NULL);
os_timer_arm (&state->timer, 1000, 1);
}
++state->attempts;
sntp_dbg("sntp: attempt %d\n", state->attempts);
struct pbuf *p = pbuf_alloc (PBUF_TRANSPORT, sizeof (ntp_frame_t), PBUF_RAM);
if (!p)
handle_error (L, NTP_MEM_ERR);
ntp_frame_t req;
os_memset (&req, 0, sizeof (req));
req.ver = 4;
req.mode = 3; // client
#ifdef LUA_USE_MODULES_RTCTIME
struct rtc_timeval tv;
2015-07-06 05:45:35 +02:00
rtctime_gettimeofday (&tv);
req.xmit.sec = htonl (tv.tv_sec);
req.xmit.frac = htonl (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);
sntp_dbg("sntp: send: %d\n", ret);
pbuf_free (p);
if (ret != ERR_OK)
handle_error (L, NTP_SEND_ERR);
}
2015-11-19 21:51:33 +01:00
static void sntp_dns_found(const char *name, ip_addr_t *ipaddr, void *arg)
{
2016-03-07 02:38:45 +01:00
(void)arg;
lua_State *L = lua_getstate ();
2015-11-19 21:51:33 +01:00
if (ipaddr == NULL)
{
sntp_dbg("DNS Fail!\n");
handle_error(L, NTP_DNS_ERR);
2015-11-19 21:51:33 +01:00
}
else
{
server = *ipaddr;
sntp_dosend(L);
}
}
static void on_timeout (void *arg)
{
2016-03-07 02:38:45 +01:00
(void)arg;
sntp_dbg("sntp: timer\n");
2016-03-07 02:38:45 +01:00
lua_State *L = lua_getstate ();
if (state->attempts >= MAX_ATTEMPTS)
handle_error (L, NTP_TIMEOUT_ERR);
else
sntp_dosend (L);
}
static void on_recv (void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *addr, uint16_t port)
{
(void)port;
sntp_dbg("sntp: on_recv\n");
lua_State *L = lua_getstate();
if (!state || state->pcb != pcb)
{
// "impossible", but don't leak if it did happen somehow...
udp_remove (pcb);
pbuf_free (p);
return;
}
if (!p)
return;
if (p->len < sizeof (ntp_frame_t))
{
pbuf_free (p);
return; // not an ntp frame, ignore
}
// make sure we have an aligned copy to work from
ntp_frame_t ntp;
os_memcpy (&ntp, p->payload, sizeof (ntp));
pbuf_free (p);
sntp_dbg("sntp: transmit timestamp: %u, %u\n", ntp.xmit.sec, ntp.xmit.frac);
// sanity checks before we touch our clocks
ip_addr_t anycast;
NTP_ANYCAST_ADDR(&anycast);
if (server.addr != anycast.addr && server.addr != addr->addr)
return; // unknown sender, ignore
if (ntp.origin.sec != state->cookie.sec ||
ntp.origin.frac != state->cookie.frac)
return; // unsolicited message, ignore
if (ntp.LI == 3)
return; // server clock not synchronized (why did it even respond?!)
server.addr = addr->addr;
ntp.origin.sec = ntohl (ntp.origin.sec);
ntp.origin.frac = ntohl (ntp.origin.frac);
ntp.recv.sec = ntohl (ntp.recv.sec);
ntp.recv.frac = ntohl (ntp.recv.frac);
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);
// if we have rtctime, do higher resolution delta calc, else just use
// the transmit timestamp
#ifdef LUA_USE_MODULES_RTCTIME
struct rtc_timeval tv;
2015-07-06 05:45:35 +02:00
rtctime_gettimeofday (&tv);
ntp_timestamp_t dest;
dest.sec = tv.tv_sec;
dest.frac = (MICROSECONDS * tv.tv_usec) / UINT32_MAXI;
// Compensation as per RFC2030
int64_t delta_s = (((int64_t)ntp.recv.sec - ntp.origin.sec) +
((int64_t)ntp.xmit.sec - dest.sec)) / 2;
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;
tv.tv_sec = dest.sec - NTP_TO_UNIX_EPOCH;
tv.tv_usec = (MICROSECONDS * dest.frac) / UINT32_MAXI;
2015-07-06 05:45:35 +02:00
rtctime_settimeofday (&tv);
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);
}
#endif
cleanup (L);
if (have_cb)
{
lua_pushstring (L, ipaddr_ntoa (&server));
lua_call (L, 3, 0);
}
}
// 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.addr == IPADDR_ANY)
NTP_ANYCAST_ADDR(&server);
const char *errmsg = 0;
#define sync_err(x) do { errmsg = x; goto error; } while (0)
if (state)
return luaL_error (L, "sync in progress");
state = (sntp_state_t *)c_malloc (sizeof (sntp_state_t));
if (!state)
sync_err ("out of memory");
memset (state, 0, sizeof (sntp_state_t));
state->sync_cb_ref = LUA_NOREF;
state->err_cb_ref = LUA_NOREF;
state->pcb = udp_new ();
if (!state->pcb)
sync_err ("out of memory");
if (udp_bind (state->pcb, IP_ADDR_ANY, 0) != ERR_OK)
sync_err ("no port available");
udp_recv (state->pcb, on_recv, L);
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;
2015-11-19 21:51:33 +01:00
// 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");
2016-03-07 02:38:45 +01:00
err_t err = dns_gethostbyname(hostname, &server, sntp_dns_found, state);
2015-11-19 21:51:33 +01:00
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");
}
sntp_dosend (L);
return 0;
error:
if (state)
{
if (state->pcb)
udp_remove (state->pcb);
c_free (state);
state = 0;
}
return luaL_error (L, errmsg);
}
// Module function map
static const LUA_REG_TYPE sntp_map[] = {
{ LSTRKEY("sync"), LFUNCVAL(sntp_sync) },
{ LNILKEY, LNILVAL }
};
NODEMCU_MODULE(SNTP, "sntp", sntp_map, NULL);