An ESP32-based Redis-watcher and info-displayer https://rpjios.com
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.

729 lines
21 KiB

  1. // zero_watch.ino
  2. // (C) 2019 Ryan Joseph <ryan@electricsheep.co>
  3. // https://github.com/rpj/zw
  4. #include <WiFi.h>
  5. #include <Update.h>
  6. #include <ArduinoJson.h>
  7. #include "zw_common.h"
  8. #include "zw_logging.h"
  9. #include "zw_redis.h"
  10. #include "zw_provision.h"
  11. #include "zw_displays.h"
  12. #include "zw_otp.h"
  13. #include "zw_ota.h"
  14. #include "zw_wifi.h"
  15. #define DEEP_SLEEP_MODE_ENABLE 1
  16. #define CONTROL_POINT_SEP_CHAR '#'
  17. #define SER_BAUD 115200
  18. #define DEF_BRIGHT 2
  19. #define CHECKIN_EVERY_X_REFRESH 5
  20. #define CHECKIN_EXPIRY_MULT 2
  21. #define HEARTBEAT_EXPIRY_MULT 5
  22. #if DEBUG
  23. #define DEF_REFRESH 20
  24. #else
  25. #define DEF_REFRESH 240
  26. #endif
  27. ZWAppConfig gConfig = {
  28. .brightness = DEF_BRIGHT,
  29. .refresh = DEF_REFRESH,
  30. .debug = DEBUG,
  31. .publishLogs = false,
  32. .pauseRefresh = false,
  33. .deepSleepMode = DEEP_SLEEP_MODE_ENABLE};
  34. ZWRedis *gRedis = NULL;
  35. DisplaySpec *gDisplays = NULL;
  36. void (*gPublishLogsEmit)(const char *fmt, ...);
  37. unsigned long gBootCount = 0;
  38. unsigned long long gSecondsSinceBoot = 0;
  39. unsigned long long gLastRefreshTick = 0;
  40. int _last_free = 0;
  41. unsigned long gUDRA = 0;
  42. unsigned long immediateLatency = 0;
  43. StaticJsonBuffer<1024> jsonBuf;
  44. hw_timer_t *__isrTimer = NULL;
  45. portMUX_TYPE __isrMutex = portMUX_INITIALIZER_UNLOCKED;
  46. volatile unsigned long __isrCount = 0;
  47. bool processGetValue(String &imEmit, ZWRedisResponder &responder)
  48. {
  49. bool matched = true;
  50. imEmit.toLowerCase();
  51. if (imEmit.startsWith("ip") || imEmit.endsWith("address"))
  52. {
  53. responder.setValue("%s", WiFi.localIP().toString().c_str());
  54. }
  55. else if (imEmit.startsWith("mem"))
  56. {
  57. auto cur_free = ESP.getFreeHeap();
  58. responder.setValue(
  59. "{ \"current\": %d, \"last\": %d, \"delta\": %d, \"heap\": %d }",
  60. cur_free, _last_free, cur_free - _last_free, ESP.getHeapSize());
  61. }
  62. else if (imEmit.startsWith("up"))
  63. {
  64. responder.setExpire(5);
  65. responder.setValue("%s", String(millis() / 1000).c_str());
  66. }
  67. else if (imEmit.startsWith("ver"))
  68. {
  69. responder.setValue(
  70. "{ \"version\": \"%s\", \"sketchMD5\": \"%s\", "
  71. "\"sketchSize\": %d, \"sdk\": \"%s\", \"chipRev\": %d, \"eFuse\": %d}",
  72. ZEROWATCH_VER, ESP.getSketchMD5().c_str(), ESP.getSketchSize(),
  73. ESP.getSdkVersion(), ESP.getChipRevision(), ESP.getEfuseMac());
  74. }
  75. else if (imEmit.equals("demo"))
  76. {
  77. demoMode(gDisplays);
  78. }
  79. else if (imEmit.equals("latency"))
  80. {
  81. responder.setValue("{ \"immediate\": %d, \"rollingAvg\": %d }",
  82. immediateLatency, gUDRA);
  83. }
  84. else
  85. {
  86. matched = false;
  87. }
  88. return matched;
  89. }
  90. bool ctrlPoint_reset()
  91. {
  92. dprint("[CMD] RESETING!\n");
  93. zlog("[CMD] RESETING!");
  94. gRedis->clearControlPoint();
  95. ESP.restart();
  96. return true; // never reached
  97. }
  98. bool ctrlPoint_clearbootcount()
  99. {
  100. dprint("[CMD] Clearing boot count!\n");
  101. zlog("[CMD] Clearing boot count!\n");
  102. return gRedis->incrementBootcount(true) == 0;
  103. }
  104. bool processControlPoint(String &imEmit, ZWRedisResponder &responder)
  105. {
  106. auto sepIdx = imEmit.indexOf(CONTROL_POINT_SEP_CHAR);
  107. if (sepIdx == -1 && sepIdx < imEmit.length())
  108. {
  109. zlog("ERROR: control point write is malformed ('%s')\n", imEmit.c_str());
  110. return false;
  111. }
  112. auto cmd = imEmit.substring(0, sepIdx);
  113. auto otp = (uint16_t)imEmit.substring(sepIdx + 1).toInt();
  114. zlog("Control point has '%s' with OTP %d...\n", cmd.c_str(), otp);
  115. if (!otpCheck(otp))
  116. {
  117. zlog("ERROR: processControlPoint: Not authorized!\n");
  118. return false;
  119. }
  120. zlog("Control point is authorized.\n");
  121. bool (*ctrlPointFunc)() = NULL;
  122. if (cmd.equals("reset"))
  123. {
  124. ctrlPointFunc = ctrlPoint_reset;
  125. }
  126. else if (cmd.equals("clearbootcount"))
  127. {
  128. ctrlPointFunc = ctrlPoint_clearbootcount;
  129. }
  130. if (ctrlPointFunc)
  131. {
  132. zlog("Control point command '%s' is valid: executing.\n", cmd.c_str());
  133. return ctrlPointFunc();
  134. }
  135. else
  136. {
  137. zlog("WARNING: unknown control point command '%s'\n", cmd.c_str());
  138. }
  139. return true;
  140. }
  141. void preUpdateIRQDisableFunc()
  142. {
  143. zlog("preUpdateIRQDisableFunc disabling __isrTimer\n");
  144. timerEnd(__isrTimer);
  145. }
  146. bool processUpdate(String &updateJson, ZWRedisResponder &responder)
  147. {
  148. JsonObject &updateObj = jsonBuf.parseObject(updateJson.c_str());
  149. if (updateObj.success())
  150. {
  151. auto url = updateObj.get<char *>("url");
  152. auto md5 = updateObj.get<char *>("md5");
  153. auto szb = updateObj.get<int>("size");
  154. auto otp = updateObj.get<unsigned long>("otp");
  155. if (!otpCheck(otp))
  156. {
  157. zlog("ERROR: Not authorized\n");
  158. return false;
  159. }
  160. zlog("Accepted OTA OTP '%ld'\n", otp);
  161. if (url && md5 && szb > 0)
  162. {
  163. auto fqUrlLen = strlen(EEPROMCFG_OTAHost) + strlen(url) + 2;
  164. char *fqUrl = (char *)malloc(fqUrlLen);
  165. bzero(fqUrl, fqUrlLen);
  166. auto fqWrote = snprintf(fqUrl, fqUrlLen, "%s/%s", EEPROMCFG_OTAHost, url);
  167. if (fqWrote != fqUrlLen - 1)
  168. zlog("WARNING: wrote %d of expected %d bytes for fqUrl\n", fqWrote, fqUrlLen);
  169. zlog("Starting OTA update of %0.2fKB\n", (szb / 1024.0));
  170. zlog("Image source (md5=%s):\n\t%s\n", md5, fqUrl);
  171. if (runUpdate(fqUrl, md5, szb, preUpdateIRQDisableFunc, []() {
  172. if (gRedis->incrementBootcount(true) != 0)
  173. zlog("WARNING: unable to reset bootcount!\n");
  174. return gRedis->postCompletedUpdate();
  175. }))
  176. {
  177. zlog("OTA update wrote successfully! Restarting in %d seconds...\n",
  178. OTA_RESET_DELAY);
  179. delay(OTA_RESET_DELAY * 1000);
  180. ESP.restart();
  181. return true; // never reached
  182. }
  183. else
  184. {
  185. zlog("ERROR: OTA failed! %d\n", Update.getError());
  186. }
  187. free(fqUrl);
  188. }
  189. else
  190. {
  191. zlog("ERROR: Bad OTA params (%p, %p, %d)\n", url, md5, szb);
  192. }
  193. }
  194. else
  195. {
  196. zlog("ERROR: failed to parse update\n");
  197. }
  198. return false;
  199. }
  200. bool processDisplaysConfig(String &updateJson, ZWRedisResponder &responder)
  201. {
  202. dprint("GOT DISPLAYS CONFIG: %s\n", updateJson.c_str());
  203. return false;
  204. }
  205. void redis_publish_logs_emit(const char *fmt, ...)
  206. {
  207. // this function should never be called before gRedis is valid
  208. zwassert(gRedis != NULL);
  209. char buf[1024];
  210. bzero(buf, 1024);
  211. va_list args;
  212. va_start(args, fmt);
  213. vsprintf(buf, fmt, args);
  214. va_end(args);
  215. auto len = strlen(buf);
  216. if (buf[len - 1] == '\n')
  217. buf[len - 1] = '\0';
  218. #define PUB_FMT_STR "{\"source\":\"%s\",\"type\":\"VALUE\",\"ts\":%s,\"value\":{\"logline\":\"%s\"}}"
  219. auto tickStr = String((unsigned long)gSecondsSinceBoot);
  220. auto pubLen = strlen(PUB_FMT_STR) + len + gHostname.length() + tickStr.length();
  221. char *jbuf = (char *)malloc(pubLen);
  222. bzero(jbuf, pubLen);
  223. snprintf(jbuf, pubLen, PUB_FMT_STR, gHostname.c_str(), tickStr.c_str(), buf);
  224. gRedis->publishLog(jbuf);
  225. free(jbuf);
  226. }
  227. #if M5STACKC
  228. void M5Stack_publish_logs_emit(const char *fmt, ...)
  229. {
  230. char buf[1024];
  231. bzero(buf, 1024);
  232. va_list args;
  233. va_start(args, fmt);
  234. vsprintf(buf, fmt, args);
  235. va_end(args);
  236. auto len = strlen(buf);
  237. if (buf[len - 1] == '\n')
  238. buf[len - 1] = '\0';
  239. M5.Lcd.println(buf);
  240. Serial.println(buf);
  241. }
  242. #endif
  243. #if M5STACKC
  244. void readAndSetTime()
  245. {
  246. RTC_TimeTypeDef ts;
  247. bzero(&ts, sizeof(ts));
  248. gRedis->getTime(&ts.Hours, &ts.Minutes, &ts.Seconds);
  249. if (!(!ts.Hours && !ts.Minutes && !ts.Seconds))
  250. {
  251. M5.Rtc.SetTime(&ts);
  252. zlog("Set time: %02d:%02d\n", ts.Hours, ts.Minutes);
  253. }
  254. else
  255. {
  256. zlog("Failed to get time from Redis (or it is exactly midnight!)\n");
  257. }
  258. }
  259. #else
  260. // TODO need to make this functional for all other devices!!
  261. #define readAndSetTime()
  262. #endif
  263. void readConfigAndUserKeys()
  264. {
  265. readAndSetTime();
  266. auto curCfg = gRedis->readConfig();
  267. bool dirty = false;
  268. #define UPDATE_IF_CHANGED(field) \
  269. if (curCfg.field != gConfig.field) \
  270. { \
  271. zlog("[Config] %s -> %d\n", #field, curCfg.field); \
  272. gConfig.field = curCfg.field; \
  273. }
  274. #define UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRAEXTRA(field, extraCond, extraIfUpdated) \
  275. if ((extraCond) && curCfg.field != gConfig.field) \
  276. { \
  277. zlog("[Config] %s -> %d\n", #field, curCfg.field); \
  278. gConfig.field = curCfg.field; \
  279. extraIfUpdated; \
  280. } \
  281. else if (!(extraCond)) \
  282. { \
  283. zlog("Redis has invalid %s configuration, %d: correcting to %d\n", \
  284. #field, curCfg.field, gConfig.field); \
  285. curCfg.field = gConfig.field; \
  286. dirty = true; \
  287. }
  288. #define UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRA(field, extraCond) \
  289. UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRAEXTRA(field, extraCond, do {} while (0))
  290. #if M5STACKC
  291. /*UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRAEXTRA(brightness,
  292. curCfg.brightness >= 0 && curCfg.brightness < 8,
  293. M5.Axp.ScreenBreath(gConfig.brightness + 7));*/
  294. #else
  295. UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRAEXTRA(brightness,
  296. curCfg.brightness >= 0 && curCfg.brightness < 8,
  297. EXEC_ALL_DISPS(gDisplays, setBrightness(gConfig.brightness)));
  298. #endif
  299. UPDATE_IF_CHANGED_ELSE_MARKED_DIRTY_WITH_EXTRA(refresh, curCfg.refresh >= 5);
  300. UPDATE_IF_CHANGED(debug);
  301. UPDATE_IF_CHANGED(publishLogs);
  302. UPDATE_IF_CHANGED(pauseRefresh);
  303. UPDATE_IF_CHANGED(deepSleepMode);
  304. if (dirty)
  305. {
  306. auto badCount = gRedis->updateConfig(curCfg);
  307. if (badCount)
  308. {
  309. zlog("WARNING: tried to update Redis config but hit %d errors\n", badCount);
  310. }
  311. }
  312. if (!gConfig.pauseRefresh)
  313. {
  314. gRedis->handleUserKey(":config:getValue", processGetValue);
  315. gRedis->handleUserKey(":config:controlPoint", processControlPoint);
  316. gRedis->handleUserKey(":config:update", processUpdate);
  317. gRedis->handleUserKey(":config:displays", processDisplaysConfig);
  318. }
  319. }
  320. void heartbeat()
  321. {
  322. static uint64_t __hb_count = 0;
  323. if (gRedis)
  324. {
  325. if (!gRedis->heartbeat(gConfig.refresh * HEARTBEAT_EXPIRY_MULT))
  326. {
  327. zlog("WARNING: heartbeat failed!\n");
  328. }
  329. if (gConfig.deepSleepMode || (__hb_count++ % CHECKIN_EVERY_X_REFRESH))
  330. {
  331. gRedis->checkin(gConfig.deepSleepMode ? gBootCount : gSecondsSinceBoot, WiFi.localIP().toString().c_str(),
  332. immediateLatency, gUDRA, gConfig.refresh * CHECKIN_EVERY_X_REFRESH * CHECKIN_EXPIRY_MULT);
  333. }
  334. }
  335. }
  336. #if M5STACKC
  337. int xLineOff = 148;
  338. int yLineOffTop = 5;
  339. int yLineOffBot = 31;
  340. auto brightLvl = gConfig.brightness * 3;
  341. auto borderClr = DARKGREY;
  342. auto barClr = DARKCYAN;
  343. uint16_t brightIcon[8][6] = {
  344. { BLACK, BLACK, BLACK, BLACK, BLACK, BLACK },
  345. { BLACK, BLACK, BLACK, BLACK, BLACK, BLACK },
  346. { DARKGREY, DARKGREY, DARKGREY, DARKGREY, DARKGREY, DARKGREY },
  347. { DARKGREY, DARKGREY, DARKGREY, DARKGREY, DARKGREY, DARKGREY },
  348. { LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY },
  349. { LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY, LIGHTGREY },
  350. { WHITE, WHITE, WHITE, WHITE, WHITE, WHITE },
  351. { WHITE, WHITE, WHITE, WHITE, WHITE, WHITE }
  352. };
  353. void zwM5StickC_DrawBitmap(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t* bitmap)
  354. {
  355. for (int x_w = x; x_w < (x + w); x_w++) {
  356. for (int y_w = y; y_w < (y + h); y_w++) {
  357. M5.Lcd.drawPixel(x_w + x, y_w + y, *((bitmap + (x_w - x) * h) + (y_w - y)));
  358. }
  359. }
  360. }
  361. void zwM5StickC_DrawBrightnessMeterBorder()
  362. {
  363. M5.Lcd.drawLine(xLineOff, yLineOffTop, xLineOff+5, yLineOffTop, borderClr);
  364. M5.Lcd.drawLine(xLineOff, yLineOffBot, xLineOff+5, yLineOffBot, borderClr);
  365. M5.Lcd.drawLine(xLineOff, yLineOffTop, xLineOff, yLineOffBot, borderClr);
  366. M5.Lcd.drawLine(xLineOff+5, yLineOffTop, xLineOff+5, yLineOffBot, borderClr);
  367. //zwM5StickC_DrawBitmap(xLineOff-2, yLineOffBot+2, 8, 8, (uint16_t*)brightIcon);
  368. }
  369. void zwM5StickC_UpdateBrightnessMeter()
  370. {
  371. auto brightLvl = gConfig.brightness * 3;
  372. M5.Lcd.drawLine(xLineOff+2, yLineOffTop+1, xLineOff+2, yLineOffBot-1, BLACK);
  373. M5.Lcd.drawLine(xLineOff+3, yLineOffTop+1, xLineOff+3, yLineOffBot-1, BLACK);
  374. auto tmpY = yLineOffBot - 2;
  375. M5.Lcd.drawLine(xLineOff+2, tmpY - brightLvl, xLineOff+2, tmpY, barClr);
  376. M5.Lcd.drawLine(xLineOff+3, tmpY - brightLvl, xLineOff+3, tmpY, barClr);
  377. zwM5StickC_DrawBrightnessMeterBorder();
  378. }
  379. void zwM5StickC_UpdateBatteryDisplay()
  380. {
  381. double vbat = 0.0;
  382. int discharge, charge;
  383. double temp = 0.0;
  384. vbat = M5.Axp.GetVbatData() * 1.1 / 1000;
  385. charge = M5.Axp.GetIchargeData() / 2;
  386. discharge = M5.Axp.GetIdischargeData() / 2;
  387. temp = -144.7 + M5.Axp.GetTempData() * 0.1;
  388. const int xOff = 94;
  389. const int yIncr = 16;
  390. const int battFont = 2;
  391. int yOff = 0;
  392. M5.Lcd.setTextColor(LIGHTGREY, BLACK);
  393. M5.Lcd.drawLine(xOff - 8, yOff, xOff - 8, yOff + 80, DARKCYAN);
  394. M5.Lcd.drawLine(xOff - 6, yOff, xOff - 6, yOff + 80, DARKCYAN);
  395. M5.Lcd.setCursor(xOff, yOff, battFont);
  396. uint16_t voltageColor = RED;
  397. if (!M5.Axp.GetWarningLeve())
  398. voltageColor = vbat > 3.9 ? GREEN : (vbat > 3.7 ? YELLOW : ORANGE);
  399. M5.Lcd.setTextColor(voltageColor, BLACK);
  400. M5.Lcd.printf("%.3fV\n", vbat); //battery voltage
  401. M5.Lcd.setCursor(xOff, yOff += yIncr, battFont);
  402. M5.Lcd.setTextColor(LIGHTGREY, BLACK);
  403. M5.Lcd.printf("%.1fC\n", temp); //axp192 inside temp
  404. M5.Lcd.setCursor(xOff, yOff += yIncr, battFont);
  405. if (charge)
  406. {
  407. M5.Lcd.setTextColor(GREEN, BLACK);
  408. M5.Lcd.printf("%dmA \n", charge); //battery charging current
  409. M5.Lcd.setCursor(xOff, yOff += yIncr, battFont);
  410. M5.Lcd.setTextColor(LIGHTGREY, BLACK);
  411. }
  412. if (discharge)
  413. {
  414. M5.Lcd.setTextColor(ORANGE, BLACK);
  415. M5.Lcd.printf("%dmA \n", discharge); //battery output current
  416. M5.Lcd.setCursor(xOff, yOff += yIncr, battFont);
  417. M5.Lcd.setTextColor(LIGHTGREY, BLACK);
  418. }
  419. M5.Rtc.GetBm8563Time();
  420. M5.Lcd.setCursor(xOff, 65 - 8, 4);
  421. M5.Lcd.setTextColor(CYAN, BLACK);
  422. M5.Lcd.printf("%02d:%02d\n", M5.Rtc.Hour % 12, M5.Rtc.Minute);
  423. M5.Lcd.setTextColor(WHITE, BLACK);
  424. zwM5StickC_UpdateBrightnessMeter();
  425. }
  426. #else
  427. #define zwM5StickC_UpdateBrightnessMeter()
  428. #define zwM5StickC_DrawBrightnessMeterBorder()
  429. #define zwM5StickC_UpdateBatteryDisplay()
  430. #endif
  431. #define PAGE_SIZE 4
  432. static int __dispPage = 0;
  433. static int __dispPages = 0;
  434. void tick(bool forceUpdate = false)
  435. {
  436. if (gConfig.pauseRefresh)
  437. if (!forceUpdate)
  438. return;
  439. zlog("Awake at us=%lu tick=%lld\n", micros(), gSecondsSinceBoot);
  440. #if M5STACKC
  441. M5.Lcd.fillScreen(TFT_BLACK);
  442. zwM5StickC_UpdateBatteryDisplay();
  443. M5.Lcd.setCursor(0, 0, 2);
  444. #endif
  445. for (DisplaySpec *w = (gDisplays + (__dispPage * PAGE_SIZE));
  446. (w - (gDisplays + (__dispPage * PAGE_SIZE))) < PAGE_SIZE && (w->clockPin != -1 && w->dioPin != -1);
  447. w++)
  448. updateDisplay(w);
  449. #if M5STACKC
  450. M5.Lcd.setTextColor(NAVY, BLACK);
  451. M5.Lcd.printf(" <%d>\n", __dispPage + 1);
  452. #endif
  453. _last_free = ESP.getFreeHeap();
  454. if (gConfig.deepSleepMode)
  455. {
  456. heartbeat();
  457. zlog("Deep-sleeping for %ds...\n", gConfig.refresh);
  458. Serial.flush();
  459. esp_sleep_enable_timer_wakeup(gConfig.refresh * 1e6);
  460. esp_deep_sleep_start();
  461. }
  462. }
  463. void IRAM_ATTR __isr()
  464. {
  465. portENTER_CRITICAL(&__isrMutex);
  466. ++__isrCount;
  467. portEXIT_CRITICAL(&__isrMutex);
  468. }
  469. static bool __lastHome = true;
  470. static bool __lastRst = true;
  471. static uint64_t __rstDebounce = 0;
  472. void loop()
  473. {
  474. if (__isrCount)
  475. {
  476. portENTER_CRITICAL(&__isrMutex);
  477. --__isrCount;
  478. portEXIT_CRITICAL(&__isrMutex);
  479. ++gSecondsSinceBoot;
  480. zwM5StickC_UpdateBatteryDisplay();
  481. }
  482. auto curHome = digitalRead(M5_BUTTON_HOME);
  483. auto curRst = digitalRead(M5_BUTTON_RST);
  484. bool forceTick = false;
  485. if (curHome != __lastHome)
  486. {
  487. if (curHome && !__lastHome)
  488. {
  489. if (__dispPages)
  490. {
  491. __dispPage = (__dispPage + 1) % (__dispPages + 1);
  492. }
  493. forceTick = true;
  494. }
  495. }
  496. if (curRst != __lastRst)
  497. {
  498. if (curRst && !__lastRst && (!__rstDebounce || (millis() - __rstDebounce > 250)))
  499. {
  500. __rstDebounce = millis();
  501. M5.Axp.ScreenBreath((gConfig.brightness = (gConfig.brightness + 1) % 8) + 7);
  502. zwM5StickC_UpdateBrightnessMeter();
  503. }
  504. }
  505. __lastHome = curHome;
  506. __lastRst = curRst;
  507. if (forceTick || (!(gSecondsSinceBoot % gConfig.refresh) && gLastRefreshTick != gSecondsSinceBoot))
  508. {
  509. if (forceTick)
  510. {
  511. #if M5STACKC
  512. M5.Lcd.fillScreen(TFT_BLACK);
  513. #endif
  514. }
  515. gLastRefreshTick = gSecondsSinceBoot;
  516. readConfigAndUserKeys();
  517. tick();
  518. heartbeat();
  519. }
  520. }
  521. void setup()
  522. {
  523. #if M5STACKC
  524. M5.begin();
  525. pinMode(M5_BUTTON_HOME, INPUT_PULLUP);
  526. pinMode(M5_BUTTON_RST, INPUT_PULLUP);
  527. zlog("Built for M5StickC\n");
  528. gConfig.publishLogs = true;
  529. gPublishLogsEmit = M5Stack_publish_logs_emit;
  530. #else
  531. pinMode(LED_BLTIN, OUTPUT);
  532. Serial.begin(SER_BAUD);
  533. #endif
  534. verifyProvisioning();
  535. if (!(gDisplays = zwdisplayInit(gHostname)))
  536. {
  537. dprint("Display init failed, halting forever\n");
  538. __haltOrCatchFire();
  539. }
  540. auto buildVariant = "";
  541. #if M5STACKC
  542. buildVariant = "-M5SC";
  543. M5.Lcd.setCursor(0, 0, 1);
  544. //zwM5StickC_DrawBitmap(10, 10, 8, 8, (uint16_t*)brightIcon);
  545. //delay(120000);
  546. #endif
  547. zlog("%s v" ZEROWATCH_VER "%s\n", gHostname.c_str(), buildVariant);
  548. auto dWalk = gDisplays;
  549. for (; dWalk->clockPin != -1 && dWalk->dioPin != -1; dWalk++);
  550. __dispPages = (int)((dWalk - gDisplays) / PAGE_SIZE) - (!((dWalk - gDisplays) % PAGE_SIZE) ? 1 : 0);
  551. if (!gConfig.deepSleepMode)
  552. {
  553. #if !M5STACKC
  554. auto verNum = String(ZEROWATCH_VER);
  555. verNum.replace(".", "");
  556. gDisplays[0].disp->showNumberDec(verNum.toInt(), true);
  557. delay(2000);
  558. #endif
  559. }
  560. if (!zwWiFiInit(gHostname.c_str(), gConfig))
  561. {
  562. dprint("WiFi init failed, halting forever\n");
  563. __haltOrCatchFire();
  564. }
  565. ZWRedisHostConfig redisConfig = {
  566. .host = EEPROMCFG_RedisHost,
  567. .port = EEPROMCFG_RedisPort,
  568. .password = EEPROMCFG_RedisPass};
  569. gRedis = new ZWRedis(gHostname, redisConfig);
  570. #define NUM_RETRIES 5
  571. int redisConnectRetries = NUM_RETRIES;
  572. float redisWaitRetryTime = 50;
  573. float redisWaitRetryBackoffMult = 1.37;
  574. int errnos[NUM_RETRIES];
  575. while (!gRedis->connect() && --redisConnectRetries)
  576. {
  577. // seen: ECONNABORTED (makes sense)
  578. errnos[NUM_RETRIES - (redisConnectRetries + 1)] = errno;
  579. zlog("Redis connect failed but %d retries left, waiting %0.2fs and trying again (m=%0.3f)\n",
  580. redisConnectRetries, redisWaitRetryTime, redisWaitRetryBackoffMult);
  581. redisWaitRetryTime *= redisWaitRetryBackoffMult;
  582. redisWaitRetryBackoffMult *= redisWaitRetryBackoffMult;
  583. delay(redisWaitRetryTime);
  584. }
  585. if (!redisConnectRetries)
  586. {
  587. zlog("ERROR: redis init failed!\n");
  588. __haltOrCatchFire();
  589. }
  590. if (redisConnectRetries != NUM_RETRIES)
  591. {
  592. String seenErrnos = "";
  593. for (int i = 0; i < NUM_RETRIES && errnos[i]; i++)
  594. seenErrnos += String(errnos[i]) + " ";
  595. zlog("Redis connection had to be retried %d times. Saw: %s\n",
  596. NUM_RETRIES - redisConnectRetries, seenErrnos.c_str());
  597. gRedis->logCritical("Redis connection had to be retried %d times. Saw: %s",
  598. NUM_RETRIES - redisConnectRetries, seenErrnos.c_str());
  599. }
  600. gBootCount = gRedis->incrementBootcount();
  601. zlog("Initialized! (debug %s)\n", gConfig.debug ? "on" : "off");
  602. zlog("Boot count: %lu\n", gBootCount);
  603. readConfigAndUserKeys();
  604. if (gConfig.debug && !gConfig.deepSleepMode)
  605. delay(5000);
  606. __isrTimer = timerBegin(0, 80, true);
  607. timerAttachInterrupt(__isrTimer, &__isr, true);
  608. timerAlarmWrite(__isrTimer, 1000000, true);
  609. timerAlarmEnable(__isrTimer);
  610. #if M5STACKC
  611. delay(gConfig.debug ? 10000 : 2000);
  612. gPublishLogsEmit = NULL;
  613. M5.Lcd.setCursor(0, 0, 2);
  614. M5.Lcd.fillScreen(TFT_BLACK);
  615. #endif
  616. gPublishLogsEmit = redis_publish_logs_emit;
  617. tick(true);
  618. }