WIFI_NTP_CLOCK.ino 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. #include <Lixie_II.h> // https://github.com/connornishijima/Lixie_II
  2. #include <ESP8266WiFi.h> // https://github.com/esp8266/Arduino
  3. #include <DNSServer.h> // |
  4. #include <ESP8266WebServer.h> // |
  5. #include <WiFiUdp.h> // |
  6. #include <FS.h> // <
  7. #include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
  8. #include <NTPClient.h> // https://github.com/arduino-libraries/NTPClient
  9. #include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson
  10. /*
  11. Lixie II NTP Clock for ESP8266
  12. by Connor Nishijima (November 2nd, 2019)
  13. This example relies on an ESP8266/ESP32 controller,
  14. which is used to pull UTC time from an NTP server over WiFi.
  15. REQUIRED HARDWARE ---------------------------------------------------------------------
  16. It requires two external buttons to fully function: (defined as HUE_BUTTON and HOUR_BUTTON below)
  17. one for changing color/modes, and one for setting the UTC offset (timezone)
  18. INSTRUCTIONS --------------------------------------------------------------------------
  19. Upon booting, it will failt to connect to WiFi as it doesn't yet know your
  20. credentials. It will then host it's own WiFi access point named "LIXIE CONFIG"
  21. that you can connect your phone to.
  22. Once connected, go to http://192.168.4.1/ in your phone's browser, and you
  23. will see a menu that will let you send the WiFi details down to the clock,
  24. to be remembered from this point forward. (Even through power cycles!)
  25. A short tap of the HUE button will change color modes, and holding it down will
  26. start to cycle the displays through the color wheel. Release the HUE button
  27. when it gets to a color you like.
  28. A short tap of the HOUR button will increase the UTC offset by 1, (wrapping back
  29. to -12 after +12) and a long tap toggles between 12 and 24-hour mode.
  30. Holding the HOUR button down during power up will erase WiFi credentials and let
  31. you access the WiFi configuration page again.
  32. OPTIONAL HARDWARE ---------------------------------------------------------------------
  33. - A piezo buzzer or small speaker between the BUZZER pin and GND
  34. - Two SPST / SPDT switches from both the COLOR_CYCLE and NIGHT_DIMMING pins, that
  35. optionally tie those pins to GND
  36. The buzzer is just for feedback when buttons are pressed, and the two switches
  37. enable automated color cycling on modes that support it (full cycle every 5 minutes)
  38. and nighttime dimming to 40% brightness from 9PM (21:00) to 6AM (06:00)
  39. If you don't have a buzzer or switches on-hand, just ignore the buzzer and
  40. short the COLOR_CYCLE / NIGHT_DIMMING pins to GND if you want to enable those features.
  41. MAKE SURE SPIFFS IS ENABLED in Tools > Flash Size
  42. Enjoy!
  43. */
  44. // USER SETTINGS //////////////////////////////////////////////////////////////////
  45. #define DATA_PIN D6 // Lixie DIN connects to this pin
  46. #define NUM_DIGITS 4
  47. #define SIX_DIGIT_CLOCK false // 6 or 4-digit clock? (6 has seconds shown)
  48. #define HOUR_BUTTON D7 // These are pulled up internally, and should be
  49. #define HUE_BUTTON D2 // tied to GND through momentary switches
  50. #define BUZZER D8 // OPTIONAL
  51. #define COLOR_CYCLE D1 // OPTIONAL
  52. #define NIGHT_DIMMING D5 // OPTIONAL
  53. ///////////////////////////////////////////////////////////////////////////////////
  54. Lixie_II lix(DATA_PIN, NUM_DIGITS);
  55. WiFiUDP ntp_UDP;
  56. NTPClient time_client(ntp_UDP, "pool.ntp.org", 3600, 60000);
  57. #define SECONDS_PER_HOUR 3600
  58. bool time_found = false;
  59. struct conf {
  60. int16_t time_zone_shift = 0;
  61. uint8_t hour_12_mode = false;
  62. uint8_t base_hue = 0;
  63. uint8_t current_mode = 0;
  64. };
  65. conf clock_config; // <- global configuration object
  66. float base_hue_f = 0;
  67. uint32_t settings_last_update = 0;
  68. bool settings_changed = false;
  69. const char* settings_file = "/settings.json";
  70. uint32_t t_now = 0;
  71. uint8_t last_seconds = 0;
  72. #define STABLE 0
  73. #define RISE 1
  74. #define FALL 2
  75. uint8_t hh = 0;
  76. uint8_t mm = 0;
  77. uint8_t ss = 0;
  78. bool color_cycle_state = HIGH;
  79. bool night_dimming_state = HIGH;
  80. bool hour_button_state = HIGH;
  81. bool hue_button_state = HIGH;
  82. bool hour_button_state_last = HIGH;
  83. bool hue_button_state_last = HIGH;
  84. uint8_t hour_button_edge = STABLE;
  85. uint8_t hue_button_edge = STABLE;
  86. uint32_t hour_button_last_hit = 0;
  87. uint32_t hue_button_last_hit = 0;
  88. uint32_t hour_button_start = 0;
  89. uint16_t hour_button_wait = 1000;
  90. bool hour_mode_started = false;
  91. uint32_t hue_button_start = 0;
  92. uint8_t button_debounce_ms = 100;
  93. uint16_t hue_countdown = 255;
  94. uint16_t hue_push_wait = 500;
  95. #define NUM_MODES 7
  96. #define MODE_SOLID 0
  97. #define MODE_GRADIENT 1
  98. #define MODE_DUAL 2
  99. #define MODE_NIXIE 3
  100. #define MODE_INCANDESCENT 4
  101. #define MODE_VFD 5
  102. #define MODE_WHITE 6
  103. void setup() {
  104. Serial.begin(115200);
  105. init_fs();
  106. load_settings();
  107. init_displays();
  108. init_buttons();
  109. init_wifi();
  110. init_ntp();
  111. }
  112. void loop() {
  113. run_clock();
  114. yield();
  115. }
  116. void run_clock() {
  117. t_now = millis();
  118. time_client.update();
  119. if (time_client.getSeconds() != last_seconds) {
  120. show_time();
  121. }
  122. if (t_now % 20 == 0) { // 50 FPS
  123. color_for_mode();
  124. check_buttons();
  125. lix.run();
  126. }
  127. if (t_now - settings_last_update > 5000 && settings_changed == true) {
  128. settings_changed = false;
  129. save_settings();
  130. }
  131. }
  132. void save_settings() {
  133. // Delete existing file, otherwise the configuration is appended to the file
  134. SPIFFS.remove(settings_file);
  135. // Open file for writing
  136. File file = SPIFFS.open(settings_file, "w+");
  137. if (!file) {
  138. Serial.println(F("Failed to create config file"));
  139. return;
  140. }
  141. else {
  142. Serial.println("Config file opened");
  143. }
  144. // Allocate a temporary JsonDocument
  145. StaticJsonDocument<512> doc_out;
  146. // Set the values in the document
  147. doc_out["base_hue"] = clock_config.base_hue;
  148. doc_out["current_mode"] = clock_config.current_mode;
  149. doc_out["time_zone_shift"] = clock_config.time_zone_shift;
  150. doc_out["hour_12_mode"] = clock_config.hour_12_mode;
  151. // Serialize JSON to file
  152. if (serializeJson(doc_out, file) == 0) {
  153. Serial.println(F("Failed to write to config file"));
  154. }
  155. else {
  156. Serial.println("Config Saved!");
  157. }
  158. // Close the file
  159. file.close();
  160. }
  161. void load_settings() {
  162. // Open file for reading
  163. File file = SPIFFS.open(settings_file, "r");
  164. // Allocate a temporary JsonDocument
  165. StaticJsonDocument<512> doc_in;
  166. // Deserialize the JSON document
  167. DeserializationError error = deserializeJson(doc_in, file);
  168. if (error) {
  169. Serial.println(F("Failed to read file, using default configuration"));
  170. }
  171. else {
  172. Serial.println("Config file opened");
  173. Serial.println("Config Loaded!");
  174. }
  175. // Copy values from the JsonDocument to the Config
  176. clock_config.base_hue = doc_in["base_hue"];
  177. base_hue_f = doc_in["base_hue"];
  178. clock_config.current_mode = doc_in["current_mode"];
  179. clock_config.time_zone_shift = doc_in["time_zone_shift"];
  180. clock_config.hour_12_mode = doc_in["hour_12_mode"];
  181. // Close the file (Curiously, File's destructor doesn't close the file)
  182. file.close();
  183. }
  184. void show_time() {
  185. hh = time_client.getHours();
  186. mm = time_client.getMinutes();
  187. ss = time_client.getSeconds();
  188. if (hh < 6 || hh >= 21) { // Dim overnight from 9PM to 6AM (21:00 to 06:00)
  189. if (night_dimming_state == HIGH) { // But only if dimming is enabled
  190. lix.brightness(0.4);
  191. }
  192. else { // If not enabled, full brightness
  193. lix.brightness(1.0);
  194. }
  195. }
  196. else { // Or if not in the overnight time window, full brightness
  197. lix.brightness(1.0);
  198. }
  199. // 12 hour format conversion
  200. if (clock_config.hour_12_mode == true) {
  201. if (hh > 12) {
  202. hh -= 12;
  203. }
  204. if (hh == 0) {
  205. hh = 12;
  206. }
  207. }
  208. uint32_t t_lixie = 1000000; // "1000000" is used to get zero-padding on hours digits
  209. // This turns a time of 22:34:57 into the integer (1)223457, whose leftmost numeral (1) will not be shown
  210. t_lixie += (hh * 10000);
  211. t_lixie += (mm * 100);
  212. t_lixie += ss;
  213. if (!SIX_DIGIT_CLOCK) {
  214. t_lixie /= 100; // Eliminate second places if using a 4 digit clock
  215. }
  216. lix.write(t_lixie); // Update numerals
  217. if (!time_found) { // Cues initial fade in
  218. time_found = true;
  219. lix.fade_in();
  220. }
  221. Serial.print("TIME: ");
  222. Serial.println((hh * 10000)+(mm * 100)+ss);
  223. last_seconds = ss;
  224. }
  225. void color_for_mode() {
  226. if (color_cycle_state == HIGH) {
  227. base_hue_f += 0.017; // Fully cycles the color wheel every 5 minutes
  228. }
  229. clock_config.base_hue = base_hue_f;
  230. uint8_t temp_hue = clock_config.base_hue + hue_countdown;
  231. if (clock_config.current_mode == MODE_SOLID) {
  232. lix.color_all(ON, CHSV(temp_hue, 255, 255));
  233. lix.color_all(OFF, CRGB(0, 0, 0));
  234. }
  235. else if (clock_config.current_mode == MODE_GRADIENT) {
  236. lix.gradient_rgb(ON, CHSV(temp_hue, 255, 255), CHSV(temp_hue + 90, 255, 255));
  237. lix.color_all(OFF, CRGB(0, 0, 0));
  238. }
  239. else if (clock_config.current_mode == MODE_DUAL) {
  240. lix.color_all_dual(ON, CHSV(temp_hue, 255, 255), CHSV(temp_hue + 90, 255, 255));
  241. lix.color_all(OFF, CRGB(0, 0, 0));
  242. }
  243. else if (clock_config.current_mode == MODE_NIXIE) {
  244. lix.color_all(ON, CRGB(255, 70, 7));
  245. CRGB col_off = CRGB(0, 100, 255);
  246. const uint8_t nixie_aura_level = 8;
  247. col_off.r *= (nixie_aura_level / 255.0);
  248. col_off.g *= (nixie_aura_level / 255.0);
  249. col_off.b *= (nixie_aura_level / 255.0);
  250. lix.color_all(OFF, col_off);
  251. }
  252. else if (clock_config.current_mode == MODE_INCANDESCENT) {
  253. lix.color_all(ON, CRGB(255, 100, 25));
  254. lix.color_all(OFF, CRGB(0, 0, 0));
  255. }
  256. else if (clock_config.current_mode == MODE_VFD) {
  257. lix.color_all(ON, CRGB(100, 255, 100));
  258. lix.color_all(OFF, CRGB(0, 0, 0));
  259. }
  260. else if (clock_config.current_mode == MODE_WHITE) {
  261. lix.color_all(ON, CRGB(255, 255, 255));
  262. }
  263. }
  264. void check_buttons() {
  265. hour_button_state = digitalRead(HOUR_BUTTON);
  266. hue_button_state = digitalRead(HUE_BUTTON);
  267. color_cycle_state = !digitalRead(COLOR_CYCLE);
  268. night_dimming_state = !digitalRead(NIGHT_DIMMING);
  269. if (hour_button_state > hour_button_state_last) {
  270. hour_button_edge = RISE;
  271. }
  272. else if (hour_button_state < hour_button_state_last) {
  273. if (t_now - hour_button_last_hit >= button_debounce_ms) {
  274. hour_button_last_hit = t_now;
  275. hour_button_edge = FALL;
  276. }
  277. }
  278. else {
  279. hour_button_edge = STABLE;
  280. }
  281. if (hue_button_state > hue_button_state_last) {
  282. hue_button_edge = RISE;
  283. }
  284. else if (hue_button_state < hue_button_state_last) {
  285. if (t_now - hue_button_last_hit >= button_debounce_ms) {
  286. hue_button_last_hit = t_now;
  287. hue_button_edge = FALL;
  288. }
  289. }
  290. else {
  291. hue_button_edge = STABLE;
  292. }
  293. parse_buttons();
  294. hour_button_state_last = hour_button_state;
  295. hue_button_state_last = hue_button_state;
  296. }
  297. void parse_buttons() {
  298. if (hour_button_edge == FALL) { // PRESS STARTED
  299. hour_button_start = t_now;
  300. }
  301. else if (hour_button_edge == RISE) { // PRESS ENDED
  302. uint16_t hour_button_duration = t_now - hour_button_start;
  303. if (hour_button_duration < hour_button_wait) { // RELEASED QUICKLY
  304. Serial.println("UP");
  305. clock_config.time_zone_shift += 1;
  306. if (clock_config.time_zone_shift >= 12) {
  307. clock_config.time_zone_shift = -12;
  308. }
  309. time_client.setTimeOffset(clock_config.time_zone_shift * SECONDS_PER_HOUR);
  310. hh = time_client.getHours();
  311. if (hh == 0) {
  312. beep(1000, 100);
  313. }
  314. else {
  315. beep(2000, 100);
  316. }
  317. show_time();
  318. update_settings();
  319. }
  320. else { // RELEASED AFTER LONG PRESS
  321. hour_mode_started = false;
  322. }
  323. }
  324. if (hue_button_edge == FALL) { // PRESS STARTED
  325. Serial.println("HUE");
  326. hue_button_start = t_now;
  327. }
  328. else if (hue_button_edge == RISE) { // PRESS ENDED
  329. uint16_t hue_button_duration = t_now - hue_button_start;
  330. if (hue_button_duration < hue_push_wait) { // RELEASED QUICKLY
  331. Serial.println("NEXT MODE");
  332. clock_config.current_mode++;
  333. if (clock_config.current_mode >= NUM_MODES) {
  334. clock_config.current_mode = 0;
  335. beep(1000, 100);
  336. }
  337. else {
  338. beep(2000, 100);
  339. }
  340. hue_countdown = 127;
  341. update_settings();
  342. }
  343. }
  344. if (hue_button_state == LOW) { // CURRENTLY PRESSING
  345. uint16_t hue_button_duration = t_now - hue_button_start;
  346. if (hue_button_duration >= hue_push_wait) {
  347. base_hue_f++;
  348. Serial.print("HUE: ");
  349. Serial.println(base_hue_f);
  350. update_settings();
  351. }
  352. }
  353. if (hour_button_state == LOW) { // CURRENTLY PRESSING
  354. uint16_t hour_button_duration = t_now - hour_button_start;
  355. if (hour_button_duration >= hour_button_wait && hour_mode_started == false) {
  356. hour_mode_started = true;
  357. Serial.println("CHANGE HOUR MODE");
  358. beep(4000, 250);
  359. clock_config.hour_12_mode = !clock_config.hour_12_mode;
  360. update_settings();
  361. }
  362. }
  363. if (hue_countdown < 255) {
  364. hue_countdown += 6;
  365. if (hue_countdown > 255) {
  366. hue_countdown = 255;
  367. }
  368. }
  369. }
  370. void update_settings() {
  371. settings_changed = true;
  372. settings_last_update = t_now;
  373. }
  374. void enter_config_mode() {
  375. lix.write(808080);
  376. lix.color_all(ON, CRGB(64, 0, 255));
  377. beep_dual(2000, 1000, 500);
  378. }
  379. void config_mode_callback (WiFiManager *myWiFiManager) {
  380. enter_config_mode();
  381. Serial.println("Entered config mode");
  382. Serial.println(WiFi.softAPIP());
  383. //if you used auto generated SSID, print it
  384. Serial.println(myWiFiManager->getConfigPortalSSID());
  385. }
  386. void init_wifi() {
  387. WiFiManager wifiManager;
  388. wifiManager.setAPCallback(config_mode_callback);
  389. wifiManager.setTimeout(300); // Five minutes
  390. if (digitalRead(HOUR_BUTTON) == LOW) {
  391. wifiManager.resetSettings();
  392. enter_config_mode();
  393. }
  394. else {
  395. beep(2000, 100);
  396. }
  397. while (!wifiManager.autoConnect("LIXIE CONFIG")) {
  398. lix.sweep_color(CRGB(0, 255, 127), 20, 3, false);
  399. lix.clear(); // Removes "8"s before fading in the time
  400. }
  401. beep_dual(1000, 2000, 100);
  402. lix.sweep_color(CRGB(0, 255, 127), 20, 3, false);
  403. lix.clear(); // Removes "8"s before fading in the time
  404. color_for_mode();
  405. }
  406. void init_ntp() {
  407. time_client.begin();
  408. time_client.setTimeOffset(clock_config.time_zone_shift * SECONDS_PER_HOUR);
  409. }
  410. void init_fs() {
  411. Serial.print("SPIFFS Initialize....");
  412. if (SPIFFS.begin()) {
  413. Serial.println("ok");
  414. }
  415. else {
  416. Serial.println("failed");
  417. }
  418. }
  419. void init_buttons() {
  420. pinMode(HOUR_BUTTON, INPUT_PULLUP);
  421. pinMode(HUE_BUTTON, INPUT_PULLUP);
  422. pinMode(BUZZER, OUTPUT);
  423. pinMode(COLOR_CYCLE, INPUT_PULLUP);
  424. pinMode(NIGHT_DIMMING, INPUT_PULLUP);
  425. }
  426. void init_displays() {
  427. lix.begin();
  428. lix.max_power(5, 1000);
  429. lix.write(888888);
  430. }
  431. void beep(uint16_t freq, uint16_t len) {
  432. uint32_t period = (F_CPU / freq) / 2;
  433. uint32_t cycle_ms = F_CPU / 1000;
  434. uint32_t t_start = ESP.getCycleCount();
  435. uint32_t last_flip = t_start;
  436. uint32_t t_end = t_start + (len * cycle_ms);
  437. uint32_t t_now = t_start;
  438. bool state = LOW;
  439. while (t_now < t_end) {
  440. t_now = ESP.getCycleCount();
  441. if (t_now - last_flip >= period) {
  442. last_flip += period;
  443. state = !state;
  444. if (state) {
  445. GPOS = (1 << BUZZER);
  446. }
  447. else {
  448. GPOC = (1 << BUZZER);
  449. }
  450. }
  451. }
  452. digitalWrite(BUZZER, LOW);
  453. }
  454. void beep_dual(uint16_t del1, uint16_t del2, uint16_t len) {
  455. beep(del1, len);
  456. beep(del2, len);
  457. }