WIFI_NTP_CLOCK.ino 15 KB

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