diff --git a/.gitignore b/.gitignore index 89cc49c..fb3ca12 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +.vscode/settings.json diff --git a/platformio.ini b/platformio.ini index f287453..8784f3e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -17,6 +17,7 @@ lib_deps = https://github.com/m5stack/M5EPD https://github.com/arduino-libraries/NTPClient https://github.com/tobozo/YAMLDuino + https://github.com/josephlarralde/ArduinoEventEmitter LovyanGFX bblanchon/ArduinoJson knolleary/PubSubClient diff --git a/src/button.hpp b/src/button.hpp new file mode 100644 index 0000000..fe1a023 --- /dev/null +++ b/src/button.hpp @@ -0,0 +1,142 @@ +#ifndef _KEYPADBUTTON_H_ +#define _KEYPADBUTTON_H_ + +#define LGFX_AUTODETECT +#include + +class Keypad; + +typedef void (*BUTTON_ON_PRESSED_CALLBACK)(void *, int); + +class KeypadButton +{ +public: + KeypadButton(String label, int value, bool enabled, BUTTON_ON_PRESSED_CALLBACK on_pressed) : label{label}, value{value}, enabled{enabled}, on_pressed{on_pressed} {} + KeypadButton(String label, int value, bool enabled, float x, float y, BUTTON_ON_PRESSED_CALLBACK on_pressed) : label{label}, value{value}, enabled{enabled}, x{x}, y{y}, on_pressed{on_pressed} {} + KeypadButton(String label, int value, bool enabled, float x, float y, int width, int height, BUTTON_ON_PRESSED_CALLBACK on_pressed) : label{label}, value{value}, enabled{enabled}, x{x}, y{y}, width{width}, height{height}, on_pressed{on_pressed} {} + + void draw(LGFX *canvas) + { + if (!dirty) + { + return; + } + dirty = false; + + float MARGIN = 10.0; + float HALF_MARGIN = MARGIN / 2.0; + + float new_x = x + HALF_MARGIN; + float new_y = y + HALF_MARGIN; + + float new_width = width - MARGIN; + float new_height = height - MARGIN; + + int bg_color = enabled ? TFT_WHITE : TFT_DARKGRAY; + int fg_color = enabled ? TFT_BLACK : TFT_WHITE; + + canvas->setEpdMode(epd_fastest); + canvas->setColor(bg_color); + canvas->fillRect(new_x + 1, new_y + 1, new_width - 2, new_height - 2); + + canvas->setTextSize(4.0); + canvas->setBaseColor(bg_color); + canvas->setTextColor(fg_color); + canvas->setCursor(x + (width / 2) - (canvas->fontWidth() / 2), y + (height / 2) - (canvas->fontHeight() / 2)); + canvas->print(label); + + canvas->setColor(TFT_BLACK); + canvas->drawFastHLine(new_x, new_y, new_width); + canvas->drawFastHLine(new_x, new_y + new_height, new_width); + canvas->drawFastVLine(new_x, new_y, new_height); + canvas->drawFastVLine(new_x + new_width, new_y, new_height); + } + + ~KeypadButton() = default; + + String get_label() + { + return label; + } + void set_label(String value) + { + label = value; + } + + int get_value() + { + return value; + } + void set_value(int value) + { + this->value = value; + } + + int get_width() + { + return width; + } + void set_width(int value) + { + width = value; + } + + int get_x() + { + return x; + } + void set_x(int value) + { + x = value; + } + + int get_y() + { + return y; + } + void set_y(int value) + { + y = value; + } + + void disable() + { + if (!enabled) + return; + + enabled = false; + dirty = true; + } + void enable() + { + if (enabled) + return; + + enabled = true; + dirty = true; + } + + void update(Keypad *keypad, uint16_t touch_x, uint16_t touch_y) + { + if (enabled && touch_x > x && touch_x < x + width && touch_y > y && touch_y < y + height) + { + on_pressed(keypad, value); + } + } + +private: + String label; + BUTTON_ON_PRESSED_CALLBACK on_pressed = NULL; + bool enabled = true; + bool dirty = true; + + int value = 0; + + int width = 0; + int height = 0; + + float x = 0; + float y = 0; +}; + +#endif \ No newline at end of file diff --git a/src/display.hpp b/src/display.hpp new file mode 100644 index 0000000..d401afd --- /dev/null +++ b/src/display.hpp @@ -0,0 +1,28 @@ +#ifndef _DISPLAY_H_ +#define _DISPLAY_H_ + +#define LGFX_M5PAPER +#define LGFX_USE_V1 +#define LGFX_AUTODETECT + +#include + +class Display : public LGFX +{ +public: + void init() + { + LGFX::init(); + setRotation(1); + + setEpdMode(epd_mode_t::epd_quality); + fillScreen(TFT_WHITE); + waitDisplay(); + fillScreen(TFT_WHITE); + setEpdMode(epd_mode_t::epd_fast); + + setColor(TFT_BLACK); + } +}; + +#endif \ No newline at end of file diff --git a/src/input.hpp b/src/input.hpp new file mode 100644 index 0000000..52b60a3 --- /dev/null +++ b/src/input.hpp @@ -0,0 +1,132 @@ +#ifndef _INPUT_H_ +#define _INPUT_H_ + +#define LGFX_AUTODETECT +#include + +class Input +{ +public: + Input(String value) : value{value}, old_value{value} {} + Input(String value, float x, float y) : value{value}, x{x}, y{y} {} + Input(String value, float x, float y, int width, int height) : value{value}, x{x}, y{y}, width{width}, height{height} {} + + ~Input() = default; + + String get_value() + { + return value; + } + void set_value(String value) + { + this->value = value; + dirty = true; + } + + void append(String value) + { + this->value += value; + dirty = true; + } + + void append(int value) + { + this->append(String(value)); + } + + void clear() + { + this->value = ""; + dirty = true; + } + + inline unsigned int length() + { + return this->value.length(); + } + + int get_width() + { + return width; + } + void set_width(int value) + { + width = value; + } + + int get_x() + { + return x; + } + void set_x(int value) + { + x = value; + } + + int get_y() + { + return y; + } + void set_y(int value) + { + y = value; + } + + void draw(LGFX *canvas) + { + if (!dirty) + { + return; + } + dirty = false; + + float MARGIN = 10.0; + float HALF_MARGIN = MARGIN / 2.0; + + float new_x = x + HALF_MARGIN; + float new_y = y + HALF_MARGIN; + + float new_width = width - MARGIN; + float new_height = height - MARGIN; + + int bg_color = TFT_WHITE; + int fg_color = TFT_BLACK; + + canvas->setTextSize(8.0); + + canvas->setEpdMode(epd_fastest); + canvas->setColor(bg_color); + canvas->fillRect(new_x + 1, new_y + 1, new_width - 2, new_height - 2); + + canvas->setBaseColor(bg_color); + canvas->setTextColor(fg_color); + canvas->setCursor(x + (width / 2) - ((canvas->fontWidth() * value.length()) / 2), y + (height / 2) - (canvas->fontHeight() / 2)); + canvas->setEpdMode(epd_text); + canvas->print(value); + + if (length() == 0) + { + canvas->setEpdMode(epd_fastest); + canvas->setColor(TFT_BLACK); + canvas->drawFastHLine(new_x, new_y, new_width); + canvas->drawFastHLine(new_x, new_y + new_height, new_width); + canvas->drawFastVLine(new_x, new_y, new_height); + canvas->drawFastVLine(new_x + new_width, new_y, new_height); + } + } + +private: + LGFX *canvas; + String value; + String old_value; + + bool dirty = true; + + int width = 0; + int height = 0; + + float x = 0; + float y = 0; +}; + +#endif \ No newline at end of file diff --git a/src/keypad.cpp b/src/keypad.cpp deleted file mode 100644 index 664721b..0000000 --- a/src/keypad.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#define LGFX_M5PAPER -#define LGFX_USE_V1 -#define LGFX_AUTODETECT - -#include "keypad.hpp" -#include -#include - -static LGFX gfx; - -static const float_t BUTTON_WIDTH = 115; -static const float_t BUTTON_HEIGHT = 115; - -int Keypad::get_button_index_from_touch(int x, int y) -{ - for (int index = 0; index <= 11; index++) - { - float_t cx = (125 * (index % 3)) + 15; - float_t cy = (125 * (index / 3)) + 20; - - if (x > cx && x < cx + BUTTON_WIDTH && y > cy && y < cy + BUTTON_HEIGHT) - { - return index; - } - } - - return -1; -} - -void Keypad::draw_button(int i) -{ - gfx.setTextSize(FONT_SIZE_NORMAL); - - String label = String(i + 1); - if (i == 9) - { - label = String("<"); - } - if (i == 10) - { - label = String("0"); - } - if (i == 11) - { - label = String(">"); - } - - float_t x = (125 * (i % 3)) + 15; - float_t y = (125 * (i / 3)) + 20; - - gfx.setCursor(x + (115 / 2) - (gfx.fontWidth() / 2), y + (115 / 2) - (gfx.fontHeight() / 2)); - gfx.print(label); - - gfx.setColor(TFT_BLACK); - gfx.drawFastHLine(x, y, BUTTON_WIDTH); - gfx.drawFastHLine(x, y + BUTTON_HEIGHT, BUTTON_WIDTH); - gfx.drawFastVLine(x, y, BUTTON_HEIGHT); - gfx.drawFastVLine(x + BUTTON_WIDTH, y, BUTTON_HEIGHT); -} - -void Keypad::draw_input_field() -{ - int x = 405; - int y = 20; - - gfx.setColor(TFT_BLACK); - gfx.drawFastHLine(x, y, 520); - gfx.drawFastHLine(x, y + 115, 520); - gfx.drawFastVLine(x, y, 115); - gfx.drawFastVLine(x + 520, y, 115); -} - -void Keypad::draw_input() -{ - int x = 405; - int y = 20; - - gfx.setEpdMode(epd_mode_t::epd_text); - gfx.setColor(TFT_BLACK); - gfx.setTextSize(FONT_SIZE_LARGE); - - if (input.length() < oldinput.length()) - { - gfx.setColor(TFT_WHITE); - gfx.fillRect(x + 1, y + 1, 519, 114); - gfx.setColor(TFT_BLACK); - } - - gfx.setCursor(x + (520 / 2) - ((gfx.fontWidth() * input.length()) / 2), y + (115 / 2) - (gfx.fontHeight() / 2)); - String keypad = ""; - for (int i = 0; i < input.length(); i++) - { - keypad += "*"; - } - gfx.print(keypad); - gfx.setEpdMode(epd_mode_t::epd_fast); -} - -void Keypad::draw() -{ - gfx.setEpdMode(epd_mode_t::epd_quality); - gfx.fillScreen(TFT_WHITE); - gfx.waitDisplay(); - gfx.fillScreen(TFT_WHITE); - gfx.setEpdMode(epd_mode_t::epd_fast); - - gfx.setColor(TFT_BLACK); - gfx.waitDisplay(); - - for (int i = 0; i <= 11; i++) - { - this->draw_button(i); - } - - draw_input_field(); - draw_input(); -} - -Keypad::Keypad() -{ - gfx.init(); - gfx.setRotation(1); - - draw(); -} - -void Keypad::write(String payload) -{ - payload.replace("_", " "); - - gfx.setTextSize(FONT_SIZE_NORMAL); - - gfx.setColor(TFT_WHITE); - gfx.fillRect(405, 155, gfx.fontWidth() * 10, gfx.fontHeight()); - gfx.setColor(TFT_BLACK); - - gfx.setCursor(405, 155); - payload[0] = toupper(payload[0]); - gfx.printf("%s", payload); -} - -std::optional Keypad::check_input() -{ - if (M5.TP.available()) - { - M5.TP.update(); - if (M5.TP.isFingerUp()) - { - last_index = -1; - return std::nullopt; - } - - tp_finger_t fingerItem = M5.TP.readFinger(0); - - int i = get_button_index_from_touch(fingerItem.x, fingerItem.y); - if (last_index != i) - { - last_index = i; - if (i == 9) - { - // revert - input = ""; - } - else if (i == 10) - { - if (input.length() >= 6) - return std::nullopt; - input += String(0); - } - else if (i == 11) - { - // submit - String retval = input; - input = ""; - return retval; - } - else - { - if (input.length() >= 6) - return std::nullopt; - input += String(i + 1); - } - } - } - return std::nullopt; -} - -std::optional Keypad::get_input() -{ - auto value = check_input(); - if (oldinput != input) - { - draw_input(); - oldinput = input; - } - - return value; -} \ No newline at end of file diff --git a/src/keypad.hpp b/src/keypad.hpp index dc39d42..eb769e2 100644 --- a/src/keypad.hpp +++ b/src/keypad.hpp @@ -1,31 +1,104 @@ #ifndef _KEYPAD_H_ #define _KEYPAD_H_ -#include -#include +#include +#include "button.hpp" -constexpr float FONT_SIZE_NORMAL = 4.0; -constexpr float FONT_SIZE_LARGE = 8.0; +constexpr int BUTTON_ROWS = 4; +constexpr int BUTTON_COLS = 3; +constexpr int BUTTON_COUNT = BUTTON_ROWS * BUTTON_COLS; -class Keypad +class Keypad : public EventEmitter { public: - Keypad(); - int get_button_index_from_touch(int x, int y); - void draw(); - void write(String); - std::optional get_input(); + Keypad(LGFX *canvas) : EventEmitter(), canvas{canvas} + { + int BUTTON_SIZE = canvas->height() / BUTTON_ROWS; + width = BUTTON_SIZE * BUTTON_COLS; + + for (int i = 0; i < BUTTON_COUNT; i++) + { + float x = BUTTON_SIZE * (i % BUTTON_COLS); + float y = BUTTON_SIZE * (i / BUTTON_COLS); + + String lbl = String(i + 1); + bool enabled = true; + + if (i == 9) + { + lbl = String("<"); + } + if (i == 10) + { + lbl = String("0"); + } + if (i == 11) + { + lbl = String(">"); + enabled = false; + } + + buttons.push_back(new KeypadButton(lbl, i, enabled, x, y, BUTTON_SIZE, BUTTON_SIZE, Keypad::on_button_pressed)); + } + } + + ~Keypad() = default; + + inline unsigned int get_width() + { + return width; + } + + void update(tp_finger_t touch) + { + for (int i = 0; i < BUTTON_COUNT; i++) + { + buttons[i]->update(this, touch.x, touch.y); + } + } + + void draw() + { + for (int i = 0; i < BUTTON_COUNT; i++) + { + buttons[i]->draw(canvas); + } + } + + void disable(int index) + { + if (index >= 0 && index < BUTTON_COUNT) + { + buttons[index]->disable(); + } + } + + void set_enabled(int index, bool enabled) + { + if (index >= 0 && index < BUTTON_COUNT) + { + if (enabled) + { + buttons[index]->enable(); + } + else + { + buttons[index]->disable(); + } + } + } private: - int last_index = -1; - String oldinput = ""; - String input = ""; + LGFX *canvas; + std::vector buttons; + bool touching = false; + unsigned int width = 0; - void draw_button(int index); - void draw_input_field(); - void draw_input(); - - std::optional check_input(); + static void on_button_pressed(void *context, int value) + { + auto c = static_cast(context); + c->emit("pressed", c->buttons[value]->get_value()); + } }; #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 5be7157..6fe7aae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,21 +3,42 @@ #define YAML_DISABLE_CJSON -#include +#define LGFX_M5PAPER +#define LGFX_USE_V1 +#define LGFX_AUTODETECT + #include +#include #include #include - #include +#include "display.hpp" +#include "keypad.hpp" +#include "input.hpp" +#include "status.hpp" +#include "touch.hpp" #include "settings.hpp" #include "mqtt.hpp" -#include "keypad.hpp" static WiFiUDP ntpUDP; static NTPClient timeClient(ntpUDP); static MQTT *mqtt; + +static Display display; static Keypad *keypad; +static Status *status; +static Input *input; +static Touch *touch; + +void keypad_state(bool enabled) +{ + for (int i = 0; i < 11; i++) + { + keypad->set_enabled(i, enabled); + } + keypad->set_enabled(11, !enabled); +} static bool is_disarmed = false; @@ -29,13 +50,13 @@ void mqtt_callback(char *topic, byte *payload, unsigned int length) String state((const char *)payload); is_disarmed = state == "disarmed"; - if (state == "armed_away") - { - keypad->draw(); - } - + state.replace("_", " "); + state[0] = toupper(state[0]); Serial.println(state); - keypad->write(state); + status->set(state); + status->draw(&display); + + keypad_state(!is_disarmed); } void lock() @@ -51,23 +72,37 @@ void unlock() mqtt->send("DISARM"); } +void submit(String code) +{ + uint32_t newCode = getCodeFromTimestamp(timeClient.getEpochTime()); + uint32_t input = code.toInt(); + + if (newCode != input) + { + status->set("Invalid"); + return; + } + + unlock(); +} + bool isConnecting = false; void initWifi(Settings *settings) { if (!settings) { - keypad->write("no settings"); + status->set("No settings"); return; } if (isConnecting) { - keypad->write("already connecting"); + status->set("Already connecting"); return; } isConnecting = true; - keypad->write("connecting"); + status->set("Connecting"); WiFi.begin(settings->gettext("wifi:ssid"), settings->gettext("wifi:password")); @@ -89,11 +124,11 @@ void initWifi(Settings *settings) Serial.print("Local IP: "); Serial.println(WiFi.localIP()); isConnecting = false; - keypad->write("connected"); + status->set("Connected"); } else { - keypad->write("failed to connect"); + status->set("Failed to connect"); isConnecting = false; return; } @@ -112,49 +147,116 @@ void initTOTP(Settings *settings) TOTP(base32_key, 20, 30); } +void p(int v) +{ + switch (v) + { + case 9: + input->clear(); + break; + case 10: + input->append("0"); + break; + case 11: + lock(); + break; + default: + input->append(v + 1); + break; + } + + if (input->length() >= 6) + { + // verify + input->draw(&display); // force one more redraw for the last character + submit(input->get_value()); + input->clear(); + } +} + void setup() { M5.begin(); - - keypad = new Keypad(); - keypad->write("loading"); + display.init(); + touch = new Touch(M5.TP); + status = new Status("Loading"); + keypad = new Keypad(&display); + keypad->addListener("pressed", p); + input = new Input("", keypad->get_width(), 0, display.width() - keypad->get_width(), 115); auto settings = new Settings(); if (!settings) { - keypad->write("unable to load settings"); + status->set("Settings Error"); return; } - mqtt = new MQTT(settings, mqtt_callback); - initWifi(settings); mqtt->connect(); timeClient.begin(); initTOTP(settings); } -void submit(String code) -{ - uint32_t newCode = getCodeFromTimestamp(timeClient.getEpochTime()); - uint32_t input = code.toInt(); - - if (newCode == input) - { - unlock(); - return; - } - - lock(); -} - void loop() { timeClient.update(); mqtt->loop(); - auto value = keypad->get_input(); - if (value.has_value()) - submit(value.value()); + auto tap = touch->tap(); + if (tap.has_value()) + keypad->update(tap.value()); + + keypad->draw(); + input->draw(&display); + status->draw(&display); } + +// static Keypad *keypad; + +// void setup() +// { +// M5.begin(); + +// keypad = new Keypad(); +// status->set("loading"); + +// auto settings = new Settings(); + +// if (!settings) +// { +// status->set("unable to load settings"); +// return; +// } + +// mqtt = new MQTT(settings, mqtt_callback); + +// initWifi(settings); +// mqtt->connect(); +// timeClient.begin(); +// initTOTP(settings); +// } + +// void submit(String code) +// { +// uint32_t newCode = getCodeFromTimestamp(timeClient.getEpochTime()); +// uint32_t input = code.toInt(); + +// if (newCode == input) +// { +// unlock(); +// return; +// } + +// lock(); +// } + +// void loop() +// { +// timeClient.update(); +// mqtt->loop(); + +// auto value = keypad->get_input(); +// if (value.has_value()) +// submit(value.value()); +// } diff --git a/src/status.hpp b/src/status.hpp new file mode 100644 index 0000000..9e5af26 --- /dev/null +++ b/src/status.hpp @@ -0,0 +1,65 @@ +#ifndef _STATUS_H_ +#define _STATUS_H_ + +#define LGFX_AUTODETECT +#include + +class Status +{ +public: + Status(String value) : value{value} + { + old_length = value.length(); + } + + ~Status() = default; + + void set(String value) + { + old_length = this->value.length(); + this->value = value; + dirty = true; + } + + void draw(LGFX *canvas) + { + if (!dirty) + { + return; + } + dirty = false; + + float MARGIN = 10.0; + + canvas->setTextSize(3.0); + + int bg_color = TFT_WHITE; + int fg_color = TFT_BLACK; + + float old_x = canvas->width() - (canvas->fontWidth() * old_length) - MARGIN; + float new_x = canvas->width() - (canvas->fontWidth() * value.length()) - MARGIN; + float new_y = canvas->height() - canvas->fontHeight() - MARGIN; + + canvas->setEpdMode(epd_fastest); + canvas->setColor(bg_color); + + canvas->fillRect(old_x, new_y, canvas->width() - old_x, canvas->height() - new_y); + canvas->waitDisplay(); + canvas->setColor(fg_color); + + canvas->setEpdMode(epd_text); + canvas->setBaseColor(bg_color); + canvas->setTextColor(fg_color); + canvas->setCursor(new_x, new_y); + canvas->print(value); + } + +private: + LGFX *canvas; + String value; + int old_length = 0; + + bool dirty = true; +}; + +#endif \ No newline at end of file diff --git a/src/touch.hpp b/src/touch.hpp new file mode 100644 index 0000000..3fc52e4 --- /dev/null +++ b/src/touch.hpp @@ -0,0 +1,42 @@ +#include +#ifndef _TOUCH_H_ +#define _TOUCH_H_ + +#include + +class Touch +{ +public: + Touch(GT911 TP) : TP{TP} {} + + std::optional tap() + { + if (!TP.available()) + { + return std::nullopt; + } + + TP.update(); + + if (TP.isFingerUp()) + { + touching = false; + return std::nullopt; + } + + if (touching) + { + return std::nullopt; + } + + touching = true; + return TP.readFinger(0); + } + +private: + GT911 TP; + tp_finger_t finger; + bool touching; +}; + +#endif \ No newline at end of file