From 9546d30f58d23e5a8caa73d55b120b5f3d89fc6f Mon Sep 17 00:00:00 2001 From: Lomanic Date: Sat, 24 Jul 2021 21:32:19 +0200 Subject: [PATCH] Implement initial somewhat naive version --- README.md | 26 +++ spaceapi-notifier.ino | 376 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 README.md create mode 100644 spaceapi-notifier.ino diff --git a/README.md b/README.md new file mode 100644 index 0000000..14d900e --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# SpaceAPI notifier + +Nodemcu v0.9/v3 + MAX7219 LED matrix display project. + +Displays configurable message on the display when the (also configurable) [SpaceAPI](https://spaceapi.io) endpoint state is open. + +Initial WiFi configuration is done by connecting to the SPACEAPI NOTIFIER access point and going through WifiManager captive portal. + +Custom SpaceAPI endpoint to query, displayed message configuration is then done on http://spaceapi-notifier.local/. + +All parameters (WiFi and customizations) can be reset by pressing GPIO0 (flash button on Nodemcu) continuously until the LED_BUILTIN blinks. +You then have to reconfigure the WiFi and custom settings. + +## Hardware + +* [V3 4M bytes (32Mbits) FLASH NodeMcu Lua WIFI Networking development board Based ESP8266 with firmware](https://www.aliexpress.com/item/32565317233.html) +* [MAX7219 Dot Matrix Module For Microcontroller 4 In One Display with 5P Line](https://www.aliexpress.com/item/32616345083.html) + +## Software + +Install the Arduino IDE + +Needed dependencies (tested with these versions): +* WiFiManager 0.16.0 +* MD_MAX72XX 3.3.0 +* MD_Parola 3.5.6 diff --git a/spaceapi-notifier.ino b/spaceapi-notifier.ino new file mode 100644 index 0000000..8eb3b8c --- /dev/null +++ b/spaceapi-notifier.ino @@ -0,0 +1,376 @@ +/* + This sketch displays a message on a LED matrix display when a given spaceapi endpoint tells a space is open + Based off a Nodemcu 0.9 (or v3) and MAX72XX-based LED matrix display + + Pinout: + D1 mini Matrix display + 5V VCC + GND GND + D5 DIN + D6 CS + D7 CLK + Yes, these pins are both sequential on both ends +*/ + +#include //this needs to be first, or it all crashes and burns... +#include //https://github.com/esp8266/Arduino +//needed for library +#include +#include +#include //https://github.com/tzapu/WiFiManager +#define ARDUINOJSON_DECODE_UNICODE 1 +#include //https://github.com/bblanchon/ArduinoJson +#include // https://tttapa.github.io/ESP8266/Chap08%20-%20mDNS.html + +#include + +//for LED status +#include +Ticker ledTicker; + +const byte LED_PIN = LED_BUILTIN; // 13 for Sonoff S20, 2 for NodeMCU/ESP12 internal LED +const byte BUTTON_PIN = 0; + +bool spaceIsOpen = false; + +#include // https://github.com/MajicDesigns/MD_Parola/blob/main/examples/Parola_HelloWorld/Parola_HelloWorld.ino +#include +#include + +// Define the number of devices we have in the chain and the hardware interface +// NOTE: These pin numbers will probably not work with your hardware and may +// need to be adapted +#define HARDWARE_TYPE MD_MAX72XX::FC16_HW +#define MAX_DEVICES 4 + +#define DATA_PIN D5 // or MOSI +#define CS_PIN D6 // or SS +#define CLK_PIN D7 // or SCK + +// SPI hardware interface +//MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); +// Arbitrary pins +//MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES); +MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES); + +void blinkLED() { + //toggle state + bool state = digitalRead(LED_PIN); // get the current state of LED_PIN pin + digitalWrite(LED_PIN, !state); // set pin to the opposite state +} + +//gets called when WiFiManager enters configuration mode (for LED status) +void configModeCallback (WiFiManager *myWiFiManager) { + Serial.println("Entered config mode"); + Serial.println(WiFi.softAPIP()); + //if you used auto generated SSID, print it + Serial.println(myWiFiManager->getConfigPortalSSID()); + //entered config mode, make led toggle faster + ledTicker.attach(0.5, blinkLED); +} + + +//define your default values here, if there are different values in config.json, they are overwritten. +String displayedMessage = "FUZ"; +String spaceAPIEndpoint = "https://spaceapi.fuz.re/"; + +//flag for saving data +bool shouldSaveConfig = false; + +//callback checking us of the need to save config +void saveConfigCallback () { + Serial.println(F("Should save config")); + shouldSaveConfig = true; +} + + +WiFiManager wifiManager; + +#include // for https://github.com/matt-williams/matrix-esp8266/blob/master/matrix-esp8266.ino + +ESP8266WebServer httpServer(80); // webserver on port 80 https://github.com/esp8266/Arduino/blob/14262af0d19a9a3b992d5aa310a684d47b6fb876/libraries/ESP8266WebServer/examples/AdvancedWebServer/AdvancedWebServer.ino +void handleRoot() { + for (int i = 0; i < httpServer.args(); i++) { + if (httpServer.argName(i) == "resetesp") { + httpServer.sendHeader("Location", httpServer.uri(), true); + httpServer.send(302, "text/plain", ""); + delay(500); + ESP.restart(); + return; + } + } + + if (httpServer.method() == HTTP_POST) { + for (int i = 0; i < httpServer.args(); i++) { + if (httpServer.argName(i) == "displayedMessage") { + displayedMessage = httpServer.arg(i); + continue; + } + if (httpServer.argName(i) == "spaceAPIEndpoint") { + spaceAPIEndpoint = httpServer.arg(i); + continue; + } + } + Serial.println(F("saving config")); + const size_t capacity = JSON_OBJECT_SIZE(3) + 440; // https://arduinojson.org/v6/assistant/ + DynamicJsonDocument jsonBuffer(capacity); + jsonBuffer["displayedMessage"] = displayedMessage; + jsonBuffer["spaceAPIEndpoint"] = spaceAPIEndpoint; + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println(F("failed to open config file for writing")); + } + + serializeJson(jsonBuffer, Serial); + Serial.println(); + serializeJson(jsonBuffer, configFile); + configFile.close(); + } + if (httpServer.args() > 0 || httpServer.method() == HTTP_POST) { // trim GET parameters and prevent resubmiting same form on refresh + httpServer.sendHeader("Location", httpServer.uri(), true); + return httpServer.send(302, "text/plain", ""); + } + + String html = + String("") + + "SpaceAPI notifier" + + "status: " + (spaceIsOpen ? "opened" : "closed") + "
" + + "Reboot ESP" + "

" + + "
" + + "
" + + "
" + + "
" + ""; + httpServer.send(200, "text/html", html); +} +void handleNotFound() { + httpServer.send(404, "text/plain", httpServer.uri() + " not found"); +} + +BearSSL::WiFiClientSecure secureClient; +HTTPClient http; +bool checkSpaceIsOpen() { + http.useHTTP10(true); // https://arduinojson.org/v6/how-to/use-arduinojson-with-httpclient/ + http.begin(secureClient, spaceAPIEndpoint); + int httpCode = http.GET(); + //Serial.println("checkSpaceIsOpen body: " + http.getString()); + Serial.println("checkSpaceIsOpen return code: " + String(httpCode)); + + ledTicker.detach(); + digitalWrite(LED_PIN, LOW); // ensure the LED is lit + if (httpCode != 200) { // something is wrong, bad network or misconfigured credentials + ledTicker.attach(0.1, blinkLED); + } + StaticJsonDocument<16> filter; // https://arduinojson.org/v6/assistant/ + filter["state"] = true; + + StaticJsonDocument<256> doc; + DeserializationError error = deserializeJson(doc, http.getStream(), DeserializationOption::Filter(filter)); + + if (error) { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(error.f_str()); + ledTicker.attach(0.1, blinkLED); + return false; + } else { + Serial.println(F("deserializeJson() successful:")); + } + + bool state_open = doc["state"]["open"]; // false + + serializeJsonPretty(doc, Serial); + Serial.println(); + http.end(); + + bool m0 = doc["state"]["open"]; + return m0; +} + +void setup() { + Serial.begin(115200); + Serial.println(); + + //set led pin as output + pinMode(LED_PIN, OUTPUT); + //set button pin as input + pinMode(BUTTON_PIN, INPUT); + + // start ledTicker with 1 because we start in AP mode and try to connect + ledTicker.attach(1.1, blinkLED); + + Serial.println(F("mounting FS...")); + + if (SPIFFS.begin()) { + Serial.println(F("mounted file system")); + if (SPIFFS.exists("/config.json")) { + //file exists, reading and loading + Serial.println(F("reading config file")); + File configFile = SPIFFS.open("/config.json", "r"); + if (configFile) { + Serial.println(F("opened config file")); + size_t size = configFile.size(); + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + const size_t capacity = JSON_OBJECT_SIZE(3) + 440; // https://arduinojson.org/v6/assistant/ + DynamicJsonDocument jsonBuffer(capacity); + auto error = deserializeJson(jsonBuffer, buf.get()); + if (error) { + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + return; + } else { + Serial.println(F("deserializeJson() successful:")); + + serializeJsonPretty(jsonBuffer, Serial); + Serial.println(); + + String m0 = jsonBuffer["displayedMessage"]; + displayedMessage = m0; + String m1 = jsonBuffer["spaceAPIEndpoint"]; + spaceAPIEndpoint = m1; + } + configFile.close(); + } + } + } else { + Serial.println(F("failed to mount FS")); + } + //end read + + + + // The extra parameters to be configured (can be either global or just in the setup) + // After connecting, parameter.getValue() will get you the configured value + // id/name placeholder/prompt default length + WiFiManagerParameter customDisplayedMessage("Displayed message", "Displayed message", displayedMessage.c_str(), 50); + WiFiManagerParameter customSpaceAPIEndpoint("SpaceAPI endpoint", "SpaceAPI endpoint", spaceAPIEndpoint.c_str(), 200); + + //WiFiManager + //Local intialization. Once its business is done, there is no need to keep it around + //WiFiManager wifiManager; + //reset settings - for testing + //wifiManager.resetSettings(); + + //set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode (for status LED) + wifiManager.setAPCallback(configModeCallback); + + //set config save check callback + wifiManager.setSaveConfigCallback(saveConfigCallback); + + //set static ip + // wifiManager.setSTAStaticIPConfig(IPAddress(10,0,1,99), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); + + //add all your parameters here + wifiManager.addParameter(&customDisplayedMessage); + wifiManager.addParameter(&customSpaceAPIEndpoint); + + //reset settings - for testing + //wifiManager.resetSettings(); + + //sets timeout until configuration portal gets turned off + //useful to make it all retry or go to sleep + //in seconds + //wifiManager.setTimeout(120); + + //fetches ssid and pass and tries to connect + //if it does not connect it starts an access point with the specified name + //here "SPACEAPI NOTIFIER" + //and goes into a blocking loop awaiting configuration + if (!wifiManager.autoConnect("SPACEAPI NOTIFIER")) { + Serial.println(F("failed to connect and hit timeout")); + delay(3000); + //reset and try again, or maybe put it to deep sleep + ESP.restart(); + delay(5000); + } + + //if you get here you have connected to the WiFi + Serial.println(F("connected...yeey :)")); + ledTicker.detach(); + //keep LED on + digitalWrite(LED_PIN, LOW); + + //read updated parameters + displayedMessage = customDisplayedMessage.getValue(); + spaceAPIEndpoint = customSpaceAPIEndpoint.getValue(); + + //save the custom parameters to FS + if (shouldSaveConfig) { + Serial.println(F("saving config")); + const size_t capacity = JSON_OBJECT_SIZE(3) + 440; // https://arduinojson.org/v6/assistant/ + DynamicJsonDocument jsonBuffer(capacity); + jsonBuffer["displayedMessage"] = displayedMessage; + jsonBuffer["spaceAPIEndpoint"] = spaceAPIEndpoint; + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println(F("failed to open config file for writing")); + } + + serializeJson(jsonBuffer, Serial); + Serial.println(); + serializeJson(jsonBuffer, configFile); + configFile.close(); + //end save + } + + Serial.println(F("local ip:")); + Serial.println(WiFi.localIP()); + Serial.println(WiFi.SSID()); + + httpServer.on("/", handleRoot); + httpServer.onNotFound(handleNotFound); + httpServer.begin(); + Serial.println("HTTP server started"); + if (!MDNS.begin("spaceapi-notifier")) { // https://github.com/esp8266/Arduino/blob/14262af0d19a9a3b992d5aa310a684d47b6fb876/libraries/ESP8266mDNS/examples/mDNS_Web_Server/mDNS_Web_Server.ino + Serial.println("Error setting up MDNS responder!"); + } else { + Serial.println("mDNS responder started"); + } + MDNS.addService("http", "tcp", 80); + + secureClient.setInsecure(); + P.begin(); + P.setIntensity(7); // yeah don't blind people +} + +bool buttonState = HIGH; +bool previousButtonState = HIGH; +long previousMillis = 0; +const long checkInterval = 5000; +unsigned long pressedTime = millis(); +void loop() { + MDNS.update(); + + httpServer.handleClient(); + + buttonState = digitalRead(BUTTON_PIN); + if (buttonState == LOW && previousButtonState == HIGH) { // long press handling, reset settings https://forum.arduino.cc/index.php?topic=276789.msg1947963#msg1947963 + Serial.println("Button pressed (longpress handling)"); + pressedTime = millis(); + } + if (buttonState == LOW && previousButtonState == LOW && (millis() - pressedTime) > 5000) { + Serial.println("Button STILL pressed (longpress handling)"); + wifiManager.resetSettings(); + SPIFFS.format(); + delay(500); + ESP.restart(); + } + unsigned long currentMillis = millis(); + if (currentMillis - previousMillis > checkInterval) { + previousMillis = currentMillis; + spaceIsOpen = checkSpaceIsOpen(); + } + if (P.displayAnimate()) { + if (spaceIsOpen) { + P.displayText(displayedMessage.c_str(), PA_CENTER, P.getSpeed(), P.getPause(), PA_NO_EFFECT, PA_NO_EFFECT); + } else { + P.displayText(".", PA_CENTER, P.getSpeed(), P.getPause(), PA_NO_EFFECT, PA_NO_EFFECT); + } + } + + previousButtonState = buttonState; +}