diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd5c2c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pioenvs +.piolibdeps +.clang_complete +.gcc-flags.json +data diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2c4ff5c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,65 @@ +# Continuous Integration (CI) is the practice, in software +# engineering, of merging all developer working copies with a shared mainline +# several times a day < http://docs.platformio.org/page/ci/index.html > +# +# Documentation: +# +# * Travis CI Embedded Builds with PlatformIO +# < https://docs.travis-ci.com/user/integration/platformio/ > +# +# * PlatformIO integration with Travis CI +# < http://docs.platformio.org/page/ci/travis.html > +# +# * User Guide for `platformio ci` command +# < http://docs.platformio.org/page/userguide/cmd_ci.html > +# +# +# Please choice one of the following templates (proposed below) and uncomment +# it (remove "# " before each line) or use own configuration according to the +# Travis CI documentation (see above). +# + + +# +# Template #1: General project. Test it using existing `platformio.ini`. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# install: +# - pip install -U platformio +# +# script: +# - platformio run + + +# +# Template #2: The project is intended to by used as a library with examples +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# env: +# - PLATFORMIO_CI_SRC=path/to/test/file.c +# - PLATFORMIO_CI_SRC=examples/file.ino +# - PLATFORMIO_CI_SRC=path/to/test/directory +# +# install: +# - pip install -U platformio +# +# script: +# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N diff --git a/lib/DataStructures/LinkedList.h b/lib/DataStructures/LinkedList.h new file mode 100644 index 0000000..8145168 --- /dev/null +++ b/lib/DataStructures/LinkedList.h @@ -0,0 +1,317 @@ +/* + ********* Adapted from: ********* + https://github.com/ivanseidel/LinkedList + Created by Ivan Seidel Gomes, March, 2013. + Released into the public domain. + ********************************* + + Changes: + - public access to ListNode (allows for splicing for LRU) + - doubly-linked + - remove caching stuff in favor of standard linked list iterating + - remove sorting +*/ + +#ifndef LinkedList_h +#define LinkedList_h + +#include + +template +struct ListNode { + T data; + ListNode *next; + ListNode *prev; +}; + +template +class LinkedList { + +protected: + int _size; + ListNode *root; + ListNode *last; + +public: + LinkedList(); + ~LinkedList(); + + /* + Returns current size of LinkedList + */ + virtual int size() const; + /* + Adds a T object in the specified index; + Unlink and link the LinkedList correcly; + Increment _size + */ + virtual bool add(int index, T); + /* + Adds a T object in the end of the LinkedList; + Increment _size; + */ + virtual bool add(T); + /* + Adds a T object in the start of the LinkedList; + Increment _size; + */ + virtual bool unshift(T); + /* + Set the object at index, with T; + Increment _size; + */ + virtual bool set(int index, T); + /* + Remove object at index; + If index is not reachable, returns false; + else, decrement _size + */ + virtual T remove(int index); + /* + Remove last object; + */ + virtual T pop(); + /* + Remove first object; + */ + virtual T shift(); + /* + Get the index'th element on the list; + Return Element if accessible, + else, return false; + */ + virtual T get(int index); + + /* + Clear the entire array + */ + virtual void clear(); + + ListNode* getNode(int index); + virtual void spliceToFront(ListNode* node); + ListNode* getHead() { return root; } + ListNode* getTail() { return last; } + T getLast() const { return last == NULL ? T() : last->data; } + +}; + + +template +void LinkedList::spliceToFront(ListNode* node) { + // Node is already root + if (node->prev == NULL) { + return; + } + + node->prev->next = node->next; + if (node->next != NULL) { + node->next->prev = node->prev; + } else { + last = node->prev; + } + + root->prev = node; + node->next = root; + node->prev = NULL; + root = node; +} + +// Initialize LinkedList with false values +template +LinkedList::LinkedList() +{ + root=NULL; + last=NULL; + _size=0; +} + +// Clear Nodes and free Memory +template +LinkedList::~LinkedList() +{ + ListNode* tmp; + while(root!=NULL) + { + tmp=root; + root=root->next; + delete tmp; + } + last = NULL; + _size=0; +} + +/* + Actualy "logic" coding +*/ + +template +ListNode* LinkedList::getNode(int index){ + + int _pos = 0; + ListNode* current = root; + + while(_pos < index && current){ + current = current->next; + + _pos++; + } + + return false; +} + +template +int LinkedList::size() const{ + return _size; +} + +template +bool LinkedList::add(int index, T _t){ + + if(index >= _size) + return add(_t); + + if(index == 0) + return unshift(_t); + + ListNode *tmp = new ListNode(), + *_prev = getNode(index-1); + tmp->data = _t; + tmp->next = _prev->next; + _prev->next = tmp; + + _size++; + + return true; +} + +template +bool LinkedList::add(T _t){ + + ListNode *tmp = new ListNode(); + tmp->data = _t; + tmp->next = NULL; + + if(root){ + // Already have elements inserted + last->next = tmp; + last = tmp; + }else{ + // First element being inserted + root = tmp; + last = tmp; + } + + _size++; + + return true; +} + +template +bool LinkedList::unshift(T _t){ + + if(_size == 0) + return add(_t); + + ListNode *tmp = new ListNode(); + tmp->next = root; + root->prev = tmp; + tmp->data = _t; + root = tmp; + + _size++; + + return true; +} + +template +bool LinkedList::set(int index, T _t){ + // Check if index position is in bounds + if(index < 0 || index >= _size) + return false; + + getNode(index)->data = _t; + return true; +} + +template +T LinkedList::pop(){ + if(_size <= 0) + return T(); + + if(_size >= 2){ + ListNode *tmp = last->prev; + T ret = tmp->next->data; + delete(tmp->next); + tmp->next = NULL; + last = tmp; + _size--; + return ret; + }else{ + // Only one element left on the list + T ret = root->data; + delete(root); + root = NULL; + last = NULL; + _size = 0; + return ret; + } +} + +template +T LinkedList::shift(){ + if(_size <= 0) + return T(); + + if(_size > 1){ + ListNode *_next = root->next; + T ret = root->data; + delete(root); + root = _next; + _size --; + + return ret; + }else{ + // Only one left, then pop() + return pop(); + } + +} + +template +T LinkedList::remove(int index){ + if (index < 0 || index >= _size) + { + return T(); + } + + if(index == 0) + return shift(); + + if (index == _size-1) + { + return pop(); + } + + ListNode *tmp = getNode(index - 1); + ListNode *toDelete = tmp->next; + T ret = toDelete->data; + tmp->next = tmp->next->next; + delete(toDelete); + _size--; + return ret; +} + + +template +T LinkedList::get(int index){ + ListNode *tmp = getNode(index); + + return (tmp ? tmp->data : T()); +} + +template +void LinkedList::clear(){ + while(size() > 0) + shift(); +} +#endif diff --git a/lib/Display/BitmapRegion.cpp b/lib/Display/BitmapRegion.cpp new file mode 100644 index 0000000..dc8b841 --- /dev/null +++ b/lib/Display/BitmapRegion.cpp @@ -0,0 +1,32 @@ +#include +#include + +BitmapRegion::BitmapRegion( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + std::shared_ptr formatter +) : Region(variable, x, y, w, h, color, formatter) +{ } + +BitmapRegion::~BitmapRegion() { } + +void BitmapRegion::render(GxEPD* display) { + if (! SPIFFS.exists(variableValue)) { + Serial.print(F("WARN - tried to render bitmap file that doesn't exist: ")); + Serial.println(variableValue); + } else { + File file = SPIFFS.open(variableValue, "r"); + size_t size = w*h/8; + uint8_t bits[size]; + size_t readBytes = file.readBytes(reinterpret_cast(bits), size); + + file.close(); + display->drawBitmap(bits, x, y, w, h, color); + } + + this->dirty = false; +} diff --git a/lib/Display/BitmapRegion.h b/lib/Display/BitmapRegion.h new file mode 100644 index 0000000..535b250 --- /dev/null +++ b/lib/Display/BitmapRegion.h @@ -0,0 +1,22 @@ +#include + +#ifndef _BITMAP_REGION +#define _BITMAP_REGION + +class BitmapRegion : public Region { +public: + BitmapRegion( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + std::shared_ptr formatter + ); + ~BitmapRegion(); + + virtual void render(GxEPD* display); +}; + +#endif diff --git a/lib/Display/DisplayTemplateDriver.cpp b/lib/Display/DisplayTemplateDriver.cpp new file mode 100644 index 0000000..7f78bce --- /dev/null +++ b/lib/Display/DisplayTemplateDriver.cpp @@ -0,0 +1,302 @@ +#include +#include + +DisplayTemplateDriver::DisplayTemplateDriver( + GxEPD* display, + Settings& settings +) + : display(display), + dirty(true), + shouldFullUpdate(false), + lastFullUpdate(0), + settings(settings) +{ } + +void DisplayTemplateDriver::init() { + display->init(); + vars.load(); +} + +void DisplayTemplateDriver::loop() { + if (shouldFullUpdate || dirty) { + time_t now = millis(); + + if (shouldFullUpdate || now > (lastFullUpdate + settings.fullRefreshPeriod)) { + shouldFullUpdate = false; + lastFullUpdate = now; + fullUpdate(); + + // No need to do partial updates + clearDirtyRegions(); + } else { + flushDirtyRegions(true); + } + + dirty = false; + } +} + +void DisplayTemplateDriver::scheduleFullUpdate() { + shouldFullUpdate = true; +} + +void DisplayTemplateDriver::clearDirtyRegions() { + ListNode>* curr = regions.getHead(); + + while (curr != NULL) { + std::shared_ptr region = curr->data; + region->clearDirty(); + + curr = curr->next; + } +} + +void DisplayTemplateDriver::flushDirtyRegions(bool updateScreen) { + ListNode>* curr = regions.getHead(); + + while (curr != NULL) { + std::shared_ptr region = curr->data; + + if (region->isDirty()) { + region->clearDirty(); + region->render(display); + + if (updateScreen) { + region->updateScreen(display); + } + } + + curr = curr->next; + } +} + +void DisplayTemplateDriver::fullUpdate() { + flushDirtyRegions(false); + display->update(); +} + +void DisplayTemplateDriver::updateVariable(const String& key, const String& value) { + vars.set(key, value); + + ListNode>* curr = regions.getHead(); + + while (curr != NULL) { + std::shared_ptr region = curr->data; + + if (region->getVariableName() == key) { + this->dirty = this->dirty || region->updateValue(value); + } + + curr = curr->next; + } +} + +void DisplayTemplateDriver::setTemplate(const String& templateFilename) { + // Delete regions so we don't have unused regions hanging around + regions.clear(); + + // Always schedule a full update. Even if template is the same, it could've + // changed. + scheduleFullUpdate(); + + this->templateFilename = templateFilename; + loadTemplate(); +} + +const String& DisplayTemplateDriver::getTemplateFilename() { + return templateFilename; +} + +void DisplayTemplateDriver::loadTemplate() { + if (! SPIFFS.exists(templateFilename)) { + Serial.println(F("WARN - template file does not exist")); + printError("Template file does not exist"); + + return; + } + + Serial.print(F("Loading template: ")); + Serial.println(templateFilename); + + File file = SPIFFS.open(templateFilename, "r"); + + DynamicJsonBuffer jsonBuffer; + JsonObject& tmpl = jsonBuffer.parseObject(file); + file.close(); + + Serial.print(F("Free heap - ")); + Serial.println(ESP.getFreeHeap()); + + if (!tmpl.success()) { + Serial.println(F("WARN - could not parse template file")); + printError("Could not parse template!"); + + return; + } + + display->fillScreen(parseColor(tmpl["background_color"])); + + if (tmpl.containsKey("lines")) { + renderLines(tmpl["lines"]); + } + + if (tmpl.containsKey("bitmaps")) { + renderBitmaps(tmpl["bitmaps"]); + } + + if (tmpl.containsKey("text")) { + renderTexts(tmpl["text"]); + } +} + +void DisplayTemplateDriver::renderBitmap(const String &filename, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { + if (! SPIFFS.exists(filename)) { + Serial.print(F("WARN - tried to render bitmap file that doesn't exist: ")); + Serial.println(filename); + return; + } + + File file = SPIFFS.open(filename, "r"); + size_t size = w*h/8; + uint8_t bits[size]; + size_t readBytes = file.readBytes(reinterpret_cast(bits), size); + + file.close(); + display->drawBitmap(bits, x, y, w, h, color); +} + +void DisplayTemplateDriver::renderBitmaps(ArduinoJson::JsonArray &bitmaps) { + for (size_t i = 0; i < bitmaps.size(); i++) { + JsonObject& bitmap = bitmaps[i]; + + const uint16_t x = bitmap["x"]; + const uint16_t y = bitmap["y"]; + const uint16_t w = bitmap["w"]; + const uint16_t h = bitmap["h"]; + const uint16_t color = extractColor(bitmap); + + if (bitmap.containsKey("static")) { + renderBitmap(bitmap.get("static"), x, y, w, h, color); + } + + if (bitmap.containsKey("variable")) { + const String& variable = bitmap["variable"]; + std::shared_ptr region = addBitmapRegion(bitmap); + region->updateValue(vars.get(variable)); + } + } +} + +void DisplayTemplateDriver::renderTexts(ArduinoJson::JsonArray &texts) { + for (size_t i = 0; i < texts.size(); i++) { + JsonObject& text = texts[i]; + + uint16_t x = text["x"]; + uint16_t y = text["y"]; + + // Font should be set first because it fiddles with the cursor. + if (text.containsKey("font")) { + display->setFont(parseFont(text["font"])); + } + + display->setCursor(x, y); + display->setTextColor(extractColor(text)); + + if (text.containsKey("static")) { + display->print(text.get("static")); + } + + if (text.containsKey("variable")) { + const String& variable = text.get("variable"); + std::shared_ptr region = addTextRegion(text); + region->updateValue(vars.get(variable)); + } + } +} + +void DisplayTemplateDriver::renderLines(ArduinoJson::JsonArray &lines) { + for (JsonArray::iterator it = lines.begin(); it != lines.end(); ++it) { + JsonObject& line = *it; + display->drawLine(line["x1"], line["y1"], line["x2"], line["y2"], extractColor(line)); + } +} + +std::shared_ptr DisplayTemplateDriver::addBitmapRegion(const JsonObject& spec) { + std::shared_ptr region( + new BitmapRegion( + spec.get("variable"), + spec["x"], + spec["y"], + spec["w"], + spec["h"], + extractColor(spec), + VariableFormatter::buildFormatter(spec) + ) + ); + regions.add(region); + + return region; +} + +std::shared_ptr DisplayTemplateDriver::addTextRegion(const JsonObject& spec) { + std::shared_ptr region( + new TextRegion( + spec.get("variable"), + spec.get("x"), + spec.get("y"), + 0, // width and height are unknown until rendered + 0, // ^ + extractColor(spec), + parseFont(spec.get("font")), + VariableFormatter::buildFormatter(spec) + ) + ); + regions.add(region); + + return region; +} + +void DisplayTemplateDriver::printError(const char *message) { + display->fillScreen(GxEPD_BLACK); + display->setFont(&FreeMonoBold9pt7b); + display->setTextColor(GxEPD_WHITE); + display->setCursor(0, display->height() / 2); + display->print(message); +} + +const GFXfont* DisplayTemplateDriver::parseFont(const String &fontName) { + if (fontName.equalsIgnoreCase("FreeMonoBold24pt7b")) { + return &FreeMonoBold24pt7b; + } else if (fontName.equalsIgnoreCase("FreeSans18pt7b")) { + return &FreeSans18pt7b; + } else if (fontName.equalsIgnoreCase("FreeSans9pt7b")) { + return &FreeSans9pt7b; + } else if (fontName.equalsIgnoreCase("FreeSansBold9pt7b")) { + return &FreeSansBold9pt7b; + } else if (fontName.equalsIgnoreCase("FreeMono9pt7b")) { + return &FreeMono9pt7b; + } else { + Serial.print(F("WARN - tried to fetch unknown font: ")); + Serial.println(fontName); + + return defaultFont; + } +} + +const uint16_t DisplayTemplateDriver::parseColor(const String &colorName) { + if (colorName.equalsIgnoreCase("black")) { + return GxEPD_BLACK; + } else if (colorName.equalsIgnoreCase("red")) { + return GxEPD_RED; + } else { + return GxEPD_WHITE; + } +} + +const uint16_t DisplayTemplateDriver::extractColor(const ArduinoJson::JsonObject &spec) { + if (spec.containsKey("color")) { + return parseColor(spec["color"]); + } else { + return defaultColor; + } +} diff --git a/lib/Display/DisplayTemplateDriver.h b/lib/Display/DisplayTemplateDriver.h new file mode 100644 index 0000000..3610e85 --- /dev/null +++ b/lib/Display/DisplayTemplateDriver.h @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef _DRIVER_H +#define _DRIVER_H + +#ifndef TEXT_BOUNDING_BOX_PADDING +#define TEXT_BOUNDING_BOX_PADDING 5 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DisplayTemplateDriver { +public: + DisplayTemplateDriver( + GxEPD* display, + Settings& settings + ); + + // Updates the value for the given variable, and marks any regions bound to + // that variable as dirty. + void updateVariable(const String& name, const String& value); + + // Sets the JSON template to load from SPIFFS. Clears any regions that may + // have been parsed from the previous template. + void setTemplate(const String& filename); + const String& getTemplateFilename(); + + // Relaods and parses the template file from flash. + void loadTemplate(); + + // Performs a full update of the display. Applies the template and refreshes + // the entire screen. + void fullUpdate(); + + // Performs a full update on the next call of loop(). + void scheduleFullUpdate(); + + // Updates the display by checking for regions that have been marked as dirty + // and performing partial updates on the bounding boxes. + void loop(); + + void init(); + +private: + GxEPD* display; + VariableDictionary vars; + String templateFilename; + Settings& settings; + + LinkedList> regions; + + bool dirty; + bool shouldFullUpdate; + time_t lastFullUpdate; + + const uint16_t defaultColor = GxEPD_BLACK; + const GFXfont* defaultFont = &FreeSans9pt7b; + + void flushDirtyRegions(bool screenUpdates); + void clearDirtyRegions(); + void printError(const char* message); + + void renderLines(JsonArray& lines); + void renderTexts(JsonArray& text); + void renderBitmaps(JsonArray& bitmaps); + void renderBitmap(const String& filename, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); + + std::shared_ptr addTextRegion(const JsonObject& spec); + std::shared_ptr addBitmapRegion(const JsonObject& spec); + + const uint16_t parseColor(const String& colorName); + const GFXfont* parseFont(const String& fontName); + const uint16_t extractColor(const JsonObject& spec); +}; + +#endif diff --git a/lib/Display/GxGDEW042Z15.cpp b/lib/Display/GxGDEW042Z15.cpp new file mode 100644 index 0000000..edbea37 --- /dev/null +++ b/lib/Display/GxGDEW042Z15.cpp @@ -0,0 +1,538 @@ +/************************************************************************************ + class GxGDEW042Z15 : Display class example for GDEW042Z15 e-Paper from Dalian Good Display Co., Ltd.: www.good-display.com + + based on Demo Example from Good Display, available here: http://www.good-display.com/download_detail/downloadsId=524.html + + Author : J-M Zingg + + Version : 2.2 + + Support: limited, provided as example, no claim to be fit for serious use + + Controller: IL91874 : http://www.good-display.com/download_detail/downloadsId=539.html + + connection to the e-Paper display is through DESTM32-S2 connection board, available from Good Display + + DESTM32-S2 pinout (top, component side view): + |------------------------------------------------- + | VCC |o o| VCC 5V not needed + | GND |o o| GND + | 3.3 |o o| 3.3 3.3V + | nc |o o| nc + | nc |o o| nc + | nc |o o| nc + MOSI | DIN |o o| CLK SCK + SS | CS |o o| DC e.g. D3 + D4 | RST |o o| BUSY e.g. D2 + | nc |o o| BS GND + |------------------------------------------------- + + note: for correct red color jumper J3 must be set on 0.47 side (towards FCP connector) + +*/ +#include "GxGDEW042Z15.h" + +#if defined(ESP8266) || defined(ESP32) +#include +#else +#include +#endif + +GxGDEW042Z15::GxGDEW042Z15(GxIO& io, uint8_t rst, uint8_t busy) + : GxEPD(GxGDEW042Z15_WIDTH, GxGDEW042Z15_HEIGHT), + IO(io), _current_page(-1), + _rst(rst), _busy(busy) +{ +} + +template static inline void +swap(T& a, T& b) +{ + T t = a; + a = b; + b = t; +} + +void GxGDEW042Z15::updateWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool usingRotation) { + uint16_t xe = min(GxGDEW042Z15_WIDTH, x + w) - 1; + uint16_t ye = min(GxGDEW042Z15_HEIGHT, y + h) - 1; + uint16_t xs_bx = x / 8; + uint16_t xe_bx = (xe + 7) / 8; + + _wakeUp(); + _writeCommand(PARTIAL_IN); + _writeCommand(PARTIAL_WINDOW); + + _writeData(x / 256); + _writeData(x % 256); + _writeData(xe / 256); + _writeData(xe % 256); + _writeData(y / 256); + _writeData(y % 256); + _writeData(ye / 256); + _writeData(ye % 256); + // _writeData(x >> 8); + // _writeData(x & 0xf8); // x should be the multiple of 8, the last 3 bit will always be ignored + // _writeData(((x & 0xf8) + w - 1) >> 8); + // _writeData(((x & 0xf8) + w - 1) | 0x07); + // _writeData(y >> 8); + // _writeData(y & 0xff); + // _writeData((y + h - 1) >> 8); + // _writeData((y + h - 1) & 0xff); + _writeData(0x01); // Gates scan both inside and outside of the partial window. (default) + delay(2); + + _writeCommand(DATA_START_TRANSMISSION_1); + for (int16_t y1 = y; y1 <= ye; y1++) { + for (int16_t x1 = xs_bx; x1 < xe_bx; x1++) { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_black_buffer)) ? _black_buffer[idx] : 0x00; + _writeData(data); + } + } + + _writeCommand(DISPLAY_REFRESH); + _waitWhileBusy("updateWindow"); + _writeCommand(PARTIAL_OUT); +} + +void GxGDEW042Z15::drawPixel(int16_t x, int16_t y, uint16_t color) +{ + if ((x < 0) || (x >= width()) || (y < 0) || (y >= height())) return; + + // check rotation, move pixel around if necessary + switch (getRotation()) + { + case 1: + swap(x, y); + x = GxGDEW042Z15_WIDTH - x - 1; + break; + case 2: + x = GxGDEW042Z15_WIDTH - x - 1; + y = GxGDEW042Z15_HEIGHT - y - 1; + break; + case 3: + swap(x, y); + y = GxGDEW042Z15_HEIGHT - y - 1; + break; + } + uint16_t i = x / 8 + y * GxGDEW042Z15_WIDTH / 8; + if (_current_page < 1) + { + if (i >= sizeof(_black_buffer)) return; + } + else + { + y -= _current_page * GxGDEW042Z15_PAGE_HEIGHT; + if ((y < 0) || (y >= GxGDEW042Z15_PAGE_HEIGHT)) return; + i = x / 8 + y * GxGDEW042Z15_WIDTH / 8; + } + + _black_buffer[i] = (_black_buffer[i] | (1 << (7 - x % 8))); // white + _red_buffer[i] = (_red_buffer[i] | (1 << (7 - x % 8))); // white + if (color == GxEPD_WHITE) return; + else if (color == GxEPD_BLACK) _black_buffer[i] = (_black_buffer[i] & (0xFF ^ (1 << (7 - x % 8)))); + else if (color == GxEPD_RED) _red_buffer[i] = (_red_buffer[i] & (0xFF ^ (1 << (7 - x % 8)))); +} + + +void GxGDEW042Z15::init(void) +{ + IO.init(); + IO.setFrequency(4000000); // 4MHz : 250ns > 150ns min RD cycle + digitalWrite(_rst, 1); + pinMode(_rst, OUTPUT); + pinMode(_busy, INPUT); + fillScreen(GxEPD_WHITE); + _current_page = -1; +} + +void GxGDEW042Z15::fillScreen(uint16_t color) +{ + uint8_t black = 0xFF; + uint8_t red = 0xFF; + if (color == GxEPD_WHITE); + else if (color == GxEPD_BLACK) black = 0x00; + else if (color == GxEPD_RED) red = 0x00; + for (uint16_t x = 0; x < sizeof(_black_buffer); x++) + { + _black_buffer[x] = black; + _red_buffer[x] = red; + } +} + +void GxGDEW042Z15::update(void) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(DATA_START_TRANSMISSION_1); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData((i < sizeof(_black_buffer)) ? _black_buffer[i] : 0x00); + } + _writeCommand(DATA_START_TRANSMISSION_2); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData((i < sizeof(_red_buffer)) ? _red_buffer[i] : 0x00); + } + _writeCommand(DISPLAY_REFRESH); //display refresh + _waitWhileBusy("update"); + _sleep(); +} + +void GxGDEW042Z15::drawBitmap(const uint8_t *bitmap, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color, int16_t mode) +{ + if (mode & bm_default) mode |= bm_normal; // no change + drawBitmapBM(bitmap, x, y, w, h, color, mode); +} + +void GxGDEW042Z15::drawExamplePicture(const uint8_t* black_bitmap, const uint8_t* red_bitmap, uint32_t black_size, uint32_t red_size) +{ + drawPicture(black_bitmap, red_bitmap, black_size, red_size, bm_normal); +} + +void GxGDEW042Z15::drawPicture(const uint8_t* black_bitmap, const uint8_t* red_bitmap, uint32_t black_size, uint32_t red_size, int16_t mode) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + uint8_t data = 0x00; // white is 0x00 on device + if (i < black_size) + { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&black_bitmap[i]); +#else + data = black_bitmap[i]; +#endif + if (mode & bm_invert) data = ~data; + } + _writeData(data); + } + _writeCommand(0x13); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + uint8_t data = 0x00; // white is 0x00 on device + if (i < red_size) + { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&red_bitmap[i]); +#else + data = red_bitmap[i]; +#endif + if (mode & bm_invert_red) data = ~data; + } + _writeData(data); + } + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawPicture"); + _sleep(); +} + +void GxGDEW042Z15::drawBitmap(const uint8_t* bitmap, uint32_t size, int16_t mode) +{ + if (_current_page != -1) return; + if (mode & bm_default) mode |= bm_normal; // no change + _wakeUp(); + _writeCommand(0x10); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + uint8_t data = 0x00; // white is 0x00 on device + if (i < size) + { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&bitmap[i]); +#else + data = bitmap[i]; +#endif + if (mode & bm_invert) data = ~data; + } + _writeData(data); + } + _writeCommand(0x13); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData(0); + } + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawBitmap"); + _sleep(); +} + +void GxGDEW042Z15::eraseDisplay(bool using_partial_update) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData(0x00); + } + _writeCommand(0x13); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData(0x00); + } + _writeCommand(0x12); //display refresh + _waitWhileBusy("eraseDisplay"); + _sleep(); +} + +void GxGDEW042Z15::powerDown() +{ + _sleep(); +} + +void GxGDEW042Z15::_writeCommand(uint8_t command) +{ + if (!digitalRead(_busy)) + { + String str = String("command 0x") + String(command, HEX); + _waitWhileBusy(str.c_str()); + } + IO.writeCommandTransaction(command); +} + +void GxGDEW042Z15::_writeData(uint8_t data) +{ + IO.writeDataTransaction(data); +} + +void GxGDEW042Z15::_waitWhileBusy(const char* comment) +{ + unsigned long start = micros(); + while (1) + { + if (digitalRead(_busy) == 1) break; + delay(1); + if (micros() - start > 20000000) // > 15.5s ! + { + Serial.println("Busy Timeout!"); + break; + } + } + if (comment) + { + //unsigned long elapsed = micros() - start; + //Serial.print(comment); + //Serial.print(" : "); + //Serial.println(elapsed); + } +} + +void GxGDEW042Z15::_wakeUp() +{ + digitalWrite(_rst, LOW); + delay(200); + digitalWrite(_rst, HIGH); + delay(200); + + _writeCommand(BOOSTER_SOFT_START); + _writeData(0x17); + _writeData(0x17); + _writeData(0x17); + _writeCommand(POWER_ON); + _waitWhileBusy("Power On"); + _writeCommand(PANEL_SETTING); + _writeData(0x0F); + // _writeCommand(0x30); // PLL + // _writeData(0x39); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ +} + +void GxGDEW042Z15::_sleep(void) +{ + _writeCommand(VCOM_AND_DATA_INTERVAL_SETTING); + _writeData(0xF7); + _writeCommand(POWER_OFF); // power off + _waitWhileBusy("Power Off"); + _writeCommand(DEEP_SLEEP); // deep sleep + _writeData(0xA5); +} + +void GxGDEW042Z15::drawPaged(void (*drawCallback)(void)) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_black_buffer)) ? _black_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _writeCommand(0x13); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_red_buffer)) ? _red_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _current_page = -1; + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawPaged"); + _sleep(); +} + +void GxGDEW042Z15::drawPaged(void (*drawCallback)(uint32_t), uint32_t p) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_black_buffer)) ? _black_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _writeCommand(0x13); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_red_buffer)) ? _red_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _current_page = -1; + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawPaged"); + _sleep(); +} + +void GxGDEW042Z15::drawPaged(void (*drawCallback)(const void*), const void* p) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_black_buffer)) ? _black_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _writeCommand(0x13); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_red_buffer)) ? _red_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _current_page = -1; + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawPaged"); + _sleep(); +} + +void GxGDEW042Z15::drawPaged(void (*drawCallback)(const void*, const void*), const void* p1, const void* p2) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p1, p2); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_black_buffer)) ? _black_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _writeCommand(0x13); + for (_current_page = 0; _current_page < GxGDEW042Z15_PAGES; _current_page++) + { + fillScreen(GxEPD_WHITE); + drawCallback(p1, p2); + for (int16_t y1 = 0; y1 < GxGDEW042Z15_PAGE_HEIGHT; y1++) + { + for (int16_t x1 = 0; x1 < GxGDEW042Z15_WIDTH / 8; x1++) + { + uint16_t idx = y1 * (GxGDEW042Z15_WIDTH / 8) + x1; + uint8_t data = (idx < sizeof(_red_buffer)) ? _red_buffer[idx] : 0x00; + _writeData(data); + } + } + } + _current_page = -1; + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawPaged"); + _sleep(); +} + +void GxGDEW042Z15::drawCornerTest(uint8_t em) +{ + if (_current_page != -1) return; + _wakeUp(); + _writeCommand(0x10); + for (uint32_t y = 0; y < GxGDEW042Z15_HEIGHT; y++) + { + for (uint32_t x = 0; x < GxGDEW042Z15_WIDTH / 8; x++) + { + uint8_t data = 0xFF; + if ((x < 1) && (y < 8)) data = 0x00; + if ((x > GxGDEW042Z15_WIDTH / 8 - 3) && (y < 16)) data = 0x00; + if ((x > GxGDEW042Z15_WIDTH / 8 - 4) && (y > GxGDEW042Z15_HEIGHT - 25)) data = 0x00; + if ((x < 4) && (y > GxGDEW042Z15_HEIGHT - 33)) data = 0x00; + _writeData(~data); + } + } + _writeCommand(0x13); + for (uint32_t i = 0; i < GxGDEW042Z15_BUFFER_SIZE; i++) + { + _writeData(0); + } + _writeCommand(0x12); //display refresh + _waitWhileBusy("drawCornerTest"); + _sleep(); +} diff --git a/lib/Display/GxGDEW042Z15.h b/lib/Display/GxGDEW042Z15.h new file mode 100644 index 0000000..2141aeb --- /dev/null +++ b/lib/Display/GxGDEW042Z15.h @@ -0,0 +1,168 @@ +/************************************************************************************ + class GxGDEW042Z15 : Display class example for GDEW027C44 e-Paper from Dalian Good Display Co., Ltd.: www.good-display.com + + based on Demo Example from Good Display, available here: http://www.good-display.com/download_detail/downloadsId=519.html + + Author : J-M Zingg + + modified by : + + Version : 2.2 + + Support: limited, provided as example, no claim to be fit for serious use + + Controller: IL91874 : http://www.good-display.com/download_detail/downloadsId=539.html + + connection to the e-Paper display is through DESTM32-S2 connection board, available from Good Display + + DESTM32-S2 pinout (top, component side view): + |------------------------------------------------- + | VCC |o o| VCC 5V not needed + | GND |o o| GND + | 3.3 |o o| 3.3 3.3V + | nc |o o| nc + | nc |o o| nc + | nc |o o| nc + MOSI | DIN |o o| CLK SCK + SS | CS |o o| DC e.g. D3 + D4 | RST |o o| BUSY e.g. D2 + | nc |o o| BS GND + |------------------------------------------------- + + note: for correct red color jumper J3 must be set on 0.47 side (towards FCP connector) + +*/ +#ifndef _GxGDEW042Z15_H_ +#define _GxGDEW042Z15_H_ + +#define PANEL_SETTING 0x00 +#define POWER_SETTING 0x01 +#define POWER_OFF 0x02 +#define POWER_OFF_SEQUENCE_SETTING 0x03 +#define POWER_ON 0x04 +#define POWER_ON_MEASURE 0x05 +#define BOOSTER_SOFT_START 0x06 +#define DEEP_SLEEP 0x07 +#define DATA_START_TRANSMISSION_1 0x10 +#define DATA_STOP 0x11 +#define DISPLAY_REFRESH 0x12 +#define DATA_START_TRANSMISSION_2 0x13 +#define LUT_FOR_VCOM 0x20 +#define LUT_WHITE_TO_WHITE 0x21 +#define LUT_BLACK_TO_WHITE 0x22 +#define LUT_WHITE_TO_BLACK 0x23 +#define LUT_BLACK_TO_BLACK 0x24 +#define PLL_CONTROL 0x30 +#define TEMPERATURE_SENSOR_COMMAND 0x40 +#define TEMPERATURE_SENSOR_SELECTION 0x41 +#define TEMPERATURE_SENSOR_WRITE 0x42 +#define TEMPERATURE_SENSOR_READ 0x43 +#define VCOM_AND_DATA_INTERVAL_SETTING 0x50 +#define LOW_POWER_DETECTION 0x51 +#define TCON_SETTING 0x60 +#define RESOLUTION_SETTING 0x61 +#define GSST_SETTING 0x65 +#define GET_STATUS 0x71 +#define AUTO_MEASUREMENT_VCOM 0x80 +#define READ_VCOM_VALUE 0x81 +#define VCM_DC_SETTING 0x82 +#define PARTIAL_WINDOW 0x90 +#define PARTIAL_IN 0x91 +#define PARTIAL_OUT 0x92 +#define PROGRAM_MODE 0xA0 +#define ACTIVE_PROGRAMMING 0xA1 +#define READ_OTP 0xA2 +#define POWER_SAVING 0xE3 + +#include "GxEPD.h" + +#define GxGDEW042Z15_WIDTH 400 +#define GxGDEW042Z15_HEIGHT 300 + +#define GxGDEW042Z15_BUFFER_SIZE (uint32_t(GxGDEW042Z15_WIDTH) * uint32_t(GxGDEW042Z15_HEIGHT) / 8) + +// divisor for AVR, should be factor of GxGDEW042Z15_HEIGHT +#define GxGDEW042Z15_PAGES 20 + +#define GxGDEW042Z15_PAGE_HEIGHT (GxGDEW042Z15_HEIGHT / GxGDEW042Z15_PAGES) +#define GxGDEW042Z15_PAGE_SIZE (GxGDEW042Z15_BUFFER_SIZE / GxGDEW042Z15_PAGES) + +// mapping suggestion from Waveshare 2.7inch e-Paper to Wemos D1 mini +// BUSY -> D2, RST -> D4, DC -> D3, CS -> D8, CLK -> D5, DIN -> D7, GND -> GND, 3.3V -> 3.3V + +// mapping suggestion for ESP32, e.g. LOLIN32, see .../variants/.../pins_arduino.h for your board +// NOTE: there are variants with different pins for SPI ! CHECK SPI PINS OF YOUR BOARD +// BUSY -> 4, RST -> 16, DC -> 17, CS -> SS(5), CLK -> SCK(18), DIN -> MOSI(23), GND -> GND, 3.3V -> 3.3V + +// mapping suggestion for AVR, UNO, NANO etc. +// BUSY -> 7, RST -> 9, DC -> 8, CS-> 10, CLK -> 13, DIN -> 11 + +class GxGDEW042Z15 : public GxEPD +{ + public: +#if defined(ESP8266) + //GxGDEW042Z15(GxIO& io, uint8_t rst = D4, uint8_t busy = D2); + // use pin numbers, other ESP8266 than Wemos may not use Dx names + GxGDEW042Z15(GxIO& io, uint8_t rst = 2, uint8_t busy = 4); +#else + GxGDEW042Z15(GxIO& io, uint8_t rst = 9, uint8_t busy = 7); +#endif + void drawPixel(int16_t x, int16_t y, uint16_t color); + void init(void); + void fillScreen(uint16_t color); // to buffer + void update(void); + // to buffer, may be cropped, drawPixel() used, update needed + void drawBitmap(const uint8_t *bitmap, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color, int16_t mode = bm_normal); + // to full screen, filled with white if size is less, no update needed, black /white / red, for example bitmaps + void drawExamplePicture(const uint8_t* black_bitmap, const uint8_t* red_bitmap, uint32_t black_size, uint32_t red_size); + // to full screen, filled with white if size is less, no update needed, black /white / red, general version + void drawPicture(const uint8_t* black_bitmap, const uint8_t* red_bitmap, uint32_t black_size, uint32_t red_size, int16_t mode = bm_normal); + // to full screen, filled with white if size is less, no update needed + void drawBitmap(const uint8_t *bitmap, uint32_t size, int16_t mode = bm_normal); // only bm_normal, bm_invert modes implemented + void eraseDisplay(bool using_partial_update = false); // parameter ignored + // terminate cleanly, not needed as all screen drawing methods of this class do power down + void powerDown(); + // paged drawing, for limited RAM, drawCallback() is called GxGDEW042Z15_PAGES times + // each call of drawCallback() should draw the same + void drawPaged(void (*drawCallback)(void)); + void drawPaged(void (*drawCallback)(uint32_t), uint32_t); + void drawPaged(void (*drawCallback)(const void*), const void*); + void drawPaged(void (*drawCallback)(const void*, const void*), const void*, const void*); + void drawCornerTest(uint8_t em = 0x01); + void updateWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool usingRotation = true); + private: + void _writeData(uint8_t data); + void _writeCommand(uint8_t command); + void _wakeUp(); + void _sleep(); + void _waitWhileBusy(const char* comment = 0); + private: +#if defined(__AVR) + uint8_t _black_buffer[GxGDEW042Z15_PAGE_SIZE]; + uint8_t _red_buffer[GxGDEW042Z15_PAGE_SIZE]; +#else + uint8_t _black_buffer[GxGDEW042Z15_BUFFER_SIZE]; + uint8_t _red_buffer[GxGDEW042Z15_BUFFER_SIZE]; +#endif + GxIO& IO; + int16_t _current_page; + uint8_t _rst; + uint8_t _busy; +#if defined(ESP8266) || defined(ESP32) + public: + // the compiler of these packages has a problem with signature matching to base classes + void drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h, uint16_t color) + { + Adafruit_GFX::drawBitmap(x, y, bitmap, w, h, color); + }; +#endif +}; + +#define GxEPD_Class GxGDEW042Z15 + +#define GxEPD_WIDTH GxGDEW042Z15_WIDTH +#define GxEPD_HEIGHT GxGDEW042Z15_HEIGHT +#define GxEPD_BitmapExamples +#define GxEPD_BitmapExamplesQ "GxGDEW042Z15/BitmapExamples.h" + +#endif diff --git a/lib/Display/Region.cpp b/lib/Display/Region.cpp new file mode 100644 index 0000000..17f458c --- /dev/null +++ b/lib/Display/Region.cpp @@ -0,0 +1,56 @@ +#include + +Region::Region( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + std::shared_ptr formatter +) : variable(variable), x(x), y(y), w(w), h(h), color(color), formatter(formatter) +{ } + +Region::~Region() { } + +const String& Region::getVariableName() const { + return this->variable; +} + +void Region::clearDirty() { + this->dirty = false; +} + +bool Region::isDirty() const { + return this->dirty; +} + +bool Region::updateValue(const String &value) { + String newValue = formatter->format(value); + + // No change + if (newValue == variableValue) { + return false; + } + + printf("--> formatted: %s\n", newValue.c_str()); + + this->variableValue = newValue; + this->dirty = true; + + return true; +} + +void Region::updateScreen(GxEPD *display) { + uint16_t x, y, w, h; + getBoundingBox(x, y, w, h); + + display->updateWindow(x, y, w, h); +} + +void Region::getBoundingBox(uint16_t& x, uint16_t& y, uint16_t& w, uint16_t& h) { + x = this->x; + y = this->y; + w = this->w; + h = this->h; +} diff --git a/lib/Display/Region.h b/lib/Display/Region.h new file mode 100644 index 0000000..fe5b033 --- /dev/null +++ b/lib/Display/Region.h @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +#ifndef _REGIONS_H +#define _REGIONS_H + +class Region { +public: + Region( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + std::shared_ptr formatter + ); + ~Region(); + + virtual bool updateValue(const String& value); + virtual void render(GxEPD* display) = 0; + + virtual void updateScreen(GxEPD* display); + virtual bool isDirty() const; + virtual void clearDirty(); + virtual const String& getVariableName() const; + virtual void getBoundingBox(uint16_t& x, uint16_t& y, uint16_t& w, uint16_t& h); + +protected: + const String variable; + String variableValue; + uint16_t x; + uint16_t y; + uint16_t w; + uint16_t h; + uint16_t color; + bool dirty; + std::shared_ptr formatter; +}; + +#endif diff --git a/lib/Display/TextRegion.cpp b/lib/Display/TextRegion.cpp new file mode 100644 index 0000000..6e96ca2 --- /dev/null +++ b/lib/Display/TextRegion.cpp @@ -0,0 +1,57 @@ +#include + +TextRegion::TextRegion( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + const GFXfont* font, + std::shared_ptr formatter +) : Region(variable, x, y, w, h, color, formatter), font(font), + bbX(x), bbY(y), + prevX(x), prevY(y), prevW(w), prevH(h) +{ } + +TextRegion::~TextRegion() { } + +void TextRegion::render(GxEPD* display) { + // Clear the previous text + // TODO: expose setting for background color + display->fillRect(this->bbX, this->bbY, this->w, this->h, GxEPD_WHITE); + + display->setTextColor(color); + display->setFont(font); + display->setCursor(x, y); + display->print(variableValue); + + // Find and persist bounding box. Need to persist in case it shrinks next + // time. Update should always be for the larger bounding box. + int16_t x1, y1; + uint16_t w, h; + char valueCpy[variableValue.length() + 1]; + memset(valueCpy, 0, variableValue.length() + 1); + strcpy(valueCpy, variableValue.c_str()); + + display->getTextBounds(valueCpy, x, y, &x1, &y1, &w, &h); + + this->prevX = this->bbX; + this->prevY = this->bbY; + this->prevW = this->w; + this->prevH = this->h; + + this->bbX = x1; + this->bbY = y1; + this->w = w; + this->h = h; + + this->dirty = false; +} + +void TextRegion::getBoundingBox(uint16_t& x, uint16_t& y, uint16_t& w, uint16_t& h) { + x = _min(this->bbX, this->prevX); + y = _min(this->bbY, this->prevY); + w = _max(this->w, this->prevW); + h = _max(this->h, this->prevH); +} diff --git a/lib/Display/TextRegion.h b/lib/Display/TextRegion.h new file mode 100644 index 0000000..bdfa68b --- /dev/null +++ b/lib/Display/TextRegion.h @@ -0,0 +1,39 @@ +#include +#include + +#ifndef _TEXT_REGION_H +#define _TEXT_REGION_H + +class TextRegion : public Region { +public: + TextRegion( + const String& variable, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h, + uint16_t color, + const GFXfont* font, + std::shared_ptr formatter + ); + ~TextRegion(); + + virtual void render(GxEPD* display); + virtual void getBoundingBox(uint16_t& x, uint16_t& y, uint16_t& w, uint16_t& h); + +protected: + const GFXfont* font; + + // Track current bounding box start coordinates separately from (x, y), which + // is the position we set the cursor at. + uint16_t bbX; + uint16_t bbY; + + // Previous bounding box coordinates + uint16_t prevX; + uint16_t prevY; + uint16_t prevW; + uint16_t prevH; +}; + +#endif diff --git a/lib/MQTT/MqttClient.cpp b/lib/MQTT/MqttClient.cpp new file mode 100644 index 0000000..88761f0 --- /dev/null +++ b/lib/MQTT/MqttClient.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include + +MqttClient::MqttClient( + String domain, + uint16_t port, + String variableTopicPattern, + String username, + String password +) : lastConnectAttempt(0), + variableUpdateCallback(NULL), + port(port), + domain(domain), + username(username), + password(password), + topicPattern(variableTopicPattern) +{ + this->topicPatternBuffer = new char[topicPattern.length() + 1]; + strcpy(this->topicPatternBuffer, this->topicPattern.c_str()); + this->topicPatternTokens = new TokenIterator(this->topicPatternBuffer, topicPattern.length(), '/'); + + this->mqttClient = new PubSubClient(tcpClient); +} + +MqttClient::~MqttClient() { + mqttClient->disconnect(); + delete this->topicPatternTokens; + delete this->topicPatternBuffer; +} + +void MqttClient::onVariableUpdate(TVariableUpdateFn fn) { + this->variableUpdateCallback = fn; +} + +void MqttClient::begin() { + mqttClient->setServer(this->domain.c_str(), port); + mqttClient->setCallback( + [this](char* topic, byte* payload, int length) { + this->publishCallback(topic, payload, length); + } + ); + reconnect(); +} + +bool MqttClient::connect() { + char nameBuffer[30]; + sprintf_P(nameBuffer, PSTR("epaper-display-%u"), ESP.getChipId()); + +#ifdef MQTT_DEBUG + Serial.println(F("MqttClient - connecting")); +#endif + + if (username.length() > 0) { + return mqttClient->connect( + nameBuffer, + username.c_str(), + password.c_str() + ); + } else { + return mqttClient->connect(nameBuffer); + } +} + +void MqttClient::reconnect() { + if (lastConnectAttempt > 0 && (millis() - lastConnectAttempt) < MQTT_CONNECTION_ATTEMPT_FREQUENCY) { + return; + } + + if (! mqttClient->connected()) { + if (connect()) { + subscribe(); + +#ifdef MQTT_DEBUG + Serial.println(F("MqttClient - Successfully connected to MQTT server")); +#endif + } else { + Serial.println(F("ERROR: Failed to connect to MQTT server")); + } + } + + lastConnectAttempt = millis(); +} + +void MqttClient::handleClient() { + reconnect(); + mqttClient->loop(); +} + +void MqttClient::subscribe() { + String topic = this->topicPattern; + topic.replace(String(":") + MQTT_TOPIC_VARIABLE_NAME_TOKEN, "+"); + +#ifdef MQTT_DEBUG + printf("MqttClient - subscribing to topic: %s\n", topic.c_str()); +#endif + + mqttClient->subscribe(topic.c_str()); +} + +void MqttClient::publishCallback(char* topic, byte* payload, int length) { + char cstrPayload[length + 1]; + cstrPayload[length] = 0; + memcpy(cstrPayload, payload, sizeof(byte)*length); + +#ifdef MQTT_DEBUG + printf("MqttClient - Got message on topic: %s\n%s\n", topic, cstrPayload); +#endif + + if (this->variableUpdateCallback != NULL) { + TokenIterator topicItr(topic, strlen(topic), '/'); + UrlTokenBindings urlTokens(*topicPatternTokens, topicItr); + + if (urlTokens.hasBinding(MQTT_TOPIC_VARIABLE_NAME_TOKEN)) { + const char* variable = urlTokens.get(MQTT_TOPIC_VARIABLE_NAME_TOKEN); + this->variableUpdateCallback(variable, cstrPayload); + } + } +} diff --git a/lib/MQTT/MqttClient.h b/lib/MQTT/MqttClient.h new file mode 100644 index 0000000..4dd32e3 --- /dev/null +++ b/lib/MQTT/MqttClient.h @@ -0,0 +1,56 @@ +#include +#include +#include + +#ifndef _MQTT_CLIENT_H +#define _MQTT_CLIENT_H + +#define MQTT_TOPIC_VARIABLE_NAME_TOKEN "variable_name" + +#ifndef MQTT_CONNECTION_ATTEMPT_FREQUENCY +#define MQTT_CONNECTION_ATTEMPT_FREQUENCY 5000 +#endif + +class MqttClient { +public: + typedef std::function TVariableUpdateFn; + + MqttClient(String domain, uint16_t port, String variableTopicPattern); + MqttClient( + String domain, + uint16_t port, + String variableTopicPattern, + String username, + String password + ); + ~MqttClient(); + + void begin(); + void handleClient(); + void reconnect(); + void onVariableUpdate(TVariableUpdateFn fn); + +private: + WiFiClient tcpClient; + PubSubClient* mqttClient; + + uint16_t port; + String domain; + String username; + String password; + + unsigned long lastConnectAttempt; + TVariableUpdateFn variableUpdateCallback; + + // This will get reused a bunch. Allows us to avoid copying into a buffer + // every time a message is received. + String topicPattern; + char* topicPatternBuffer; + TokenIterator* topicPatternTokens; + + bool connect(); + void subscribe(); + void publishCallback(char* topic, byte* payload, int length); +}; + +#endif diff --git a/lib/Settings/Settings.cpp b/lib/Settings/Settings.cpp new file mode 100644 index 0000000..37d50da --- /dev/null +++ b/lib/Settings/Settings.cpp @@ -0,0 +1,127 @@ +#include +#include + +#define PORT_POSITION(s) ( s.indexOf(':') ) + +Settings::Settings() + : fullRefreshPeriod(3600000), + hostname("epaper-display"), + onUpdateFn(NULL), + timezoneName(TimezonesClass::DEFAULT_TIMEZONE_NAME) +{ } + +Settings::~Settings() { } + +void Settings::onUpdate(TSettingsUpdateFn fn) { + this->onUpdateFn = fn; +} + +bool Settings::hasAuthSettings() { + return adminUsername.length() > 0 && adminPassword.length() > 0; +} + +void Settings::deserialize(Settings& settings, String json) { + DynamicJsonBuffer jsonBuffer; + JsonObject& parsedSettings = jsonBuffer.parseObject(json); + settings.patch(parsedSettings); +} + +void Settings::patch(JsonObject& parsedSettings) { + if (parsedSettings.success()) { + this->setIfPresent(parsedSettings, "admin_username", adminUsername); + this->setIfPresent(parsedSettings, "admin_password", adminPassword); + this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer); + this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername); + this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword); + this->setIfPresent(parsedSettings, "mqtt_variables_topic_pattern", mqttVariablesTopicPattern); + this->setIfPresent(parsedSettings, "full_refresh_period", fullRefreshPeriod); + this->setIfPresent(parsedSettings, "hostname", hostname); + this->setIfPresent(parsedSettings, "template_path", templatePath); + this->setIfPresent(parsedSettings, "timezone", timezoneName); + + if (this->onUpdateFn != NULL) { + this->onUpdateFn(); + } + } +} + +void Settings::load(Settings& settings) { + if (SPIFFS.exists(SETTINGS_FILE)) { + File f = SPIFFS.open(SETTINGS_FILE, "r"); + String settingsContents = f.readStringUntil(SETTINGS_TERMINATOR); + f.close(); + + deserialize(settings, settingsContents); + } else { + settings.save(); + } +} + +void Settings::save() { + File f = SPIFFS.open(SETTINGS_FILE, "w"); + + if (!f) { + Serial.println(F("Opening settings file failed")); + } else { + serialize(f); + f.close(); + } +} + +void Settings::serialize(Stream& stream, const bool prettyPrint) { + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + + root["admin_username"] = this->adminUsername; + root["admin_password"] = this->adminPassword; + root["mqtt_server"] = this->_mqttServer; + root["mqtt_username"] = this->mqttUsername; + root["mqtt_password"] = this->mqttPassword; + root["mqtt_variables_topic_pattern"] = this->mqttVariablesTopicPattern; + root["full_refresh_period"] = this->fullRefreshPeriod; + root["hostname"] = this->hostname; + root["template_path"] = this->templatePath; + root["timezone"] = this->timezoneName; + + if (prettyPrint) { + root.prettyPrintTo(stream); + } else { + root.printTo(stream); + } +} + +String Settings::toJson(const bool prettyPrint) { + String buffer = ""; + StringStream s(buffer); + serialize(s, prettyPrint); + return buffer; +} + +Timezone& Settings::getTimezone() { + return Timezones.getTimezone(timezoneName); +} + +void Settings::setTimezone(const String &timezone) { + // Resolve to TZ and back to name to make sure it's valid + this->timezoneName = Timezones.getTimezoneName(Timezones.getTimezone(timezone)); +} + +String Settings::mqttServer() { + int pos = PORT_POSITION(_mqttServer); + + if (pos == -1) { + return _mqttServer; + } else { + return _mqttServer.substring(0, pos); + } +} + +uint16_t Settings::mqttPort() { + int pos = PORT_POSITION(_mqttServer); + + if (pos == -1) { + return DEFAULT_MQTT_PORT; + } else { + return atoi(_mqttServer.c_str() + pos + 1); + } +} diff --git a/lib/Settings/Settings.h b/lib/Settings/Settings.h new file mode 100644 index 0000000..a00c136 --- /dev/null +++ b/lib/Settings/Settings.h @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include + +#ifndef _SETTINGS_H +#define _SETTINGS_H + +#ifndef BITMAP_DIRECTORY_PREFIX +#define BITMAP_DIRECTORY_PREFIX "/bitmaps/" +#endif + +#ifndef TEMPLATE_DIRECTORY_PREFIX +#define TEMPLATE_DIRECTORY_PREFIX "/templates/" +#endif + +#define XQUOTE(x) #x +#define QUOTE(x) XQUOTE(x) + +#ifndef FIRMWARE_VARIANT +#define FIRMWARE_VARIANT unknown +#endif + +#ifndef MILIGHT_HUB_VERSION +#define MILIGHT_HUB_VERSION unknown +#endif + +#ifndef MILIGHT_MAX_STATE_ITEMS +#define MILIGHT_MAX_STATE_ITEMS 100 +#endif + +#ifndef MILIGHT_MAX_STALE_MQTT_GROUPS +#define MILIGHT_MAX_STALE_MQTT_GROUPS 10 +#endif + +#define SETTINGS_FILE "/config.json" +#define SETTINGS_TERMINATOR '\0' + +#define DEFAULT_MQTT_PORT 1883 + +class Settings { +public: + typedef std::function TSettingsUpdateFn; + + Settings(); + ~Settings(); + + void onUpdate(TSettingsUpdateFn fn); + + bool hasAuthSettings(); + + static void deserialize(Settings& settings, String json); + static void load(Settings& settings); + String toJson(const bool prettyPrint = true); + void save(); + void serialize(Stream& stream, const bool prettyPrint = false); + void patch(JsonObject& obj); + + Timezone& getTimezone(); + void setTimezone(const String& timezone); + + String adminUsername; + String adminPassword; + + String mqttServer(); + uint16_t mqttPort(); + String _mqttServer; + String mqttUsername; + String mqttPassword; + String mqttVariablesTopicPattern; + unsigned long fullRefreshPeriod; + String hostname; + String templatePath; + +protected: + template + void setIfPresent(JsonObject& obj, const char* key, T& var) { + if (obj.containsKey(key)) { + var = obj.get(key); + } + } + + TSettingsUpdateFn onUpdateFn; + String timezoneName; +}; + +#endif diff --git a/lib/Settings/StringStream.h b/lib/Settings/StringStream.h new file mode 100644 index 0000000..15d212e --- /dev/null +++ b/lib/Settings/StringStream.h @@ -0,0 +1,29 @@ +/* +* Adapated from https://gist.github.com/cmaglie/5883185 +*/ + +#ifndef _STRING_STREAM_H_INCLUDED_ +#define _STRING_STREAM_H_INCLUDED_ + +#include + +class StringStream : public Stream +{ +public: + StringStream(String &s) : string(s), position(0) { } + + // Stream methods + virtual int available() { return string.length() - position; } + virtual int read() { return position < string.length() ? string[position++] : -1; } + virtual int peek() { return position < string.length() ? string[position] : -1; } + virtual void flush() { }; + // Print methods + virtual size_t write(uint8_t c) { string += (char)c; }; + +private: + String &string; + unsigned int length; + unsigned int position; +}; + +#endif // _STRING_STREAM_H_INCLUDED_ diff --git a/lib/Time/Timezones.cpp b/lib/Time/Timezones.cpp new file mode 100644 index 0000000..1a2f395 --- /dev/null +++ b/lib/Time/Timezones.cpp @@ -0,0 +1,65 @@ +#include + +//United Kingdom (London, Belfast) +static TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60}; //British Summer Time +static TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0}; //Standard Time +static Timezone UK(BST, GMT); + +//US Eastern Time Zone (New York, Detroit) +static TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; //Eastern Daylight Time = UTC - 4 hours +static TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; //Eastern Standard Time = UTC - 5 hours +static Timezone usET(usEDT, usEST); + +//US Central Time Zone (Chicago, Houston) +static TimeChangeRule usCDT = {"CDT", Second, dowSunday, Mar, 2, -300}; +static TimeChangeRule usCST = {"CST", First, dowSunday, Nov, 2, -360}; +static Timezone usCT(usCDT, usCST); + +//US Mountain Time Zone (Denver, Salt Lake City) +static TimeChangeRule usMDT = {"MDT", Second, dowSunday, Mar, 2, -360}; +static TimeChangeRule usMST = {"MST", First, dowSunday, Nov, 2, -420}; +static Timezone usMT(usMDT, usMST); + +//Arizona is US Mountain Time Zone but does not use DST +static Timezone usAZ(usMST, usMST); + +//US Pacific Time Zone (Las Vegas, Los Angeles) +static TimeChangeRule usPDT = {"PDT", Second, dowSunday, Mar, 2, -420}; +static TimeChangeRule usPST = {"PST", First, dowSunday, Nov, 2, -480}; +static Timezone usPT(usPDT, usPST); + +Timezone& TimezonesClass::DEFAULT_TIMEZONE = usPT; +const char* TimezonesClass::DEFAULT_TIMEZONE_NAME = "PT"; + +TimezonesClass::TimezonesClass() { + timezonesByName["UK"] = &UK; + timezonesByName["ET"] = &usET; + timezonesByName["CT"] = &usCT; + timezonesByName["MT"] = &usMT; + timezonesByName["AZ"] = &usAZ; + timezonesByName["PT"] = &usPT; +} + +TimezonesClass::~TimezonesClass() { } + +bool TimezonesClass::hasTimezone(const String& tzName) { + return timezonesByName.count(tzName) > 0; +} + +Timezone& TimezonesClass::getTimezone(const String& tzName) { + if (hasTimezone(tzName)) { + return *timezonesByName[tzName]; + } + return DEFAULT_TIMEZONE; +} + +String TimezonesClass::getTimezoneName(Timezone &tz) { + for (std::map::iterator itr = timezonesByName.begin(); itr != timezonesByName.end(); ++itr) { + if (itr->second == &tz) { + return itr->first; + } + } + return ""; +} + +TimezonesClass Timezones; diff --git a/lib/Time/Timezones.h b/lib/Time/Timezones.h new file mode 100644 index 0000000..c065e4a --- /dev/null +++ b/lib/Time/Timezones.h @@ -0,0 +1,25 @@ +#include +#include + +#ifndef _TIMEZONES_H +#define _TIMEZONES_H + +class TimezonesClass { +public: + TimezonesClass(); + ~TimezonesClass(); + + static const char* DEFAULT_TIMEZONE_NAME; + static Timezone& DEFAULT_TIMEZONE; + + bool hasTimezone(const String& tzName); + Timezone& getTimezone(const String& tzName); + String getTimezoneName(Timezone& tz); + +private: + std::map timezonesByName; +}; + +extern TimezonesClass Timezones; + +#endif diff --git a/lib/TokenParsing/TokenIterator.cpp b/lib/TokenParsing/TokenIterator.cpp new file mode 100644 index 0000000..5bb5989 --- /dev/null +++ b/lib/TokenParsing/TokenIterator.cpp @@ -0,0 +1,51 @@ +#include + +TokenIterator::TokenIterator(char* data, size_t length, const char sep) + : data(data), + current(data), + length(length), + sep(sep), + i(0) +{ + for (size_t i = 0; i < length; i++) { + if (data[i] == sep) { + data[i] = 0; + } + } +} + +const char* TokenIterator::nextToken() { + if (i >= length) { + return NULL; + } + + char* token = current; + char* nextToken = current; + + for (; i < length && *nextToken != 0; i++, nextToken++); + + if (i == length) { + nextToken = NULL; + } else { + i = (nextToken - data); + + if (i < length) { + nextToken++; + } else { + nextToken = NULL; + } + } + + current = nextToken; + + return token; +} + +void TokenIterator::reset() { + current = data; + i = 0; +} + +bool TokenIterator::hasNext() { + return i < length; +} diff --git a/lib/TokenParsing/TokenIterator.h b/lib/TokenParsing/TokenIterator.h new file mode 100644 index 0000000..ed824db --- /dev/null +++ b/lib/TokenParsing/TokenIterator.h @@ -0,0 +1,21 @@ +#include + +#ifndef _TOKEN_ITERATOR_H +#define _TOKEN_ITERATOR_H + +class TokenIterator { +public: + TokenIterator(char* data, size_t length, char sep = ','); + + bool hasNext(); + const char* nextToken(); + void reset(); + +private: + char* data; + char* current; + size_t length; + char sep; + int i; +}; +#endif diff --git a/lib/TokenParsing/UrlTokenBindings.cpp b/lib/TokenParsing/UrlTokenBindings.cpp new file mode 100644 index 0000000..69b1b6d --- /dev/null +++ b/lib/TokenParsing/UrlTokenBindings.cpp @@ -0,0 +1,35 @@ +#include + +UrlTokenBindings::UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens) + : patternTokens(patternTokens), + requestTokens(requestTokens) +{ } + +bool UrlTokenBindings::hasBinding(const char* searchToken) const { + patternTokens.reset(); + while (patternTokens.hasNext()) { + const char* token = patternTokens.nextToken(); + + if (token[0] == ':' && strcmp(token+1, searchToken) == 0) { + return true; + } + } + + return false; +} + +const char* UrlTokenBindings::get(const char* searchToken) const { + patternTokens.reset(); + requestTokens.reset(); + + while (patternTokens.hasNext() && requestTokens.hasNext()) { + const char* token = patternTokens.nextToken(); + const char* binding = requestTokens.nextToken(); + + if (token[0] == ':' && strcmp(token+1, searchToken) == 0) { + return binding; + } + } + + return NULL; +} diff --git a/lib/TokenParsing/UrlTokenBindings.h b/lib/TokenParsing/UrlTokenBindings.h new file mode 100644 index 0000000..9d23f6d --- /dev/null +++ b/lib/TokenParsing/UrlTokenBindings.h @@ -0,0 +1,18 @@ +#include + +#ifndef _URL_TOKEN_BINDINGS_H +#define _URL_TOKEN_BINDINGS_H + +class UrlTokenBindings { +public: + UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens); + + bool hasBinding(const char* key) const; + const char* get(const char* key) const; + +private: + TokenIterator& patternTokens; + TokenIterator& requestTokens; +}; + +#endif diff --git a/lib/Variables/CasesFormatter.cpp b/lib/Variables/CasesFormatter.cpp new file mode 100644 index 0000000..6d2589e --- /dev/null +++ b/lib/Variables/CasesFormatter.cpp @@ -0,0 +1,23 @@ +#include + +CasesVariableFormatter::CasesVariableFormatter(const JsonObject& args) { + JsonObject& cases = args["cases"]; + for (JsonObject::iterator itr = cases.begin(); itr != cases.end(); ++itr) { + this->cases[itr->key] = itr->value.as(); + } + + this->defaultValue = args.get("default"); + this->prefix = args.get("prefix"); +} + +String CasesVariableFormatter::format(const String& value) const { + String result = prefix; + + if (cases.count(value) > 0) { + result += cases.at(value); + } else { + result += defaultValue; + } + + return result; +} diff --git a/lib/Variables/TimeFormatter.cpp b/lib/Variables/TimeFormatter.cpp new file mode 100644 index 0000000..c16627d --- /dev/null +++ b/lib/Variables/TimeFormatter.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include + +static const char FORMAT_ARG_NAME[] = "format"; +static const char TIMEZONE_ARG_NAME[] = "timezone"; + +const char TimeVariableFormatter::DEFAULT_TIME_FORMAT[] = "%H:%M"; + +TimeVariableFormatter::TimeVariableFormatter(const String& timeFormat, Timezone& timezone) + : timeFormat(timeFormat), + timezone(timezone) +{ } + +std::shared_ptr TimeVariableFormatter::build(const JsonObject& args) { + Timezone& tz = Timezones.getTimezone(args[TIMEZONE_ARG_NAME]); + String timeFormat; + + if (args.containsKey(FORMAT_ARG_NAME)) { + timeFormat = args.get(FORMAT_ARG_NAME); + } else { + timeFormat = DEFAULT_TIME_FORMAT; + } + + return std::shared_ptr(new TimeVariableFormatter(timeFormat, tz)); +} + +String TimeVariableFormatter::format(const String &value) const { + time_t parsedTime = value.toInt(); + parsedTime = timezone.toLocal(parsedTime); + + char buffer[100]; + memset(buffer, 0, sizeof(buffer)); + struct tm* tminfo = localtime(&parsedTime); + + strftime(buffer, sizeof(buffer), timeFormat.c_str(), tminfo); + + return buffer; +} diff --git a/lib/Variables/VariableDictionary.cpp b/lib/Variables/VariableDictionary.cpp new file mode 100644 index 0000000..fdc7966 --- /dev/null +++ b/lib/Variables/VariableDictionary.cpp @@ -0,0 +1,46 @@ +#include +#include + +static const char DEFAULT_VALUE[] = ""; +const char VariableDictionary::FILENAME[] = "/variables.json"; + +VariableDictionary::VariableDictionary() + : dict(&buffer.createObject()) +{ } + +void VariableDictionary::load() { + buffer.clear(); + + File f = SPIFFS.open(FILENAME, "r"); + dict = &buffer.parseObject(f); + f.close(); +} + +void VariableDictionary::save() { + File f = SPIFFS.open(FILENAME, "w"); + dict->printTo(f); + f.close(); +} + +void VariableDictionary::registerVariable(const String& key) { + if (! containsKey(key)) { + set(key, ""); + } +} + +bool VariableDictionary::containsKey(const String &key) { + return dict->containsKey(key); +} + +void VariableDictionary::set(const String &key, const String &value) { + dict->set(key, value); + save(); +} + +String VariableDictionary::get(const String &key) { + if (dict->containsKey(key)) { + return dict->get(key); + } else { + return DEFAULT_VALUE; + } +} diff --git a/lib/Variables/VariableDictionary.h b/lib/Variables/VariableDictionary.h new file mode 100644 index 0000000..013eaa9 --- /dev/null +++ b/lib/Variables/VariableDictionary.h @@ -0,0 +1,25 @@ +#include + +#ifndef VARIABLE_DICTIONARY +#define VARIABLE_DICTIONARY + +class VariableDictionary { +public: + static const char FILENAME[]; + + VariableDictionary(); + + String get(const String& key); + void set(const String& key, const String& value); + void registerVariable(const String& key); + bool containsKey(const String& key); + + void save(); + void load(); + +private: + DynamicJsonBuffer buffer; + JsonObject* dict; +}; + +#endif diff --git a/lib/Variables/VariableFormatters.cpp b/lib/Variables/VariableFormatters.cpp new file mode 100644 index 0000000..65c1002 --- /dev/null +++ b/lib/Variables/VariableFormatters.cpp @@ -0,0 +1,18 @@ +#include +#include + +std::shared_ptr VariableFormatter::buildFormatter(const JsonObject& args) { + String formatter = args["formatter"]; + + if (formatter.equalsIgnoreCase("time")) { + return TimeVariableFormatter::build(args["args"]); + } else if (formatter.equalsIgnoreCase("cases")) { + return std::shared_ptr(new CasesVariableFormatter(args.get("args"))); + } else { + return std::shared_ptr(new IdentityVariableFormatter()); + } +} + +String IdentityVariableFormatter::format(const String& value) const { + return value; +} diff --git a/lib/Variables/VariableFormatters.h b/lib/Variables/VariableFormatters.h new file mode 100644 index 0000000..7491be5 --- /dev/null +++ b/lib/Variables/VariableFormatters.h @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#ifndef _VARIABLE_FORMATTER_H +#define _VARIABLE_FORMATTER_H + +class VariableFormatter { +public: + virtual String format(const String& value) const = 0; + + ~VariableFormatter() { Serial.println("~VF"); } + + static std::shared_ptr buildFormatter(const JsonObject& args); +}; + +class IdentityVariableFormatter : public VariableFormatter { +public: + virtual String format(const String& value) const; +}; + +class TimeVariableFormatter : public VariableFormatter { +public: + static const char DEFAULT_TIME_FORMAT[]; + + TimeVariableFormatter(const String& timeFormat, Timezone& timezone); + + virtual String format(const String& value) const; + static std::shared_ptr build(const JsonObject& args); + +protected: + String timeFormat; + Timezone& timezone; +}; + +class CasesVariableFormatter : public VariableFormatter { +public: + CasesVariableFormatter(const JsonObject& args); + + virtual String format(const String& value) const; + +protected: + std::map cases; + String defaultValue; + String prefix; +}; + +#endif diff --git a/lib/WebServer/PatternHandler.cpp b/lib/WebServer/PatternHandler.cpp new file mode 100644 index 0000000..60ae79b --- /dev/null +++ b/lib/WebServer/PatternHandler.cpp @@ -0,0 +1,62 @@ +#include + +PatternHandler::PatternHandler( + const String& pattern, + const HTTPMethod method, + const PatternHandler::TPatternHandlerFn fn) + : method(method), + fn(fn), + _pattern(new char[pattern.length() + 1]), + patternTokens(NULL) +{ + strcpy(_pattern, pattern.c_str()); + patternTokens = new TokenIterator(_pattern, pattern.length(), '/'); +} + +PatternHandler::~PatternHandler() { + delete _pattern; + delete patternTokens; +} + +bool PatternHandler::canHandle(HTTPMethod requestMethod, String requestUri) { + if (this->method != HTTP_ANY && requestMethod != this->method) { + return false; + } + + bool canHandle = true; + + char requestUriCopy[requestUri.length() + 1]; + strcpy(requestUriCopy, requestUri.c_str()); + TokenIterator requestTokens(requestUriCopy, requestUri.length(), '/'); + + patternTokens->reset(); + while (patternTokens->hasNext() && requestTokens.hasNext()) { + const char* patternToken = patternTokens->nextToken(); + const char* requestToken = requestTokens.nextToken(); + + if (patternToken[0] != ':' && strcmp(patternToken, requestToken) != 0) { + canHandle = false; + break; + } + + if (patternTokens->hasNext() != requestTokens.hasNext()) { + canHandle = false; + break; + } + } + + return canHandle; +} + +bool PatternHandler::handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) { + if (! canHandle(requestMethod, requestUri)) { + return false; + } + + char requestUriCopy[requestUri.length()]; + strcpy(requestUriCopy, requestUri.c_str()); + TokenIterator requestTokens(requestUriCopy, requestUri.length(), '/'); + + UrlTokenBindings bindings(*patternTokens, requestTokens); + fn(&bindings); +} diff --git a/lib/WebServer/PatternHandler.h b/lib/WebServer/PatternHandler.h new file mode 100644 index 0000000..54d5107 --- /dev/null +++ b/lib/WebServer/PatternHandler.h @@ -0,0 +1,30 @@ +#ifndef _PATTERNHANDLER_H +#define _PATTERNHANDLER_H + +#include +#include +#include +#include +#include + +class PatternHandler : public RequestHandler { +public: + typedef std::function TPatternHandlerFn; + + PatternHandler(const String& pattern, + const HTTPMethod method, + const TPatternHandlerFn fn); + + ~PatternHandler(); + + bool canHandle(HTTPMethod requestMethod, String requestUri) override; + bool handle(ESP8266WebServer& server, HTTPMethod requesetMethod, String requestUri) override; + +private: + char* _pattern; + TokenIterator* patternTokens; + const HTTPMethod method; + const PatternHandler::TPatternHandlerFn fn; +}; + +#endif diff --git a/lib/WebServer/WebServer.cpp b/lib/WebServer/WebServer.cpp new file mode 100644 index 0000000..8d4bbee --- /dev/null +++ b/lib/WebServer/WebServer.cpp @@ -0,0 +1,403 @@ +#include + +static const char INDEX_FILENAME[] = "/index.html"; +static const char TEXT_HTML[] = "text/html"; +static const char TEXT_PLAIN[] = "text/plain"; +static const char APPLICATION_JSON[] = "application/json"; + +static const char CONTENT_TYPE_HEADER[] = "Content-Type"; + +WebServer::WebServer(DisplayTemplateDriver& driver, Settings& settings) + : driver(driver), + settings(settings) +{ } + +void WebServer::begin() { + on("/", HTTP_GET, handleServeFile(INDEX_FILENAME, TEXT_HTML)); + on("/index.html", HTTP_POST, sendSuccess(), handleUpdateFile(INDEX_FILENAME)); + on("/variables", HTTP_PUT, [this]() { handleUpdateVariables(); }); + on("/variables", HTTP_GET, handleServeFile(VariableDictionary::FILENAME, APPLICATION_JSON)); + + on("/templates", HTTP_GET, handleListTemplates()); + on("/templates", HTTP_POST, [this]() { sendSuccess(); }, handleCreateTemplate()); + onPattern("/templates/:filename", HTTP_DELETE, handleDeleteTemplate()); + onPattern("/templates/:filename", HTTP_GET, handleShowTemplate()); + onPattern("/templates/:filename", HTTP_PUT, handleUpdateTemplate()); + + on("/bitmaps", HTTP_GET, handleListBitmaps()); + on("/bitmaps", HTTP_POST, [this]() { sendSuccess(); }, handleCreateBitmap()); + onPattern("/bitmaps/:filename", HTTP_DELETE, handleDeleteBitmap()); + onPattern("/bitmaps/:filename", HTTP_GET, handleShowBitmap()); + + on("/settings", HTTP_GET, handleListSettings()); + on("/settings", HTTP_PUT, handleUpdateSettings()); + + on("/about", HTTP_GET, handleAbout()); + + server.begin(); +} + +void WebServer::loop() { + server.handleClient(); +} + +ESP8266WebServer::THandlerFunction WebServer::handleAbout() { + return [this]() { + // Measure before allocating buffers + uint32_t freeHeap = ESP.getFreeHeap(); + + StaticJsonBuffer<50> buffer; + JsonObject& res = buffer.createObject(); + + res["free_heap"] = freeHeap; + + String body; + res.printTo(body); + server.send(200, APPLICATION_JSON, body); + }; +} + +ESP8266WebServer::THandlerFunction WebServer::sendSuccess() { + return [this]() { + server.send(200, APPLICATION_JSON, "true"); + }; +} + +void WebServer::handleUpdateVariables() { + DynamicJsonBuffer buffer; + JsonObject& vars = buffer.parseObject(server.arg("plain")); + + for (JsonObject::iterator itr = vars.begin(); itr != vars.end(); ++itr) { + driver.updateVariable(itr->key, itr->value); + } + + server.send(200, "application/json", "true"); +} + +ESP8266WebServer::THandlerFunction WebServer::handleServeFile( + const char* filename, + const char* contentType, + const char* defaultText) { + + return [this, filename, contentType, defaultText]() { + if (!serveFile(filename, contentType)) { + if (defaultText) { + server.send(200, contentType, defaultText); + } else { + server.send(404); + } + } + }; +} + +bool WebServer::serveFile(const char* file, const char* contentType) { + if (SPIFFS.exists(file)) { + File f = SPIFFS.open(file, "r"); + server.streamFile(f, contentType); + f.close(); + return true; + } + + return false; +} + +// --------- +// CRUD handlers for bitmaps +// --------- + +PatternHandler::TPatternHandlerFn WebServer::handleShowBitmap() { + return [this](const UrlTokenBindings* bindings) { + if (bindings->hasBinding("filename")) { + const char* filename = bindings->get("filename"); + String path = String(BITMAP_DIRECTORY_PREFIX) + filename; + + if (SPIFFS.exists(path)) { + File file = SPIFFS.open(path, "r"); + server.streamFile(file, "application/octet-stream"); + file.close(); + } else { + server.send(404, TEXT_PLAIN); + } + } else { + server.send_P(400, TEXT_PLAIN, PSTR("You must provide a filename")); + } + }; +} + +PatternHandler::TPatternHandlerFn WebServer::handleDeleteBitmap() { + return [this](const UrlTokenBindings* bindings) { + if (bindings->hasBinding("filename")) { + const char* filename = bindings->get("filename"); + String path = String(BITMAP_DIRECTORY_PREFIX) + filename; + + if (SPIFFS.exists(path)) { + if (SPIFFS.remove(path)) { + server.send_P(200, TEXT_PLAIN, PSTR("success")); + } else { + server.send_P(500, TEXT_PLAIN, PSTR("Failed to delete file")); + } + } else { + server.send(404, TEXT_PLAIN); + } + } else { + server.send_P(400, TEXT_PLAIN, PSTR("You must provide a filename")); + } + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleListBitmaps() { + return [this]() { + DynamicJsonBuffer buffer; + JsonArray& responseObj = buffer.createArray(); + + Dir bitmapDir = SPIFFS.openDir(BITMAP_DIRECTORY_PREFIX); + + while (bitmapDir.next()) { + JsonObject& file = buffer.createObject(); + file["name"] = bitmapDir.fileName(); + file["size"] = bitmapDir.fileSize(); + responseObj.add(file); + } + + String response; + responseObj.printTo(response); + + server.send(200, APPLICATION_JSON, response); + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleCreateBitmap() { + return [this]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + String filename = String(BITMAP_DIRECTORY_PREFIX) + upload.filename; + updateFile = SPIFFS.open(filename, "w"); + } else if(upload.status == UPLOAD_FILE_WRITE){ + if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) { + Serial.println(F("Error creating bitmap - write failed")); + } + } else if (upload.status == UPLOAD_FILE_END) { + updateFile.close(); + } + }; +} + +// --------- +// CRUD handlers for templates +// --------- + +PatternHandler::TPatternHandlerFn WebServer::handleShowTemplate() { + return [this](const UrlTokenBindings* bindings) { + if (bindings->hasBinding("filename")) { + const char* filename = bindings->get("filename"); + String path = String(TEMPLATE_DIRECTORY_PREFIX) + filename; + + if (SPIFFS.exists(path)) { + File file = SPIFFS.open(path, "r"); + server.streamFile(file, APPLICATION_JSON); + file.close(); + + server.send(200, APPLICATION_JSON); + } else { + server.send(404, TEXT_PLAIN); + } + } else { + server.send_P(400, TEXT_PLAIN, PSTR("You must provide a filename")); + } + }; +} + +PatternHandler::TPatternHandlerFn WebServer::handleUpdateTemplate() { + return [this](const UrlTokenBindings* bindings) { + if (bindings->hasBinding("filename")) { + const char* filename = bindings->get("filename"); + String path = String(TEMPLATE_DIRECTORY_PREFIX) + filename; + handleUpdateJsonFile(path); + } + }; +} + +PatternHandler::TPatternHandlerFn WebServer::handleDeleteTemplate() { + return [this](const UrlTokenBindings* bindings) { + if (bindings->hasBinding("filename")) { + const char* filename = bindings->get("filename"); + String path = String(BITMAP_DIRECTORY_PREFIX) + filename; + + if (SPIFFS.exists(path)) { + if (SPIFFS.remove(path)) { + server.send_P(200, TEXT_PLAIN, PSTR("success")); + } else { + server.send_P(500, TEXT_PLAIN, PSTR("Failed to delete file")); + } + } else { + server.send(404, TEXT_PLAIN); + } + } else { + server.send_P(400, TEXT_PLAIN, PSTR("You must provide a filename")); + } + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleListTemplates() { + return [this]() { + DynamicJsonBuffer buffer; + JsonArray& responseObj = buffer.createArray(); + + Dir bitmapDir = SPIFFS.openDir(TEMPLATE_DIRECTORY_PREFIX); + + while (bitmapDir.next()) { + JsonObject& file = buffer.createObject(); + file["name"] = bitmapDir.fileName(); + file["size"] = bitmapDir.fileSize(); + responseObj.add(file); + } + + String response; + responseObj.printTo(response); + + server.send(200, APPLICATION_JSON, response); + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleCreateTemplate() { + return [this]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + String filename = String(TEMPLATE_DIRECTORY_PREFIX) + upload.filename; + updateFile = SPIFFS.open(filename, "w"); + } else if(upload.status == UPLOAD_FILE_WRITE){ + if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) { + Serial.println(F("Error creating template - write failed")); + } + } else if (upload.status == UPLOAD_FILE_END) { + updateFile.close(); + } + }; +} + +void WebServer::handleUpdateJsonFile(const String& path) { + DynamicJsonBuffer requestBuffer; + JsonObject& request = requestBuffer.parseObject(server.arg("plain")); + + if (! request.success()) { + server.send_P(400, TEXT_PLAIN, PSTR("Invalid JSON")); + return; + } + + if (SPIFFS.exists(path)) { + File file = SPIFFS.open(path, "r"); + + DynamicJsonBuffer fileBuffer; + JsonObject& tmpl = fileBuffer.parse(file); + file.close(); + + if (! tmpl.success()) { + server.send_P(500, TEXT_PLAIN, PSTR("Failed to load template file")); + return; + } + + for (JsonObject::iterator itr = request.begin(); itr != request.end(); ++itr) { + tmpl[itr->key] = itr->value; + } + + file = SPIFFS.open(path, "w"); + tmpl.printTo(file); + file.close(); + + String body; + tmpl.printTo(body); + server.send(200, APPLICATION_JSON, body); + } else { + server.send(404, TEXT_PLAIN); + } +} + +ESP8266WebServer::THandlerFunction WebServer::handleUpdateSettings() { + return [this]() { + DynamicJsonBuffer buffer; + JsonObject& req = buffer.parse(server.arg("plain")); + + if (! req.success()) { + server.send_P(400, TEXT_PLAIN, PSTR("Invalid JSON")); + return; + } + + settings.patch(req); + settings.save(); + + server.send(200); + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleListSettings() { + return [this]() { + server.send(200, APPLICATION_JSON, settings.toJson()); + }; +} + +ESP8266WebServer::THandlerFunction WebServer::handleUpdateFile(const char* filename) { + return [this, filename]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + updateFile = SPIFFS.open(filename, "w"); + } else if(upload.status == UPLOAD_FILE_WRITE){ + if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) { + Serial.println(F("Error updating web file")); + } + } else if (upload.status == UPLOAD_FILE_END) { + updateFile.close(); + } + }; +} + +bool WebServer::isAuthenticated() { + if (settings.hasAuthSettings()) { + if (server.authenticate(settings.adminUsername.c_str(), settings.adminPassword.c_str())) { + return true; + } else { + server.send_P(403, TEXT_PLAIN, PSTR("Authentication required")); + return false; + } + } else { + return true; + } +} + +void WebServer::onPattern(const String& pattern, const HTTPMethod method, PatternHandler::TPatternHandlerFn fn) { + PatternHandler::TPatternHandlerFn authedFn = [this, fn](const UrlTokenBindings* b) { + if (isAuthenticated()) { + fn(b); + } + }; + + server.addHandler(new PatternHandler(pattern, method, authedFn)); +} + +void WebServer::on(const String& path, const HTTPMethod method, ESP8266WebServer::THandlerFunction fn) { + ESP8266WebServer::THandlerFunction authedFn = [this, fn]() { + if (isAuthenticated()) { + fn(); + } + }; + + server.on(path, method, authedFn); +} + +void WebServer::on(const String& path, const HTTPMethod method, ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction uploadFn) { + ESP8266WebServer::THandlerFunction authedFn = [this, fn]() { + if (isAuthenticated()) { + fn(); + } + }; + + ESP8266WebServer::THandlerFunction authedUploadFn = [this, uploadFn]() { + if (isAuthenticated()) { + uploadFn(); + } + }; + + server.on(path, method, authedFn, authedUploadFn); +} diff --git a/lib/WebServer/WebServer.h b/lib/WebServer/WebServer.h new file mode 100644 index 0000000..ebffe5d --- /dev/null +++ b/lib/WebServer/WebServer.h @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + +#ifndef _WEB_SERVER_H +#define _WEB_SERVER_H + +class WebServer { +public: + WebServer(DisplayTemplateDriver& driver, Settings& settings); + + void begin(); + void loop(); + +private: + ESP8266WebServer server; + File updateFile; + DisplayTemplateDriver& driver; + Settings& settings; + + void handleUpdateVariables(); + ESP8266WebServer::THandlerFunction sendSuccess(); + + ESP8266WebServer::THandlerFunction handleAbout(); + + // CRUD handlers for Bitmaps + ESP8266WebServer::THandlerFunction handleListBitmaps(); + ESP8266WebServer::THandlerFunction handleCreateBitmap(); + PatternHandler::TPatternHandlerFn handleDeleteBitmap(); + PatternHandler::TPatternHandlerFn handleShowBitmap(); + + // CRUD handlers for Templates + ESP8266WebServer::THandlerFunction handleListTemplates(); + ESP8266WebServer::THandlerFunction handleCreateTemplate(); + PatternHandler::TPatternHandlerFn handleDeleteTemplate(); + PatternHandler::TPatternHandlerFn handleShowTemplate(); + PatternHandler::TPatternHandlerFn handleUpdateTemplate(); + + ESP8266WebServer::THandlerFunction handleUpdateSettings(); + ESP8266WebServer::THandlerFunction handleListSettings(); + + ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename); + ESP8266WebServer::THandlerFunction handleServeFile( + const char* filename, + const char* contentType, + const char* defaultText = "" + ); + bool serveFile(const char* file, const char* contentType); + void handleUpdateJsonFile(const String& file); + + // Checks if auth is enabled, and requires appropriate username/password if so + bool isAuthenticated(); + + // Support for routes with tokens like a/:id/:id2. Injects auth handling. + void onPattern(const String& pattern, const HTTPMethod method, PatternHandler::TPatternHandlerFn fn); + + // Injects auth handling + void on(const String& pattern, const HTTPMethod method, ESP8266WebServer::THandlerFunction fn); + void on(const String& pattern, const HTTPMethod method, ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction uploadFn); +}; + +#endif diff --git a/lib/readme.txt b/lib/readme.txt new file mode 100644 index 0000000..dbadc3d --- /dev/null +++ b/lib/readme.txt @@ -0,0 +1,36 @@ + +This directory is intended for the project specific (private) libraries. +PlatformIO will compile them to static libraries and link to executable file. + +The source code of each library should be placed in separate directory, like +"lib/private_lib/[here are source files]". + +For example, see how can be organized `Foo` and `Bar` libraries: + +|--lib +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| |--Foo +| | |- Foo.c +| | |- Foo.h +| |- readme.txt --> THIS FILE +|- platformio.ini +|--src + |- main.c + +Then in `src/main.c` you should use: + +#include +#include + +// rest H/C/CPP code + +PlatformIO will find your libraries automatically, configure preprocessor's +include paths and build them. + +More information about PlatformIO Library Dependency Finder +- http://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..878640e --- /dev/null +++ b/platformio.ini @@ -0,0 +1,62 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; http://docs.platformio.org/page/projectconf.html + +[common] +framework = arduino +platform = https://github.com/platformio/platform-espressif8266.git#feature/stage +board_f_cpu = 80000000L +lib_deps_builtin = +lib_deps_external = + https://github.com/adafruit/Adafruit-GFX-Library + https://github.com/ZinggJM/GxEPD + ArduinoJson + https://github.com/willjoha/Timezone + Time + NtpClientLib + PubSubClient +lib_ldf_mode = deep +extra_scripts = +build_flags = -DMQTT_DEBUG -DHTTP_UPLOAD_BUFLEN=20 + +[env:nodemcuv2] +platform = ${common.platform} +framework = ${common.framework} +board = nodemcuv2 +upload_speed = 460800 +build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=nodemcuv2 +extra_scripts = ${common.extra_scripts} +lib_ldf_mode = ${common.lib_ldf_mode} +lib_deps = + ${common.lib_deps_builtin} + ${common.lib_deps_external} + +; [env:esp32] +; platform = espressif32 +; framework = ${common.framework} +; board = esp32doit-devkit-v1 +; upload_speed = 115200 +; build_flags = ${common.build_flags} -D FIRMWARE_VARIANT=esp32-doit +; extra_scripts = ${common.extra_scripts} +; lib_ldf_mode = ${common.lib_ldf_mode} +; lib_deps = +; ${common.lib_deps_builtin} +; ${common.lib_deps_external} + +; [env:d1_mini] +; platform = ${common.platform} +; framework = ${common.framework} +; board = d1_mini +; upload_speed = 460800 +; build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=d1_mini +; extra_scripts = ${common.extra_scripts} +; lib_ldf_mode = ${common.lib_ldf_mode} +; lib_deps = +; ${common.lib_deps_builtin} +; ${common.lib_deps_external} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..20d0417 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +GxIO_Class io(SPI, SS, D3, D4); +GxEPD_Class display(io); + +Settings settings; +// Config config; + +DisplayTemplateDriver driver(&display, settings); +WebServer webServer(driver, settings); +MqttClient* mqttClient = NULL; + +uint8_t lastSecond = 60; + +void applySettings() { + if (mqttClient != NULL) { + delete mqttClient; + mqttClient = NULL; + } + + if (settings.mqttServer().length() > 0) { + mqttClient = new MqttClient( + settings.mqttServer(), + settings.mqttPort(), + settings.mqttVariablesTopicPattern, + settings.mqttUsername, + settings.mqttPassword + ); + mqttClient->onVariableUpdate([](const String& variable, const String& value) { + driver.updateVariable(variable, value); + }); + mqttClient->begin(); + } + + if (settings.templatePath.length() > 0) { + driver.setTemplate(settings.templatePath); + } +} + +void setup() { + Serial.begin(115200); + SPIFFS.begin(); + + Settings::load(settings); + settings.onUpdate(applySettings); + + WiFi.hostname(settings.hostname); + WiFi.begin(); + WiFi.waitForConnectResult(); + + NTP.begin("pool.ntp.org", 0, false); + NTP.setInterval(63); + + webServer.begin(); + driver.init(); + + applySettings(); + + Serial.println(WiFi.localIP().toString()); + Serial.println(ESP.getFreeHeap()); +} + +void loop() { + if (timeStatus() == timeSet && lastSecond != second()) { + lastSecond = second(); + driver.updateVariable("timestamp", String(now())); + Serial.println(ESP.getFreeHeap()); + } + + webServer.loop(); + driver.loop(); + + if (mqttClient != NULL) { + mqttClient->handleClient(); + } +}