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.

468 lines
12 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
7 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. static_assert(
  15. (SNTP_SERVER_DNS == 1),
  16. "lwip must be configured with SNTP_SERVER_DNS"
  17. );
  18. #include "config/buildtime.h"
  19. #include "debug.h"
  20. #include "broker.h"
  21. #include "ws.h"
  22. BrokerBind(NtpBroker);
  23. // Arduino/esp8266 lwip2 custom functions that can be redefined
  24. // Must return time in milliseconds, legacy settings are in seconds.
  25. String _ntp_server;
  26. uint32_t _ntp_startup_delay = (NTP_START_DELAY * 1000);
  27. uint32_t _ntp_update_delay = (NTP_UPDATE_INTERVAL * 1000);
  28. uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() {
  29. return _ntp_startup_delay;
  30. }
  31. uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
  32. return _ntp_update_delay;
  33. }
  34. // We also must shim TimeLib functions until everything else is ported.
  35. // We can't sometimes avoid TimeLib as dependancy though, which would be really bad
  36. static Ticker _ntp_broker_timer;
  37. static bool _ntp_synced = false;
  38. static time_t _ntp_last = 0;
  39. static time_t _ntp_ts = 0;
  40. static tm _ntp_tm_local;
  41. static tm _ntp_tm_utc;
  42. void _ntpTmCache(time_t ts) {
  43. if (_ntp_ts != ts) {
  44. _ntp_ts = ts;
  45. localtime_r(&_ntp_ts, &_ntp_tm_local);
  46. gmtime_r(&_ntp_ts, &_ntp_tm_utc);
  47. }
  48. }
  49. int hour(time_t ts) {
  50. _ntpTmCache(ts);
  51. return _ntp_tm_local.tm_hour;
  52. }
  53. int minute(time_t ts) {
  54. _ntpTmCache(ts);
  55. return _ntp_tm_local.tm_min;
  56. }
  57. int second(time_t ts) {
  58. _ntpTmCache(ts);
  59. return _ntp_tm_local.tm_sec;
  60. }
  61. int day(time_t ts) {
  62. _ntpTmCache(ts);
  63. return _ntp_tm_local.tm_mday;
  64. }
  65. // `tm.tm_wday` range is 0..6, TimeLib is 1..7
  66. int weekday(time_t ts) {
  67. _ntpTmCache(ts);
  68. return _ntp_tm_local.tm_wday + 1;
  69. }
  70. // `tm.tm_mon` range is 0..11, TimeLib range is 1..12
  71. int month(time_t ts) {
  72. _ntpTmCache(ts);
  73. return _ntp_tm_local.tm_mon + 1;
  74. }
  75. int year(time_t ts) {
  76. _ntpTmCache(ts);
  77. return _ntp_tm_local.tm_year + 1900;
  78. }
  79. int utc_hour(time_t ts) {
  80. _ntpTmCache(ts);
  81. return _ntp_tm_utc.tm_hour;
  82. }
  83. int utc_minute(time_t ts) {
  84. _ntpTmCache(ts);
  85. return _ntp_tm_utc.tm_min;
  86. }
  87. int utc_second(time_t ts) {
  88. _ntpTmCache(ts);
  89. return _ntp_tm_utc.tm_sec;
  90. }
  91. int utc_day(time_t ts) {
  92. _ntpTmCache(ts);
  93. return _ntp_tm_utc.tm_mday;
  94. }
  95. int utc_weekday(time_t ts) {
  96. _ntpTmCache(ts);
  97. return _ntp_tm_utc.tm_wday + 1;
  98. }
  99. int utc_month(time_t ts) {
  100. _ntpTmCache(ts);
  101. return _ntp_tm_utc.tm_mon + 1;
  102. }
  103. int utc_year(time_t ts) {
  104. _ntpTmCache(ts);
  105. return _ntp_tm_utc.tm_year + 1900;
  106. }
  107. time_t now() {
  108. return time(nullptr);
  109. }
  110. // -----------------------------------------------------------------------------
  111. #if WEB_SUPPORT
  112. bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  113. return (strncmp(key, "ntp", 3) == 0);
  114. }
  115. void _ntpWebSocketOnVisible(JsonObject& root) {
  116. root["ntpVisible"] = 1;
  117. }
  118. void _ntpWebSocketOnData(JsonObject& root) {
  119. root["ntpStatus"] = ntpSynced();
  120. }
  121. void _ntpWebSocketOnConnected(JsonObject& root) {
  122. root["ntpServer"] = getSetting("ntpServer", F(NTP_SERVER));
  123. root["ntpTZ"] = getSetting("ntpTZ", NTP_TIMEZONE);
  124. }
  125. #endif
  126. // TODO: mention possibility of multiple servers
  127. String _ntpGetServer() {
  128. String server;
  129. server = sntp_getservername(0);
  130. if (!server.length()) {
  131. server = IPAddress(sntp_getserver(0)).toString();
  132. }
  133. return server;
  134. }
  135. NtpInfo ntpInfo() {
  136. NtpInfo result;
  137. auto ts = now();
  138. result.now = ts;
  139. tm sync_tm;
  140. gmtime_r(&_ntp_last, &sync_tm);
  141. result.sync = ntpDateTime(&sync_tm);
  142. tm utc_tm;
  143. gmtime_r(&ts, &utc_tm);
  144. result.utc = ntpDateTime(&utc_tm);
  145. const char* cfg_tz = getenv("TZ");
  146. if ((cfg_tz != nullptr) && (strcmp(cfg_tz, "UTC0") != 0)) {
  147. tm local_tm;
  148. localtime_r(&ts, &local_tm);
  149. result.local = ntpDateTime(&local_tm);
  150. result.tz = cfg_tz;
  151. }
  152. return result;
  153. }
  154. void _ntpReport() {
  155. if (!ntpSynced()) {
  156. DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
  157. return;
  158. }
  159. auto info = ntpInfo();
  160. DEBUG_MSG_P(PSTR("[NTP] Server : %s\n"), _ntp_server.c_str());
  161. DEBUG_MSG_P(PSTR("[NTP] Sync Time : %s (UTC)\n"), info.sync.c_str());
  162. DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), info.utc.c_str());
  163. if (info.tz.length()) {
  164. DEBUG_MSG_P(PSTR("[NTP] Local Time : %s (%s)\n"), info.local.c_str(), info.tz.c_str());
  165. }
  166. }
  167. void _ntpConfigure() {
  168. // Note: TZ_... provided by the Core are already wrapped with PSTR(...)
  169. const auto cfg_tz = getSetting("ntpTZ", NTP_TIMEZONE);
  170. const char* active_tz = getenv("TZ");
  171. if (cfg_tz != active_tz) {
  172. setenv("TZ", cfg_tz.c_str(), 1);
  173. tzset();
  174. }
  175. const auto cfg_server = getSetting("ntpServer", F(NTP_SERVER));
  176. const auto active_server = _ntpGetServer();
  177. if (cfg_tz != active_tz) {
  178. _ntp_server = cfg_server;
  179. configTime(cfg_tz.c_str(), _ntp_server.c_str());
  180. DEBUG_MSG_P(PSTR("[NTP] Server: %s, TZ: %s\n"), cfg_server.c_str(), cfg_tz.length() ? cfg_tz.c_str() : "UTC0");
  181. }
  182. }
  183. // -----------------------------------------------------------------------------
  184. bool ntpSynced() {
  185. return _ntp_synced;
  186. }
  187. String ntpDateTime(tm* timestruct) {
  188. char buffer[20];
  189. snprintf_P(buffer, sizeof(buffer),
  190. PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
  191. timestruct->tm_year + 1900,
  192. timestruct->tm_mon + 1,
  193. timestruct->tm_mday,
  194. timestruct->tm_hour,
  195. timestruct->tm_min,
  196. timestruct->tm_sec
  197. );
  198. return String(buffer);
  199. }
  200. String ntpDateTime(time_t ts) {
  201. tm timestruct;
  202. localtime_r(&ts, &timestruct);
  203. return ntpDateTime(&timestruct);
  204. }
  205. String ntpDateTime() {
  206. if (ntpSynced()) {
  207. return ntpDateTime(now());
  208. }
  209. return String();
  210. }
  211. // -----------------------------------------------------------------------------
  212. #if BROKER_SUPPORT
  213. void _ntpBrokerSchedule(int offset);
  214. void _ntpBrokerCallback() {
  215. if (!ntpSynced()) {
  216. _ntpBrokerSchedule(60);
  217. return;
  218. }
  219. const auto ts = now();
  220. // current time and formatter string is in local TZ
  221. tm local_tm;
  222. localtime_r(&ts, &local_tm);
  223. int now_hour = local_tm.tm_hour;
  224. int now_minute = local_tm.tm_min;
  225. static int last_hour = -1;
  226. static int last_minute = -1;
  227. String datetime;
  228. if ((last_minute != now_minute) || (last_hour != now_hour)) {
  229. datetime = ntpDateTime(&local_tm);
  230. }
  231. // notify subscribers about each tick interval (note that both can happen simultaneously)
  232. if (last_hour != now_hour) {
  233. last_hour = now_hour;
  234. NtpBroker::Publish(NtpTick::EveryHour, ts, datetime.c_str());
  235. }
  236. if (last_minute != now_minute) {
  237. last_minute = now_minute;
  238. NtpBroker::Publish(NtpTick::EveryMinute, ts, datetime.c_str());
  239. }
  240. // try to autocorrect each invocation
  241. _ntpBrokerSchedule(60 - local_tm.tm_sec);
  242. }
  243. // XXX: Nonos docs for some reason mention 100 micros as minimum time. Schedule next second in case this is 0
  244. void _ntpBrokerSchedule(int offset) {
  245. _ntp_broker_timer.once_scheduled(offset ?: 1, _ntpBrokerCallback);
  246. }
  247. #endif
  248. void _ntpSetTimeOfDayCallback() {
  249. _ntp_synced = true;
  250. _ntp_last = time(nullptr);
  251. #if BROKER_SUPPORT
  252. static bool once = true;
  253. if (once) {
  254. schedule_function(_ntpBrokerCallback);
  255. once = false;
  256. }
  257. #endif
  258. #if WEB_SUPPORT
  259. wsPost(_ntpWebSocketOnData);
  260. #endif
  261. schedule_function(_ntpReport);
  262. }
  263. void _ntpSetTimestamp(time_t ts) {
  264. timeval tv { ts, 0 };
  265. timezone tz { 0, 0 };
  266. settimeofday(&tv, &tz);
  267. }
  268. // -----------------------------------------------------------------------------
  269. void _ntpConvertLegacyOffsets() {
  270. bool found { false };
  271. bool europe { true };
  272. bool dst { true };
  273. int offset { 60 };
  274. settings::kv_store.foreach([&](settings::kvs_type::KeyValueResult&& kv) {
  275. const auto key = kv.key.read();
  276. if (key == F("ntpOffset")) {
  277. offset = kv.value.read().toInt();
  278. found = true;
  279. } else if (key == F("ntpDST")) {
  280. dst = (1 == kv.value.read().toInt());
  281. found = true;
  282. } else if (key == F("ntpRegion")) {
  283. europe = (0 == kv.value.read().toInt());
  284. found = true;
  285. }
  286. });
  287. if (!found) {
  288. return;
  289. }
  290. // XXX: only expect offsets in hours
  291. String custom { europe ? F("CET") : F("CST") };
  292. custom.reserve(32);
  293. if (offset > 0) {
  294. custom += '-';
  295. }
  296. custom += abs(offset) / 60;
  297. if (dst) {
  298. custom += europe ? F("CEST") : F("EDT");
  299. if (europe) {
  300. custom += F(",M3.5.0,M10.5.0/3");
  301. } else {
  302. custom += F(",M3.2.0,M11.1.0");
  303. }
  304. }
  305. delSetting("ntpOffset");
  306. delSetting("ntpDST");
  307. delSetting("ntpRegion");
  308. setSetting("ntpTZ", custom);
  309. }
  310. void ntpSetup() {
  311. // Randomized in time to avoid clogging the server with simultaneous requests from multiple devices
  312. // (for example, when multiple devices start up at the same time)
  313. const uint32_t startup_delay = getSetting("ntpStartDelay", NTP_START_DELAY);
  314. const uint32_t update_delay = getSetting("ntpUpdateIntvl", NTP_UPDATE_INTERVAL);
  315. _ntp_startup_delay = secureRandom(startup_delay, startup_delay * 2);
  316. _ntp_update_delay = secureRandom(update_delay, update_delay * 2);
  317. DEBUG_MSG_P(PSTR("[NTP] Startup delay: %us, Update delay: %us\n"),
  318. _ntp_startup_delay, _ntp_update_delay
  319. );
  320. _ntp_startup_delay = _ntp_startup_delay * 1000;
  321. _ntp_update_delay = _ntp_update_delay * 1000;
  322. // start up with some reasonable timestamp already available
  323. _ntpSetTimestamp(__UNIX_TIMESTAMP__);
  324. // will be called every time after ntp syncs AND loop() finishes
  325. settimeofday_cb(_ntpSetTimeOfDayCallback);
  326. // generic configuration, always handled
  327. espurnaRegisterReload(_ntpConfigure);
  328. _ntpConvertLegacyOffsets();
  329. _ntpConfigure();
  330. // make sure our logic does know about the actual server
  331. // in case dhcp sends out ntp settings
  332. static WiFiEventHandler on_sta = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP) {
  333. const auto server = _ntpGetServer();
  334. if (sntp_enabled() && (!_ntp_server.length() || (server != _ntp_server))) {
  335. DEBUG_MSG_P(PSTR("[NTP] Updating `ntpServer` setting from DHCP: %s\n"), server.c_str());
  336. _ntp_server = server;
  337. setSetting("ntpServer", server);
  338. }
  339. });
  340. // optional functionality
  341. #if WEB_SUPPORT
  342. wsRegister()
  343. .onVisible(_ntpWebSocketOnVisible)
  344. .onConnected(_ntpWebSocketOnConnected)
  345. .onData(_ntpWebSocketOnData)
  346. .onKeyCheck(_ntpWebSocketOnKeyCheck);
  347. #endif
  348. #if TERMINAL_SUPPORT
  349. terminalRegisterCommand(F("NTP"), [](const terminal::CommandContext&) {
  350. _ntpReport();
  351. terminalOK();
  352. });
  353. terminalRegisterCommand(F("NTP.SETTIME"), [](const terminal::CommandContext& ctx) {
  354. if (ctx.argc != 2) return;
  355. _ntp_synced = true;
  356. _ntpSetTimestamp(ctx.argv[1].toInt());
  357. terminalOK();
  358. });
  359. // TODO:
  360. // terminalRegisterCommand(F("NTP.SYNC"), [](const terminal::CommandContext&) { ... }
  361. //
  362. #endif
  363. }
  364. #endif // NTP_SUPPORT && !NTP_LEGACY_SUPPORT