ESP32 / ESP8266 & BME280 / SHT2x sensor with InfluxDB support
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.

ui.cpp 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /*
  2. * ui.cpp
  3. *
  4. * https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/Examples/Basics/2-TouchTest/2-TouchTest.ino
  5. * https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/Examples/Basics/4-BacklightControlTest/4-BacklightControlTest.ino
  6. *
  7. * ESP8266 / ESP32 Environmental Sensor
  8. *
  9. * ----------------------------------------------------------------------------
  10. * "THE BEER-WARE LICENSE" (Revision 42):
  11. * <xythobuz@xythobuz.de> wrote this file. As long as you retain this notice
  12. * you can do whatever you want with this stuff. If we meet some day, and you
  13. * think this stuff is worth it, you can buy me a beer in return. Thomas Buck
  14. * ----------------------------------------------------------------------------
  15. */
  16. #include <Arduino.h>
  17. #include <WiFi.h>
  18. #include "config.h"
  19. #include "mqtt.h"
  20. #include "ui.h"
  21. #ifdef FEATURE_UI
  22. #include <SPI.h>
  23. #include <XPT2046_Touchscreen.h>
  24. #include <TFT_eSPI.h>
  25. #define XPT2046_IRQ 36
  26. #define XPT2046_MOSI 32
  27. #define XPT2046_MISO 39
  28. #define XPT2046_CLK 25
  29. #define XPT2046_CS 33
  30. #define LEDC_CHANNEL_0 0
  31. #define LEDC_TIMER_12_BIT 12
  32. #define LEDC_BASE_FREQ 5000
  33. #define LDR_PIN 34
  34. #define BTN_PIN 0
  35. #define TOUCH_LEFT 180
  36. #define TOUCH_RIGHT 3750
  37. #define TOUCH_TOP 230
  38. #define TOUCH_BOTTOM 3800
  39. #define BTN_W 120
  40. #define BTN_H 60
  41. #define BTN_GAP 20
  42. #define BTNS_OFF_X ((LCD_WIDTH - (2 * BTN_W) - (1 * BTN_GAP)) / 2)
  43. #define BTNS_OFF_Y ((LCD_HEIGHT - (3 * BTN_H) - (2 * BTN_GAP)) / 2)
  44. #define INVERT_BOOL(x) (x) = !(x)
  45. #define LDR_CHECK_MS 1000
  46. #define MIN_TOUCH_DELAY_MS 200
  47. #define TOUCH_PRESSURE_MIN 200
  48. static SPIClass mySpi = SPIClass(HSPI);
  49. static XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ);
  50. static TFT_eSPI tft = TFT_eSPI();
  51. struct ui_status ui_status = {0};
  52. enum ui_pages {
  53. UI_START = 0,
  54. UI_LIVINGROOM1,
  55. UI_LIVINGROOM2,
  56. UI_BATHROOM,
  57. UI_INFO,
  58. UI_NUM_PAGES
  59. };
  60. static enum ui_pages ui_page = UI_START;
  61. static bool is_touched = false;
  62. static unsigned long last_ldr = 0;
  63. static int ldr_value = 0;
  64. static unsigned long last_touch_time = 0;
  65. static TS_Point touchToScreen(TS_Point p) {
  66. p.x = map(p.x, TOUCH_LEFT, TOUCH_RIGHT, 0, LCD_WIDTH);
  67. p.y = map(p.y, TOUCH_TOP, TOUCH_BOTTOM, 0, LCD_HEIGHT);
  68. return p;
  69. }
  70. static void ledcAnalogWrite(uint8_t channel, uint32_t value, uint32_t valueMax = 255) {
  71. uint32_t duty = (4095 / valueMax) * min(value, valueMax);
  72. ledcWrite(channel, duty);
  73. }
  74. static void draw_button(const char *name, uint32_t x, uint32_t y, uint32_t color) {
  75. tft.fillRect(x - BTN_W / 2, y - BTN_H / 2, BTN_W, BTN_H, color);
  76. tft.setTextDatum(MC_DATUM); // middle center
  77. tft.drawString(name, x, y, 2);
  78. }
  79. static void draw_livingroom1(void) {
  80. // 1
  81. draw_button("Lights Corner",
  82. BTNS_OFF_X + BTN_W / 2,
  83. BTNS_OFF_Y + BTN_H / 2,
  84. ui_status.light_corner ? TFT_GREEN : TFT_RED);
  85. // 2
  86. draw_button("Lights Workspace",
  87. BTNS_OFF_X + BTN_W / 2,
  88. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  89. ui_status.light_workspace ? TFT_GREEN : TFT_RED);
  90. // 3
  91. draw_button("Lights Sink",
  92. BTNS_OFF_X + BTN_W / 2,
  93. BTNS_OFF_Y + BTN_H / 2 + (BTN_H + BTN_GAP) * 2,
  94. ui_status.light_sink ? TFT_GREEN : TFT_RED);
  95. // 4
  96. draw_button("Sound Amp.",
  97. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  98. BTNS_OFF_Y + BTN_H / 2,
  99. ui_status.sound_amplifier ? TFT_GREEN : TFT_RED);
  100. // 5
  101. bool on = ui_status.light_corner || ui_status.light_sink || ui_status.light_workspace
  102. || ui_status.light_amp || ui_status.light_bench || ui_status.light_box
  103. || ui_status.light_kitchen || ui_status.light_pc;
  104. draw_button(on ? "All Lights Off" : "Wake Up Lights",
  105. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  106. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  107. TFT_MAGENTA);
  108. }
  109. static void draw_livingroom2(void) {
  110. // 1
  111. draw_button("Lights PC",
  112. BTNS_OFF_X + BTN_W / 2,
  113. BTNS_OFF_Y + BTN_H / 2,
  114. ui_status.light_pc ? TFT_GREEN : TFT_RED);
  115. // 2
  116. draw_button("Lights Bench",
  117. BTNS_OFF_X + BTN_W / 2,
  118. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  119. ui_status.light_bench ? TFT_GREEN : TFT_RED);
  120. // 3
  121. draw_button("Lights Kitchen",
  122. BTNS_OFF_X + BTN_W / 2,
  123. BTNS_OFF_Y + BTN_H / 2 + (BTN_H + BTN_GAP) * 2,
  124. ui_status.light_kitchen ? TFT_GREEN : TFT_RED);
  125. // 4
  126. draw_button("Lights Amp.",
  127. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  128. BTNS_OFF_Y + BTN_H / 2,
  129. ui_status.light_amp ? TFT_GREEN : TFT_RED);
  130. // 5
  131. draw_button("Lights Box",
  132. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  133. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  134. ui_status.light_box ? TFT_GREEN : TFT_RED);
  135. }
  136. static void draw_bathroom(void) {
  137. // 1
  138. draw_button("Bath Lights Auto",
  139. BTNS_OFF_X + BTN_W / 2,
  140. BTNS_OFF_Y + BTN_H / 2,
  141. ui_status.bathroom_lights == BATH_LIGHT_NONE ? TFT_GREEN : TFT_RED);
  142. // 2
  143. draw_button("Bath Lights Big",
  144. BTNS_OFF_X + BTN_W / 2,
  145. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  146. ui_status.bathroom_lights == BATH_LIGHT_BIG ? TFT_GREEN : TFT_RED);
  147. // 4
  148. draw_button("Bath Lights Off",
  149. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  150. BTNS_OFF_Y + BTN_H / 2,
  151. ui_status.bathroom_lights == BATH_LIGHT_OFF ? TFT_GREEN : TFT_RED);
  152. // 5
  153. draw_button("Bath Lights Small",
  154. BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP,
  155. BTNS_OFF_Y + BTN_H / 2 + BTN_H + BTN_GAP,
  156. ui_status.bathroom_lights == BATH_LIGHT_SMALL ? TFT_GREEN : TFT_RED);
  157. }
  158. static void draw_info(void) {
  159. tft.fillScreen(TFT_BLACK);
  160. tft.setTextDatum(TC_DATUM); // top center
  161. tft.drawString(ESP_PLATFORM_NAME " " NAME_OF_FEATURE " V" ESP_ENV_VERSION, LCD_WIDTH / 2, 0, 2);
  162. tft.drawString("by xythobuz.de", LCD_WIDTH / 2, 16, 2);
  163. tft.setTextDatum(TL_DATUM); // top left
  164. tft.drawString("Build Date: " __DATE__, 0, 40 + 16 * 0, 1);
  165. tft.drawString("Build Time: " __TIME__, 0, 40 + 16 * 1, 1);
  166. tft.drawString("Location: " SENSOR_LOCATION, 0, 40 + 16 * 2, 1);
  167. tft.drawString("ID: " SENSOR_ID, 0, 40 + 16 * 3, 1);
  168. tft.drawString("MAC: " + String(WiFi.macAddress()), 0, 40 + 16 * 4, 1);
  169. tft.drawString("Free heap: " + String(ESP.getFreeHeap() / 1024.0f) + "k", 0, 40 + 16 * 5, 1);
  170. tft.drawString("Free sketch space: " + String(ESP.getFreeSketchSpace() / 1024.0f) + "k", 0, 40 + 16 * 6, 1);
  171. tft.drawString("Flash chip size: " + String(ESP.getFlashChipSize() / 1024.0f) + "k", 0, 40 + 16 * 7, 1);
  172. tft.drawString("Uptime: " + String(millis() / 1000) + "sec", 0, 40 + 16 * 8, 1);
  173. tft.drawString("IPv4: " + WiFi.localIP().toString(), 0, 40 + 16 * 9, 1);
  174. tft.drawString("IPv6: " + WiFi.localIPv6().toString(), 0, 40 + 16 * 10, 1);
  175. tft.drawString("Hostname: " + String(SENSOR_HOSTNAME_PREFIX) + String(SENSOR_ID), 0, 40 + 16 * 11, 1);
  176. tft.drawString("LDR: " + String(ldr_value), 0, 40 + 16 * 12, 1);
  177. }
  178. void ui_init(void) {
  179. mySpi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  180. ts.begin(mySpi);
  181. ts.setRotation(1);
  182. tft.init();
  183. tft.setRotation(1);
  184. ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_12_BIT);
  185. ledcAttachPin(TFT_BL, LEDC_CHANNEL_0);
  186. ledcAnalogWrite(LEDC_CHANNEL_0, 255);
  187. pinMode(BTN_PIN, INPUT);
  188. pinMode(LDR_PIN, ANALOG);
  189. analogSetAttenuation(ADC_0db);
  190. analogReadResolution(12);
  191. analogSetPinAttenuation(LDR_PIN, ADC_0db);
  192. ldr_value = analogRead(LDR_PIN);
  193. ui_progress(UI_INIT);
  194. }
  195. static void ui_draw_menu(void) {
  196. switch (ui_page) {
  197. case UI_START:
  198. tft.fillScreen(TFT_BLACK);
  199. ui_page = UI_LIVINGROOM1;
  200. // fall-through
  201. case UI_LIVINGROOM1:
  202. draw_livingroom1();
  203. break;
  204. case UI_LIVINGROOM2:
  205. draw_livingroom2();
  206. break;
  207. case UI_BATHROOM:
  208. draw_bathroom();
  209. break;
  210. case UI_INFO:
  211. draw_info();
  212. return; // no next button
  213. default:
  214. ui_page = UI_START;
  215. ui_draw_menu();
  216. return;
  217. }
  218. draw_button("Next...", BTNS_OFF_X + BTN_W / 2 + BTN_W + BTN_GAP, BTNS_OFF_Y + BTN_H / 2 + (BTN_H + BTN_GAP) * 2, TFT_CYAN);
  219. }
  220. void ui_progress(enum ui_state state) {
  221. int x = LCD_WIDTH / 2;
  222. int y = LCD_HEIGHT / 2;
  223. int fontSize = 2;
  224. switch (state) {
  225. case UI_INIT: {
  226. tft.fillScreen(TFT_BLACK);
  227. tft.setTextDatum(MC_DATUM); // middle center
  228. tft.drawString("Initializing ESP-ENV", x, y - 32, fontSize);
  229. tft.drawString("xythobuz.de", x, y, fontSize);
  230. } break;
  231. case UI_WIFI_CONNECT: {
  232. tft.setTextDatum(MC_DATUM); // middle center
  233. tft.drawString("Connecting to '" WIFI_SSID "'", x, y + 32, fontSize);
  234. } break;
  235. case UI_WIFI_CONNECTING: {
  236. static int n = 0;
  237. const char anim[] = { '\\', '|', '/', '-' };
  238. n++;
  239. if (n >= sizeof(anim)) {
  240. n = 0;
  241. }
  242. char s[2] = { anim[n], '\0' };
  243. tft.drawCentreString(s, x, y + 64, fontSize);
  244. } break;
  245. case UI_WIFI_CONNECTED: {
  246. tft.setTextDatum(MC_DATUM); // middle center
  247. tft.drawString("Connected!", x, y + 64, fontSize);
  248. } break;
  249. case UI_READY: {
  250. ui_page = UI_START;
  251. ui_draw_menu();
  252. } break;
  253. case UI_UPDATE: {
  254. ui_draw_menu();
  255. } break;
  256. }
  257. }
  258. void ui_run(void) {
  259. unsigned long now = millis();
  260. if (!digitalRead(BTN_PIN)) {
  261. ui_page = UI_INFO;
  262. }
  263. if (now >= (last_ldr + LDR_CHECK_MS)) {
  264. last_ldr = now;
  265. int ldr = analogRead(LDR_PIN);
  266. // TODO lowpass?
  267. //ldr_value = (ldr_value * 0.9f) + (ldr * 0.1f);
  268. ldr_value = ldr;
  269. if (ui_page == UI_INFO) {
  270. ui_draw_menu();
  271. }
  272. }
  273. bool touched = ts.tirqTouched() && ts.touched();
  274. TS_Point p;
  275. if (touched) {
  276. p = touchToScreen(ts.getPoint());
  277. // minimum pressure
  278. if (p.z < TOUCH_PRESSURE_MIN) {
  279. touched = false;
  280. }
  281. }
  282. if (touched && (!is_touched)) {
  283. is_touched = true;
  284. last_touch_time = millis();
  285. if (ui_page == UI_INFO) {
  286. // switch to next page, skip init and info screen
  287. do {
  288. ui_page = (enum ui_pages)((ui_page + 1) % UI_NUM_PAGES);
  289. } while ((ui_page == UI_START) || (ui_page == UI_INFO));
  290. tft.fillScreen(TFT_BLACK);
  291. ui_draw_menu();
  292. return;
  293. }
  294. if ((p.x >= BTNS_OFF_X) && (p.x <= BTNS_OFF_X + BTN_W) && (p.y >= BTNS_OFF_Y) && (p.y <= BTNS_OFF_Y + BTN_H)) {
  295. // 1
  296. if (ui_page == UI_LIVINGROOM1) {
  297. INVERT_BOOL(ui_status.light_corner);
  298. } else if (ui_page == UI_LIVINGROOM2) {
  299. INVERT_BOOL(ui_status.light_pc);
  300. } else if (ui_page == UI_BATHROOM) {
  301. ui_status.bathroom_lights = BATH_LIGHT_NONE;
  302. }
  303. writeMQTT_UI();
  304. } else if ((p.x >= BTNS_OFF_X) && (p.x <= BTNS_OFF_X + BTN_W) && (p.y >= (BTNS_OFF_Y + BTN_H + BTN_GAP)) && (p.y <= (BTNS_OFF_Y + BTN_H + BTN_GAP + BTN_H))) {
  305. // 2
  306. if (ui_page == UI_LIVINGROOM1) {
  307. INVERT_BOOL(ui_status.light_workspace);
  308. } else if (ui_page == UI_LIVINGROOM2) {
  309. INVERT_BOOL(ui_status.light_bench);
  310. } else if (ui_page == UI_BATHROOM) {
  311. ui_status.bathroom_lights = BATH_LIGHT_BIG;
  312. }
  313. writeMQTT_UI();
  314. } else if ((p.x >= BTNS_OFF_X) && (p.x <= BTNS_OFF_X + BTN_W) && (p.y >= (BTNS_OFF_Y + BTN_H * 2 + BTN_GAP * 2)) && (p.y <= (BTNS_OFF_Y + BTN_H * 2 + BTN_GAP * 2 + BTN_H))) {
  315. // 3
  316. if (ui_page == UI_LIVINGROOM1) {
  317. INVERT_BOOL(ui_status.light_sink);
  318. } else if (ui_page == UI_LIVINGROOM2) {
  319. INVERT_BOOL(ui_status.light_kitchen);
  320. }
  321. writeMQTT_UI();
  322. } else if ((p.x >= BTNS_OFF_X + BTN_W + BTN_GAP) && (p.x <= BTNS_OFF_X + BTN_W + BTN_GAP + BTN_W) && (p.y >= BTNS_OFF_Y) && (p.y <= BTNS_OFF_Y + BTN_H)) {
  323. // 4
  324. if (ui_page == UI_LIVINGROOM1) {
  325. INVERT_BOOL(ui_status.sound_amplifier);
  326. } else if (ui_page == UI_LIVINGROOM2) {
  327. INVERT_BOOL(ui_status.light_amp);
  328. } else if (ui_page == UI_BATHROOM) {
  329. ui_status.bathroom_lights = BATH_LIGHT_OFF;
  330. }
  331. writeMQTT_UI();
  332. } else if ((p.x >= BTNS_OFF_X + BTN_W + BTN_GAP) && (p.x <= BTNS_OFF_X + BTN_W + BTN_GAP + BTN_W) && (p.y >= (BTNS_OFF_Y + BTN_H + BTN_GAP)) && (p.y <= (BTNS_OFF_Y + BTN_H + BTN_GAP + BTN_H))) {
  333. // 5
  334. if (ui_page == UI_LIVINGROOM1) {
  335. bool on = ui_status.light_corner || ui_status.light_sink || ui_status.light_workspace
  336. || ui_status.light_amp || ui_status.light_bench || ui_status.light_box
  337. || ui_status.light_kitchen || ui_status.light_pc;
  338. if (on) {
  339. ui_status.light_amp = false;
  340. ui_status.light_kitchen = false;
  341. ui_status.light_bench= false;
  342. ui_status.light_workspace = false;
  343. ui_status.light_pc = false;
  344. ui_status.light_corner = false;
  345. ui_status.light_box = false;
  346. ui_status.light_sink = false;
  347. } else {
  348. ui_status.light_corner = true;
  349. ui_status.light_sink = true;
  350. }
  351. } else if (ui_page == UI_LIVINGROOM2) {
  352. INVERT_BOOL(ui_status.light_box);
  353. } else if (ui_page == UI_BATHROOM) {
  354. ui_status.bathroom_lights = BATH_LIGHT_SMALL;
  355. }
  356. writeMQTT_UI();
  357. } else if ((p.x >= BTNS_OFF_X + BTN_W + BTN_GAP) && (p.x <= BTNS_OFF_X + BTN_W + BTN_GAP + BTN_W) && (p.y >= (BTNS_OFF_Y + BTN_H * 2 + BTN_GAP * 2)) && (p.y <= (BTNS_OFF_Y + BTN_H * 2 + BTN_GAP * 2 + BTN_H))) {
  358. // switch to next page, skip init and info screen
  359. do {
  360. ui_page = (enum ui_pages)((ui_page + 1) % UI_NUM_PAGES);
  361. } while ((ui_page == UI_START) || (ui_page == UI_INFO));
  362. tft.fillScreen(TFT_BLACK);
  363. }
  364. ui_draw_menu();
  365. } else if ((!touched) && is_touched && ((now - last_touch_time) >= MIN_TOUCH_DELAY_MS)) {
  366. is_touched = false;
  367. }
  368. }
  369. #endif // FEATURE_UI