Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

504 lines
13 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
6 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
8 years ago
  1. /*
  2. NTP MODULE
  3. Based on esp8266 / esp32 configTime and C date and time functions:
  4. - https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/NTP-TZ-DST/NTP-TZ-DST.ino
  5. - https://www.nongnu.org/lwip/2_1_x/group__sntp.html
  6. - man 3 ctime
  7. Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  8. */
  9. #include "ntp.h"
  10. #if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
  11. #include <Arduino.h>
  12. #include <coredecls.h>
  13. #include <Ticker.h>
  14. #include <lwip/apps/sntp.h>
  15. #include <TZ.h>
  16. static_assert(
  17. (SNTP_SERVER_DNS == 1),
  18. "lwip must be configured with SNTP_SERVER_DNS"
  19. );
  20. #include "config/buildtime.h"
  21. #include "ntp_timelib.h"
  22. #include "broker.h"
  23. #include "ws.h"
  24. BrokerBind(NtpBroker);
  25. // Arduino/esp8266 lwip2 custom functions that can be redefined
  26. // Must return time in milliseconds, legacy settings are in seconds.
  27. String _ntp_server;
  28. uint32_t _ntp_startup_delay = (NTP_START_DELAY * 1000);
  29. uint32_t _ntp_update_delay = (NTP_UPDATE_INTERVAL * 1000);
  30. uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() {
  31. return _ntp_startup_delay;
  32. }
  33. uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
  34. return _ntp_update_delay;
  35. }
  36. // We also must shim TimeLib functions until everything else is ported.
  37. // We can't sometimes avoid TimeLib as dependancy though, which would be really bad
  38. static Ticker _ntp_broker_timer;
  39. static bool _ntp_synced = false;
  40. static time_t _ntp_last = 0;
  41. static time_t _ntp_ts = 0;
  42. static tm _ntp_tm_local;
  43. static tm _ntp_tm_utc;
  44. void _ntpTmCache(time_t ts) {
  45. if (_ntp_ts != ts) {
  46. _ntp_ts = ts;
  47. localtime_r(&_ntp_ts, &_ntp_tm_local);
  48. gmtime_r(&_ntp_ts, &_ntp_tm_utc);
  49. }
  50. }
  51. int hour(time_t ts) {
  52. _ntpTmCache(ts);
  53. return _ntp_tm_local.tm_hour;
  54. }
  55. int minute(time_t ts) {
  56. _ntpTmCache(ts);
  57. return _ntp_tm_local.tm_min;
  58. }
  59. int second(time_t ts) {
  60. _ntpTmCache(ts);
  61. return _ntp_tm_local.tm_sec;
  62. }
  63. int day(time_t ts) {
  64. _ntpTmCache(ts);
  65. return _ntp_tm_local.tm_mday;
  66. }
  67. // `tm.tm_wday` range is 0..6, TimeLib is 1..7
  68. int weekday(time_t ts) {
  69. _ntpTmCache(ts);
  70. return _ntp_tm_local.tm_wday + 1;
  71. }
  72. // `tm.tm_mon` range is 0..11, TimeLib range is 1..12
  73. int month(time_t ts) {
  74. _ntpTmCache(ts);
  75. return _ntp_tm_local.tm_mon + 1;
  76. }
  77. int year(time_t ts) {
  78. _ntpTmCache(ts);
  79. return _ntp_tm_local.tm_year + 1900;
  80. }
  81. int utc_hour(time_t ts) {
  82. _ntpTmCache(ts);
  83. return _ntp_tm_utc.tm_hour;
  84. }
  85. int utc_minute(time_t ts) {
  86. _ntpTmCache(ts);
  87. return _ntp_tm_utc.tm_min;
  88. }
  89. int utc_second(time_t ts) {
  90. _ntpTmCache(ts);
  91. return _ntp_tm_utc.tm_sec;
  92. }
  93. int utc_day(time_t ts) {
  94. _ntpTmCache(ts);
  95. return _ntp_tm_utc.tm_mday;
  96. }
  97. int utc_weekday(time_t ts) {
  98. _ntpTmCache(ts);
  99. return _ntp_tm_utc.tm_wday + 1;
  100. }
  101. int utc_month(time_t ts) {
  102. _ntpTmCache(ts);
  103. return _ntp_tm_utc.tm_mon + 1;
  104. }
  105. int utc_year(time_t ts) {
  106. _ntpTmCache(ts);
  107. return _ntp_tm_utc.tm_year + 1900;
  108. }
  109. time_t now() {
  110. return time(nullptr);
  111. }
  112. // -----------------------------------------------------------------------------
  113. #if WEB_SUPPORT
  114. bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  115. return (strncmp(key, "ntp", 3) == 0);
  116. }
  117. void _ntpWebSocketOnVisible(JsonObject& root) {
  118. root["ntpVisible"] = 1;
  119. }
  120. void _ntpWebSocketOnData(JsonObject& root) {
  121. root["ntpStatus"] = ntpSynced();
  122. }
  123. void _ntpWebSocketOnConnected(JsonObject& root) {
  124. root["ntpServer"] = getSetting("ntpServer", F(NTP_SERVER));
  125. root["ntpTZ"] = getSetting("ntpTZ", NTP_TIMEZONE);
  126. }
  127. #endif
  128. String _ntpGetServer() {
  129. String server;
  130. server = sntp_getservername(0);
  131. if (!server.length()) {
  132. auto ip = IPAddress(sntp_getserver(0));
  133. if (ip) {
  134. server = ip.toString();
  135. }
  136. }
  137. return server;
  138. }
  139. NtpInfo ntpInfo() {
  140. NtpInfo result;
  141. auto ts = now();
  142. result.now = ts;
  143. tm sync_tm;
  144. gmtime_r(&_ntp_last, &sync_tm);
  145. result.sync = ntpDateTime(&sync_tm);
  146. tm utc_tm;
  147. gmtime_r(&ts, &utc_tm);
  148. result.utc = ntpDateTime(&utc_tm);
  149. const char* cfg_tz = getenv("TZ");
  150. if ((cfg_tz != nullptr) && (strcmp(cfg_tz, "UTC0") != 0)) {
  151. tm local_tm;
  152. localtime_r(&ts, &local_tm);
  153. result.local = ntpDateTime(&local_tm);
  154. result.tz = cfg_tz;
  155. }
  156. return result;
  157. }
  158. void _ntpReport() {
  159. if (!ntpSynced()) {
  160. DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
  161. return;
  162. }
  163. auto info = ntpInfo();
  164. DEBUG_MSG_P(PSTR("[NTP] Server : %s\n"), _ntp_server.c_str());
  165. DEBUG_MSG_P(PSTR("[NTP] Sync Time : %s (UTC)\n"), info.sync.c_str());
  166. DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), info.utc.c_str());
  167. if (info.tz.length()) {
  168. DEBUG_MSG_P(PSTR("[NTP] Local Time : %s (%s)\n"), info.local.c_str(), info.tz.c_str());
  169. }
  170. }
  171. void _ntpConfigure() {
  172. // Ignore or accept the DHCP SNTP option
  173. // When enabled, it is possible that lwip will replace the NTP server pointer from under us
  174. sntp_servermode_dhcp(getSetting("ntpDhcp", 1 == NTP_DHCP_SERVER));
  175. // Note: TZ_... provided by the Core are already wrapped with PSTR(...)
  176. // but, String() already handles every char pointer as a flash-string
  177. auto cfg_tz = getSetting("ntpTZ", NTP_TIMEZONE);
  178. const char* active_tz = getenv("TZ");
  179. bool changed = cfg_tz != active_tz;
  180. if (changed) {
  181. if (cfg_tz.length()) {
  182. setenv("TZ", cfg_tz.c_str(), 1);
  183. } else {
  184. unsetenv("TZ");
  185. }
  186. tzset();
  187. }
  188. const auto cfg_server = getSetting("ntpServer", F(NTP_SERVER));
  189. const auto active_server = _ntpGetServer();
  190. changed = (cfg_server != active_server) || changed;
  191. // We skip configTime() API since we already set the TZ just above
  192. // (and most of the time we expect NTP server to proxy to multiple servers instead of defining more than one here)
  193. if (changed) {
  194. sntp_stop();
  195. _ntp_server = cfg_server;
  196. sntp_setservername(0, _ntp_server.c_str());
  197. sntp_init();
  198. DEBUG_MSG_P(PSTR("[NTP] Server: %s, TZ: %s\n"), cfg_server.c_str(),
  199. cfg_tz.length() ? cfg_tz.c_str() : "UTC0");
  200. }
  201. }
  202. // -----------------------------------------------------------------------------
  203. bool ntpSynced() {
  204. return _ntp_synced;
  205. }
  206. String ntpDateTime(tm* timestruct) {
  207. char buffer[32];
  208. snprintf_P(buffer, sizeof(buffer),
  209. PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
  210. timestruct->tm_year + 1900,
  211. timestruct->tm_mon + 1,
  212. timestruct->tm_mday,
  213. timestruct->tm_hour,
  214. timestruct->tm_min,
  215. timestruct->tm_sec
  216. );
  217. return String(buffer);
  218. }
  219. String ntpDateTime(time_t ts) {
  220. tm timestruct;
  221. localtime_r(&ts, &timestruct);
  222. return ntpDateTime(&timestruct);
  223. }
  224. String ntpDateTime() {
  225. if (ntpSynced()) {
  226. return ntpDateTime(now());
  227. }
  228. return String();
  229. }
  230. // -----------------------------------------------------------------------------
  231. #if BROKER_SUPPORT
  232. void _ntpBrokerSchedule(int offset);
  233. void _ntpBrokerCallback() {
  234. if (!ntpSynced()) {
  235. _ntpBrokerSchedule(60);
  236. return;
  237. }
  238. const auto ts = now();
  239. // current time and formatter string is in local TZ
  240. tm local_tm;
  241. localtime_r(&ts, &local_tm);
  242. int now_hour = local_tm.tm_hour;
  243. int now_minute = local_tm.tm_min;
  244. static int last_hour = -1;
  245. static int last_minute = -1;
  246. String datetime;
  247. if ((last_minute != now_minute) || (last_hour != now_hour)) {
  248. datetime = ntpDateTime(&local_tm);
  249. }
  250. // notify subscribers about each tick interval (note that both can happen simultaneously)
  251. if (last_hour != now_hour) {
  252. last_hour = now_hour;
  253. NtpBroker::Publish(NtpTick::EveryHour, ts, datetime);
  254. }
  255. if (last_minute != now_minute) {
  256. last_minute = now_minute;
  257. NtpBroker::Publish(NtpTick::EveryMinute, ts, datetime);
  258. }
  259. // try to autocorrect each invocation
  260. _ntpBrokerSchedule(60 - local_tm.tm_sec);
  261. }
  262. // XXX: Nonos docs for some reason mention 100 micros as minimum time. Schedule next second in case this is 0
  263. void _ntpBrokerSchedule(int offset) {
  264. _ntp_broker_timer.once_scheduled(offset ?: 1, _ntpBrokerCallback);
  265. }
  266. #endif
  267. void _ntpSetTimeOfDayCallback() {
  268. _ntp_synced = true;
  269. _ntp_last = time(nullptr);
  270. #if BROKER_SUPPORT
  271. static bool once = true;
  272. if (once) {
  273. schedule_function(_ntpBrokerCallback);
  274. once = false;
  275. }
  276. #endif
  277. #if WEB_SUPPORT
  278. wsPost(_ntpWebSocketOnData);
  279. #endif
  280. schedule_function(_ntpReport);
  281. }
  282. void _ntpSetTimestamp(time_t ts) {
  283. timeval tv { ts, 0 };
  284. timezone tz { 0, 0 };
  285. settimeofday(&tv, &tz);
  286. }
  287. // -----------------------------------------------------------------------------
  288. void _ntpConvertLegacyOffsets() {
  289. bool save { true };
  290. bool found { false };
  291. bool europe { true };
  292. bool dst { true };
  293. int offset { 60 };
  294. settings::kv_store.foreach([&](settings::kvs_type::KeyValueResult&& kv) {
  295. const auto key = kv.key.read();
  296. if (key == F("ntpTZ")) {
  297. save = false;
  298. } else if (key == F("ntpOffset")) {
  299. offset = kv.value.read().toInt();
  300. found = true;
  301. } else if (key == F("ntpDST")) {
  302. dst = (1 == kv.value.read().toInt());
  303. found = true;
  304. } else if (key == F("ntpRegion")) {
  305. europe = (0 == kv.value.read().toInt());
  306. found = true;
  307. }
  308. });
  309. if (save && found) {
  310. // XXX: only expect offsets in hours
  311. String custom { europe ? F("CET") : F("CST") };
  312. custom.reserve(32);
  313. if (offset > 0) {
  314. custom += '-';
  315. }
  316. custom += abs(offset) / 60;
  317. if (dst) {
  318. custom += europe ? F("CEST") : F("EDT");
  319. if (europe) {
  320. custom += F(",M3.5.0,M10.5.0/3");
  321. } else {
  322. custom += F(",M3.2.0,M11.1.0");
  323. }
  324. }
  325. setSetting("ntpTZ", custom);
  326. }
  327. delSetting("ntpOffset");
  328. delSetting("ntpDST");
  329. delSetting("ntpRegion");
  330. }
  331. void ntpSetup() {
  332. // Randomized in time to avoid clogging the server with simultaneous requests from multiple devices
  333. // (for example, when multiple devices start up at the same time)
  334. const uint32_t startup_delay = getSetting("ntpStartDelay", NTP_START_DELAY);
  335. const uint32_t update_delay = getSetting("ntpUpdateIntvl", NTP_UPDATE_INTERVAL);
  336. _ntp_startup_delay = secureRandom(startup_delay, startup_delay * 2);
  337. _ntp_update_delay = secureRandom(update_delay, update_delay * 2);
  338. DEBUG_MSG_P(PSTR("[NTP] Startup delay: %u (s), Update delay: %u (s)\n"),
  339. _ntp_startup_delay, _ntp_update_delay);
  340. _ntp_startup_delay = _ntp_startup_delay * 1000;
  341. _ntp_update_delay = _ntp_update_delay * 1000;
  342. // start up with some reasonable timestamp already available
  343. _ntpSetTimestamp(__UNIX_TIMESTAMP__);
  344. // will be called every time after ntp syncs AND loop() finishes
  345. settimeofday_cb(_ntpSetTimeOfDayCallback);
  346. // generic configuration, always handled
  347. espurnaRegisterReload(_ntpConfigure);
  348. _ntpConvertLegacyOffsets();
  349. _ntpConfigure();
  350. // make sure our logic does know about the actual server
  351. // in case dhcp sends out ntp settings
  352. static WiFiEventHandler on_sta = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP) {
  353. if (!sntp_enabled()) {
  354. return;
  355. }
  356. auto server = _ntpGetServer();
  357. if (!server.length()) {
  358. DEBUG_MSG_P(PSTR("[NTP] Updating `ntpDhcp` to ignore the DHCP values\n"));
  359. setSetting("ntpDhcp", "0");
  360. sntp_servermode_dhcp(0);
  361. schedule_function(_ntpConfigure);
  362. return;
  363. }
  364. if (!_ntp_server.length() || (server != _ntp_server)) {
  365. DEBUG_MSG_P(PSTR("[NTP] Updating `ntpServer` setting from DHCP: %s\n"), server.c_str());
  366. _ntp_server = server;
  367. setSetting("ntpServer", server);
  368. }
  369. });
  370. // optional functionality
  371. #if WEB_SUPPORT
  372. wsRegister()
  373. .onVisible(_ntpWebSocketOnVisible)
  374. .onConnected(_ntpWebSocketOnConnected)
  375. .onData(_ntpWebSocketOnData)
  376. .onKeyCheck(_ntpWebSocketOnKeyCheck);
  377. #endif
  378. #if TERMINAL_SUPPORT
  379. terminalRegisterCommand(F("NTP"), [](const terminal::CommandContext&) {
  380. _ntpReport();
  381. terminalOK();
  382. });
  383. terminalRegisterCommand(F("NTP.SETTIME"), [](const terminal::CommandContext& ctx) {
  384. if (ctx.argc != 2) return;
  385. _ntp_synced = true;
  386. _ntpSetTimestamp(ctx.argv[1].toInt());
  387. terminalOK();
  388. });
  389. // TODO:
  390. // terminalRegisterCommand(F("NTP.SYNC"), [](const terminal::CommandContext&) { ... }
  391. //
  392. #endif
  393. }
  394. #endif // NTP_SUPPORT && !NTP_LEGACY_SUPPORT