diff --git a/CO2-100/BlynkEdgent.h b/CO2-100/BlynkEdgent.h new file mode 100644 index 0000000..6f0464f --- /dev/null +++ b/CO2-100/BlynkEdgent.h @@ -0,0 +1,117 @@ + +extern "C" { + void app_loop(); + void eraseMcuConfig(); + void restartMCU(); +} + +#include "Settings.h" +#include + +#ifndef BLYNK_NEW_LIBRARY +#error "Old version of Blynk library is in use. Please replace it with the new one." +#endif + +#if !defined(BLYNK_TEMPLATE_ID) || !defined(BLYNK_DEVICE_NAME) +#error "Please specify your BLYNK_TEMPLATE_ID and BLYNK_DEVICE_NAME" +#endif + +#include "BlynkState.h" +#include "ConfigStore.h" +#include "ResetButton.h" +#include "ConfigMode.h" +#include "Indicator.h" +#include "OTA.h" +#include "Console.h" + +inline +void BlynkState::set(State m) { + if (state != m && m < MODE_MAX_VALUE) { + DEBUG_PRINT(String(StateStr[state]) + " => " + StateStr[m]); + state = m; + + // You can put your state handling here, + // i.e. implement custom indication + } +} + +void printDeviceBanner() +{ + Blynk.printBanner(); + DEBUG_PRINT("--------------------------"); + DEBUG_PRINT(String("Product: ") + BLYNK_DEVICE_NAME); + DEBUG_PRINT(String("Firmware: ") + BLYNK_FIRMWARE_VERSION " (build " __DATE__ " " __TIME__ ")"); + if (configStore.getFlag(CONFIG_FLAG_VALID)) { + DEBUG_PRINT(String("Token: ...") + (configStore.cloudToken+28)); + } + DEBUG_PRINT(String("Device: ") + BLYNK_INFO_DEVICE + " @ " + ESP.getCpuFreqMHz() + "MHz"); + DEBUG_PRINT(String("MAC: ") + WiFi.macAddress()); + DEBUG_PRINT(String("Flash: ") + ESP.getFlashChipSize() / 1024 + "K"); + DEBUG_PRINT(String("ESP sdk: ") + ESP.getSdkVersion()); + DEBUG_PRINT(String("Chip rev: ") + ESP.getChipRevision()); + DEBUG_PRINT(String("Free mem: ") + ESP.getFreeHeap()); + DEBUG_PRINT("--------------------------"); +} + +void runBlynkWithChecks() { + Blynk.run(); + if (BlynkState::get() == MODE_RUNNING) { + if (!Blynk.connected()) { + if (WiFi.status() == WL_CONNECTED) { + BlynkState::set(MODE_CONNECTING_CLOUD); + } else { + BlynkState::set(MODE_CONNECTING_NET); + } + } + } +} + +class Edgent { + +public: + void begin() + { + WiFi.persistent(false); + WiFi.enableSTA(true); // Needed to get MAC + + indicator_init(); + button_init(); + config_init(); + console_init(); + + printDeviceBanner(); + + if (configStore.getFlag(CONFIG_FLAG_VALID)) { + BlynkState::set(MODE_CONNECTING_NET); + } else if (config_load_blnkopt()) { + DEBUG_PRINT("Firmware is preprovisioned"); + BlynkState::set(MODE_CONNECTING_NET); + } else { + BlynkState::set(MODE_WAIT_CONFIG); + } + } + + void run() { + app_loop(); + switch (BlynkState::get()) { + case MODE_WAIT_CONFIG: + case MODE_CONFIGURING: enterConfigMode(); break; + case MODE_CONNECTING_NET: enterConnectNet(); break; + case MODE_CONNECTING_CLOUD: enterConnectCloud(); break; + case MODE_RUNNING: runBlynkWithChecks(); break; + case MODE_OTA_UPGRADE: enterOTA(); break; + case MODE_SWITCH_TO_STA: enterSwitchToSTA(); break; + case MODE_RESET_CONFIG: enterResetConfig(); break; + default: enterError(); break; + } + } + +}; + +Edgent BlynkEdgent; +BlynkTimer edgentTimer; + +void app_loop() { + edgentTimer.run(); + edgentConsole.run(); +} diff --git a/CO2-100/BlynkState.h b/CO2-100/BlynkState.h new file mode 100644 index 0000000..f6ad0ec --- /dev/null +++ b/CO2-100/BlynkState.h @@ -0,0 +1,39 @@ + +enum State { + MODE_WAIT_CONFIG, + MODE_CONFIGURING, + MODE_CONNECTING_NET, + MODE_CONNECTING_CLOUD, + MODE_RUNNING, + MODE_OTA_UPGRADE, + MODE_SWITCH_TO_STA, + MODE_RESET_CONFIG, + MODE_ERROR, + + MODE_MAX_VALUE +}; + +#if defined(APP_DEBUG) +const char* StateStr[MODE_MAX_VALUE+1] = { + "WAIT_CONFIG", + "CONFIGURING", + "CONNECTING_NET", + "CONNECTING_CLOUD", + "RUNNING", + "OTA_UPGRADE", + "SWITCH_TO_STA", + "RESET_CONFIG", + "ERROR", + + "INIT" +}; +#endif + +namespace BlynkState +{ + volatile State state = MODE_MAX_VALUE; + + State get() { return state; } + bool is (State m) { return (state == m); } + void set(State m); +}; diff --git a/CO2-100/CO2-100.ino b/CO2-100/CO2-100.ino new file mode 100644 index 0000000..e93d898 --- /dev/null +++ b/CO2-100/CO2-100.ino @@ -0,0 +1,77 @@ +#define BLYNK_TEMPLATE_ID "TMPLsdSzyUnw" +#define BLYNK_DEVICE_NAME "CO2 100" +#define BLYNK_AUTH_TOKEN "28OeDXtCtS5dnMICcx2LuT3xO0Kt4myz" + +#define BLYNK_FIRMWARE_VERSION "0.1.0" +#define BLYNK_PRINT Serial +//#define BLYNK_DEBUG + +#define APP_DEBUG + +#include +#include +#include "BlynkEdgent.h" +// Pin RX Arduino conectado al pin TX del MHZ19 +#define RX_PIN 16 +// Pin TX Arduino conectado al pin RX del MHZ19 +#define TX_PIN 17 + +// Objeto para sensor MHZ19 +MHZ19 myMHZ19; +// Serial requerido por el MHZ19 +SoftwareSerial mySerial(RX_PIN, TX_PIN); + + +// Contador para temporizar las mediciones +unsigned long timer = 0; + +void setup() { + Serial.begin(9600); + mySerial.begin(9600); + myMHZ19.begin(mySerial); + // Turn auto calibration ON (OFF autoCalibration(false)) + myMHZ19.autoCalibration(); + BlynkEdgent.begin(); +} + +void loop() { + BlynkEdgent.run(); + Blynk.virtualWrite(V24, WiFi.SSID()); + Blynk.virtualWrite(V22, generateChipID()); + medicionCO2(); +} + +void medicionCO2(){ + if (millis() - timer >= 10000) { + + // Obtener la medición de CO2 actual como ppm + int nivelCO2 = myMHZ19.getCO2(); + + if(nivelCO2<900){ + Blynk.setProperty(V6, "color", "#7cff00"); + }else{ + Blynk.setProperty(V6, "color", "#ff1e1e"); + } + + // Mostrar el nivel de CO2 en el monitor serie + Serial.print("CO2 (ppm): "); + Serial.println(nivelCO2); + Blynk.virtualWrite(V6, nivelCO2); + + timer = millis(); + + } + +} + +String generateChipID() +{ + String aux; + uint64_t chipid = ESP.getEfuseMac(); + char chip[14]; + chip[0] = NULL; + for (int i = 0; i < 6; i++) + sprintf(chip, "%s%02X", chip, (uint8_t)(chipid >> 8 * i)); + aux = String(chip); + return aux; +} diff --git a/CO2-100/ConfigMode.h b/CO2-100/ConfigMode.h new file mode 100644 index 0000000..44ba93f --- /dev/null +++ b/CO2-100/ConfigMode.h @@ -0,0 +1,488 @@ + +#include +#include +#include +#include + +#include +#include + +WebServer server(80); +DNSServer dnsServer; +const byte DNS_PORT = 53; + +#ifdef BLYNK_USE_SPIFFS + #include "SPIFFS.h" +#else + const char* config_form = R"html( + + + + WiFi setup + + + +
+
+ + + + + + +

+ +
+
+ + +)html"; +#endif + +static const char serverUpdateForm[] PROGMEM = + R"( +
+ + +
+ )"; + +void restartMCU() { + ESP.restart(); + while(1) {}; +} + +void eraseMcuConfig() { + // Erase ESP32 NVS + int err; + //err=nvs_flash_init(); + //BLYNK_LOG2("nvs_flash_init: ", err ? String(err) : "Success"); + err=nvs_flash_erase(); + BLYNK_LOG2("nvs_flash_erase: ", err ? String(err) : "Success"); +} + +void getWiFiName(char* buff, size_t len, bool withPrefix = true) { + const uint64_t chipId = ESP.getEfuseMac(); + uint32_t unique = 0; + for (int i=0; i<4; i++) { + unique = BlynkCRC32(&chipId, sizeof(chipId), unique); + } + unique &= 0xFFFFF; + + if (withPrefix) { + snprintf(buff, len, "Blynk %s-%05X", BLYNK_DEVICE_NAME, unique); + } else { + snprintf(buff, len, "%s-%05X", BLYNK_DEVICE_NAME, unique); + } +} + +void enterConfigMode() +{ + char ssidBuff[64]; + getWiFiName(ssidBuff, sizeof(ssidBuff)); + + WiFi.mode(WIFI_OFF); + delay(100); + WiFi.mode(WIFI_AP); + delay(2000); + WiFi.softAPConfig(WIFI_AP_IP, WIFI_AP_IP, WIFI_AP_Subnet); + WiFi.softAP(ssidBuff); + delay(500); + + IPAddress myIP = WiFi.softAPIP(); + DEBUG_PRINT(String("AP SSID: ") + ssidBuff); + DEBUG_PRINT(String("AP IP: ") + myIP[0] + "." + myIP[1] + "." + myIP[2] + "." + myIP[3]); + + // Set up DNS Server + dnsServer.setTTL(300); // Time-to-live 300s + dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure); // Return code for non-accessible domains +#ifdef WIFI_CAPTIVE_PORTAL_ENABLE + dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); // Point all to our IP + server.onNotFound(handleRoot); +#else + dnsServer.start(DNS_PORT, CONFIG_AP_URL, WiFi.softAPIP()); + DEBUG_PRINT(String("AP URL: ") + CONFIG_AP_URL); +#endif + + server.on("/update", HTTP_GET, []() { + server.sendHeader("Connection", "close"); + server.send(200, "text/html", serverUpdateForm); + }); + server.on("/update", HTTP_POST, []() { + server.sendHeader("Connection", "close"); + if (!Update.hasError()) { + server.send(200, "text/plain", "OK"); + } else { + server.send(500, "text/plain", "FAIL"); + } + delay(1000); + restartMCU(); + }, []() { + HTTPUpload& upload = server.upload(); + if (upload.status == UPLOAD_FILE_START) { + DEBUG_PRINT(String("Update: ") + upload.filename); + //WiFiUDP::stop(); + + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size + Update.printError(BLYNK_PRINT); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + /* flashing firmware to ESP*/ + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(BLYNK_PRINT); + } + BLYNK_PRINT.print("."); + } else if (upload.status == UPLOAD_FILE_END) { + BLYNK_PRINT.println(); + DEBUG_PRINT("Finishing..."); + if (Update.end(true)) { //true to set the size to the current progress + DEBUG_PRINT("Update Success. Rebooting"); + } else { + Update.printError(BLYNK_PRINT); + } + } + }); + + server.on("/config", []() { + DEBUG_PRINT("Applying configuration..."); + String ssid = server.arg("ssid"); + String ssidManual = server.arg("ssidManual"); + String pass = server.arg("pass"); + if (ssidManual != "") { + ssid = ssidManual; + } + String token = server.arg("blynk"); + String host = server.arg("host"); + String port = server.arg("port_ssl"); + + String ip = server.arg("ip"); + String mask = server.arg("mask"); + String gw = server.arg("gw"); + String dns = server.arg("dns"); + String dns2 = server.arg("dns2"); + + bool save = server.arg("save").toInt(); + + String content; + + DEBUG_PRINT(String("WiFi SSID: ") + ssid + " Pass: " + pass); + DEBUG_PRINT(String("Blynk cloud: ") + token + " @ " + host + ":" + port); + + if (token.length() == 32 && ssid.length() > 0) { + configStore.setFlag(CONFIG_FLAG_VALID, false); + CopyString(ssid, configStore.wifiSSID); + CopyString(pass, configStore.wifiPass); + CopyString(token, configStore.cloudToken); + if (host.length()) { + CopyString(host, configStore.cloudHost); + } + if (port.length()) { + configStore.cloudPort = port.toInt(); + } + + IPAddress addr; + + if (ip.length() && addr.fromString(ip)) { + configStore.staticIP = addr; + configStore.setFlag(CONFIG_FLAG_STATIC_IP, true); + } else { + configStore.setFlag(CONFIG_FLAG_STATIC_IP, false); + } + if (mask.length() && addr.fromString(mask)) { + configStore.staticMask = addr; + } + if (gw.length() && addr.fromString(gw)) { + configStore.staticGW = addr; + } + if (dns.length() && addr.fromString(dns)) { + configStore.staticDNS = addr; + } + if (dns2.length() && addr.fromString(dns2)) { + configStore.staticDNS2 = addr; + } + + if (save) { + configStore.setFlag(CONFIG_FLAG_VALID, true); + config_save(); + + content = R"json({"status":"ok","msg":"Configuration saved"})json"; + } else { + content = R"json({"status":"ok","msg":"Trying to connect..."})json"; + } + server.send(200, "application/json", content); + + BlynkState::set(MODE_SWITCH_TO_STA); + } else { + DEBUG_PRINT("Configuration invalid"); + content = R"json({"status":"error","msg":"Configuration invalid"})json"; + server.send(500, "application/json", content); + } + }); + server.on("/board_info.json", []() { + DEBUG_PRINT("Sending board info..."); + const char* tmpl = BLYNK_TEMPLATE_ID; + char ssidBuff[64]; + getWiFiName(ssidBuff, sizeof(ssidBuff)); + char buff[512]; + snprintf(buff, sizeof(buff), + R"json({"board":"%s","tmpl_id":"%s","fw_type":"%s","fw_ver":"%s","ssid":"%s","bssid":"%s","last_error":%d,"wifi_scan":true,"static_ip":true})json", + BLYNK_DEVICE_NAME, + tmpl ? tmpl : "Unknown", + BLYNK_FIRMWARE_TYPE, + BLYNK_FIRMWARE_VERSION, + ssidBuff, + WiFi.softAPmacAddress().c_str(), + configStore.last_error + ); + server.send(200, "application/json", buff); + }); + server.on("/wifi_scan.json", []() { + DEBUG_PRINT("Scanning networks..."); + int wifi_nets = WiFi.scanNetworks(true, true); + const uint32_t t = millis(); + while (wifi_nets < 0 && + millis() - t < 20000) + { + delay(20); + wifi_nets = WiFi.scanComplete(); + } + DEBUG_PRINT(String("Found networks: ") + wifi_nets); + + if (wifi_nets > 0) { + // Sort networks + int indices[wifi_nets]; + for (int i = 0; i < wifi_nets; i++) { + indices[i] = i; + } + for (int i = 0; i < wifi_nets; i++) { + for (int j = i + 1; j < wifi_nets; j++) { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { + std::swap(indices[i], indices[j]); + } + } + } + + wifi_nets = BlynkMin(15, wifi_nets); // Show top 15 networks + + // TODO: skip empty names + String result = "[\n"; + + char buff[256]; + for (int i = 0; i < wifi_nets; i++){ + int id = indices[i]; + + const char* sec; + switch (WiFi.encryptionType(id)) { + case WIFI_AUTH_WEP: sec = "WEP"; break; + case WIFI_AUTH_WPA_PSK: sec = "WPA/PSK"; break; + case WIFI_AUTH_WPA2_PSK: sec = "WPA2/PSK"; break; + case WIFI_AUTH_WPA_WPA2_PSK: sec = "WPA/WPA2/PSK"; break; + case WIFI_AUTH_OPEN: sec = "OPEN"; break; + default: sec = "unknown"; break; + } + + snprintf(buff, sizeof(buff), + R"json( {"ssid":"%s","bssid":"%s","rssi":%i,"sec":"%s","ch":%i})json", + WiFi.SSID(id).c_str(), + WiFi.BSSIDstr(id).c_str(), + WiFi.RSSI(id), + sec, + WiFi.channel(id) + ); + + result += buff; + if (i != wifi_nets-1) result += ",\n"; + } + server.send(200, "application/json", result + "\n]"); + } else { + server.send(200, "application/json", "[]"); + } + }); + server.on("/reset", []() { + BlynkState::set(MODE_RESET_CONFIG); + server.send(200, "application/json", R"json({"status":"ok","msg":"Configuration reset"})json"); + }); + server.on("/reboot", []() { + restartMCU(); + }); + +#ifdef BLYNK_USE_SPIFFS + if (SPIFFS.begin()) { + server.serveStatic("/img/favicon.png", SPIFFS, "/img/favicon.png"); + server.serveStatic("/img/logo.png", SPIFFS, "/img/logo.png"); + server.serveStatic("/", SPIFFS, "/index.html"); + } else { + DEBUG_PRINT("Webpage: No SPIFFS"); + } +#endif + + server.begin(); + + while (BlynkState::is(MODE_WAIT_CONFIG) || BlynkState::is(MODE_CONFIGURING)) { + delay(10); + dnsServer.processNextRequest(); + server.handleClient(); + app_loop(); + if (BlynkState::is(MODE_WAIT_CONFIG) && WiFi.softAPgetStationNum() > 0) { + BlynkState::set(MODE_CONFIGURING); + } else if (BlynkState::is(MODE_CONFIGURING) && WiFi.softAPgetStationNum() == 0) { + BlynkState::set(MODE_WAIT_CONFIG); + } + } + + server.stop(); + +#ifdef BLYNK_USE_SPIFFS + SPIFFS.end(); +#endif +} + +void enterConnectNet() { + BlynkState::set(MODE_CONNECTING_NET); + DEBUG_PRINT(String("Connecting to WiFi: ") + configStore.wifiSSID); + + char ssidBuff[64]; + getWiFiName(ssidBuff, sizeof(ssidBuff)); + String hostname(ssidBuff); + hostname.replace(" ", "-"); + WiFi.setHostname(hostname.c_str()); + + if (configStore.getFlag(CONFIG_FLAG_STATIC_IP)) { + if (!WiFi.config(configStore.staticIP, + configStore.staticGW, + configStore.staticMask, + configStore.staticDNS, + configStore.staticDNS2) + ) { + DEBUG_PRINT("Failed to configure Static IP"); + config_set_last_error(BLYNK_PROV_ERR_CONFIG); + BlynkState::set(MODE_ERROR); + return; + } + } + + WiFi.begin(configStore.wifiSSID, configStore.wifiPass); + + unsigned long timeoutMs = millis() + WIFI_NET_CONNECT_TIMEOUT; + while ((timeoutMs > millis()) && (WiFi.status() != WL_CONNECTED)) + { + delay(10); + app_loop(); + + if (!BlynkState::is(MODE_CONNECTING_NET)) { + WiFi.disconnect(); + return; + } + } + + if (WiFi.status() == WL_CONNECTED) { + IPAddress localip = WiFi.localIP(); + if (configStore.getFlag(CONFIG_FLAG_STATIC_IP)) { + BLYNK_LOG_IP("Using Static IP: ", localip); + } else { + BLYNK_LOG_IP("Using Dynamic IP: ", localip); + } + + BlynkState::set(MODE_CONNECTING_CLOUD); + } else { + config_set_last_error(BLYNK_PROV_ERR_NETWORK); + BlynkState::set(MODE_ERROR); + } +} + +void enterConnectCloud() { + BlynkState::set(MODE_CONNECTING_CLOUD); + + Blynk.config(configStore.cloudToken, configStore.cloudHost, configStore.cloudPort); + Blynk.connect(0); + + unsigned long timeoutMs = millis() + WIFI_CLOUD_CONNECT_TIMEOUT; + while ((timeoutMs > millis()) && + (!Blynk.isTokenInvalid()) && + (Blynk.connected() == false)) + { + delay(10); + Blynk.run(); + app_loop(); + if (!BlynkState::is(MODE_CONNECTING_CLOUD)) { + Blynk.disconnect(); + return; + } + } + + if (millis() > timeoutMs) { + DEBUG_PRINT("Timeout"); + } + + if (Blynk.isTokenInvalid()) { + config_set_last_error(BLYNK_PROV_ERR_TOKEN); + BlynkState::set(MODE_WAIT_CONFIG); + } else if (Blynk.connected()) { + BlynkState::set(MODE_RUNNING); + + if (!configStore.getFlag(CONFIG_FLAG_VALID)) { + configStore.last_error = BLYNK_PROV_ERR_NONE; + configStore.setFlag(CONFIG_FLAG_VALID, true); + config_save(); + } + } else { + config_set_last_error(BLYNK_PROV_ERR_CLOUD); + BlynkState::set(MODE_ERROR); + } +} + +void enterSwitchToSTA() { + BlynkState::set(MODE_SWITCH_TO_STA); + + DEBUG_PRINT("Switching to STA..."); + + delay(1000); + WiFi.mode(WIFI_OFF); + delay(100); + WiFi.mode(WIFI_STA); + + BlynkState::set(MODE_CONNECTING_NET); +} + +void enterError() { + BlynkState::set(MODE_ERROR); + + unsigned long timeoutMs = millis() + 10000; + while (timeoutMs > millis() || g_buttonPressed) + { + delay(10); + app_loop(); + if (!BlynkState::is(MODE_ERROR)) { + return; + } + } + DEBUG_PRINT("Restarting after error."); + delay(10); + + restartMCU(); +} diff --git a/CO2-100/ConfigStore.h b/CO2-100/ConfigStore.h new file mode 100644 index 0000000..7745bfd --- /dev/null +++ b/CO2-100/ConfigStore.h @@ -0,0 +1,146 @@ + +#define CONFIG_FLAG_VALID 0x01 +#define CONFIG_FLAG_STATIC_IP 0x02 + +#define BLYNK_PROV_ERR_NONE 0 // All good +#define BLYNK_PROV_ERR_CONFIG 700 // Invalid config from app (malformed token,etc) +#define BLYNK_PROV_ERR_NETWORK 701 // Could not connect to the router +#define BLYNK_PROV_ERR_CLOUD 702 // Could not connect to the cloud +#define BLYNK_PROV_ERR_TOKEN 703 // Invalid token error (after connection) +#define BLYNK_PROV_ERR_INTERNAL 704 // Other issues (i.e. hardware failure) + +struct ConfigStore { + uint32_t magic; + char version[15]; + uint8_t flags; + + char wifiSSID[34]; + char wifiPass[64]; + + char cloudToken[34]; + char cloudHost[34]; + uint16_t cloudPort; + + uint32_t staticIP; + uint32_t staticMask; + uint32_t staticGW; + uint32_t staticDNS; + uint32_t staticDNS2; + + int last_error; + + void setFlag(uint8_t mask, bool value) { + if (value) { + flags |= mask; + } else { + flags &= ~mask; + } + } + + bool getFlag(uint8_t mask) { + return (flags & mask) == mask; + } +} __attribute__((packed)); + +ConfigStore configStore; + +const ConfigStore configDefault = { + 0x626C6E6B, + BLYNK_FIRMWARE_VERSION, + 0x00, + + "", + "", + + "invalid token", + CONFIG_DEFAULT_SERVER, + CONFIG_DEFAULT_PORT, + 0, + BLYNK_PROV_ERR_NONE +}; + +template +void CopyString(const String& s, T(&arr)[size]) { + s.toCharArray(arr, size); +} + +static bool config_load_blnkopt() +{ + static const char blnkopt[] = "blnkopt\0" + BLYNK_PARAM_KV("ssid" , BLYNK_PARAM_PLACEHOLDER_64 + BLYNK_PARAM_PLACEHOLDER_64 + BLYNK_PARAM_PLACEHOLDER_64 + BLYNK_PARAM_PLACEHOLDER_64) + BLYNK_PARAM_KV("host" , CONFIG_DEFAULT_SERVER) + BLYNK_PARAM_KV("port" , BLYNK_TOSTRING(CONFIG_DEFAULT_PORT)) + "\0"; + + BlynkParam prov(blnkopt+8, sizeof(blnkopt)-8-2); + BlynkParam::iterator ssid = prov["ssid"]; + BlynkParam::iterator pass = prov["pass"]; + BlynkParam::iterator auth = prov["auth"]; + BlynkParam::iterator host = prov["host"]; + BlynkParam::iterator port = prov["port"]; + + if (!(ssid.isValid() && auth.isValid())) { + return false; + } + + // reset to defaut before loading values from blnkopt + configStore = configDefault; + + if (ssid.isValid()) { CopyString(ssid.asStr(), configStore.wifiSSID); } + if (pass.isValid()) { CopyString(pass.asStr(), configStore.wifiPass); } + if (auth.isValid()) { CopyString(auth.asStr(), configStore.cloudToken); } + if (host.isValid()) { CopyString(host.asStr(), configStore.cloudHost); } + if (port.isValid()) { configStore.cloudPort = port.asInt(); } + + return true; +} + +#include +Preferences preferences; + +void config_load() +{ + memset(&configStore, 0, sizeof(configStore)); + preferences.getBytes("config", &configStore, sizeof(configStore)); + if (configStore.magic != configDefault.magic) { + DEBUG_PRINT("Using default config."); + configStore = configDefault; + return; + } +} + +bool config_save() +{ + preferences.putBytes("config", &configStore, sizeof(configStore)); + DEBUG_PRINT("Configuration stored to flash"); + return true; +} + +bool config_init() +{ + preferences.begin("blynk", false); + config_load(); + return true; +} + +void enterResetConfig() +{ + DEBUG_PRINT("Resetting configuration!"); + configStore = configDefault; + config_save(); + eraseMcuConfig(); + BlynkState::set(MODE_WAIT_CONFIG); +} + +void config_set_last_error(int error) { + // Only set error if not provisioned + if (!configStore.getFlag(CONFIG_FLAG_VALID)) { + configStore = configDefault; + configStore.last_error = error; + BLYNK_LOG2("Last error code: ", error); + config_save(); + } +} diff --git a/CO2-100/Console.h b/CO2-100/Console.h new file mode 100644 index 0000000..05616d3 --- /dev/null +++ b/CO2-100/Console.h @@ -0,0 +1,53 @@ + +#include + +BlynkConsole edgentConsole; + +void console_init() +{ + edgentConsole.init(BLYNK_PRINT); + + edgentConsole.print("\n>"); + + edgentConsole.addCommand("reboot", []() { + edgentConsole.print(R"json({"status":"OK","msg":"resetting device"})json" "\n"); + delay(100); + restartMCU(); + }); + + edgentConsole.addCommand("config", []() { + edgentConsole.print(R"json({"status":"OK","msg":"entering configuration mode"})json" "\n"); + BlynkState::set(MODE_WAIT_CONFIG); + }); + + edgentConsole.addCommand("devinfo", []() { + edgentConsole.printf( + R"json({"board":"%s","tmpl_id":"%s","fw_type":"%s","fw_ver":"%s"})json" "\n", + BLYNK_DEVICE_NAME, + BLYNK_TEMPLATE_ID, + BLYNK_FIRMWARE_TYPE, + BLYNK_FIRMWARE_VERSION + ); + }); + + edgentConsole.addCommand("netinfo", []() { + char ssidBuff[64]; + getWiFiName(ssidBuff, sizeof(ssidBuff)); + + byte mac[6] = { 0, }; + WiFi.macAddress(mac); + + edgentConsole.printf( + R"json({"ssid":"%s","bssid":"%02x:%02x:%02x:%02x:%02x:%02x","rssi":%d})json" "\n", + ssidBuff, + mac[5], mac[4], mac[3], mac[2], mac[1], mac[0], + WiFi.RSSI() + ); + }); + +} + +BLYNK_WRITE(InternalPinDBG) { + String cmd = String(param.asStr()) + "\n"; + edgentConsole.runCommand((char*)cmd.c_str()); +} diff --git a/CO2-100/Indicator.h b/CO2-100/Indicator.h new file mode 100644 index 0000000..5b75377 --- /dev/null +++ b/CO2-100/Indicator.h @@ -0,0 +1,309 @@ + +#if defined(BOARD_LED_PIN_WS2812) + #include // Library: https://github.com/adafruit/Adafruit_NeoPixel + + Adafruit_NeoPixel rgb = Adafruit_NeoPixel(1, BOARD_LED_PIN_WS2812, NEO_GRB + NEO_KHZ800); +#endif + +void indicator_run(); + +#if !defined(BOARD_LED_BRIGHTNESS) +#define BOARD_LED_BRIGHTNESS 255 +#endif + +#if defined(BOARD_LED_PIN_WS2812) || defined(BOARD_LED_PIN_R) +#define BOARD_LED_IS_RGB +#endif + +#define DIMM(x) ((uint32_t)(x)*(BOARD_LED_BRIGHTNESS)/255) +#define RGB(r,g,b) (DIMM(r) << 16 | DIMM(g) << 8 | DIMM(b) << 0) +#define TO_PWM(x) ((uint32_t)(x)*(BOARD_PWM_MAX)/255) + +class Indicator { +public: + + enum Colors { + COLOR_BLACK = RGB(0x00, 0x00, 0x00), + COLOR_WHITE = RGB(0xFF, 0xFF, 0xE7), + COLOR_BLUE = RGB(0x0D, 0x36, 0xFF), + COLOR_BLYNK = RGB(0x2E, 0xFF, 0xB9), + COLOR_RED = RGB(0xFF, 0x10, 0x08), + COLOR_MAGENTA = RGB(0xA7, 0x00, 0xFF), + }; + + Indicator() { + } + + void init() { + m_Counter = 0; + initLED(); + } + + uint32_t run() { + State currState = BlynkState::get(); + + // Reset counter if indicator state changes + if (m_PrevState != currState) { + m_PrevState = currState; + m_Counter = 0; + } + + if (g_buttonPressed) { + if (millis() - g_buttonPressTime > BUTTON_HOLD_TIME_ACTION) { return beatLED(COLOR_WHITE, (int[]){ 100, 100 }); } + if (millis() - g_buttonPressTime > BUTTON_HOLD_TIME_INDICATION) { return waveLED(COLOR_WHITE, 1000); } + } + switch (currState) { + case MODE_RESET_CONFIG: + case MODE_WAIT_CONFIG: return beatLED(COLOR_BLUE, (int[]){ 50, 500 }); + case MODE_CONFIGURING: return beatLED(COLOR_BLUE, (int[]){ 200, 200 }); + case MODE_CONNECTING_NET: return beatLED(COLOR_BLYNK, (int[]){ 50, 500 }); + case MODE_CONNECTING_CLOUD: return beatLED(COLOR_BLYNK, (int[]){ 100, 100 }); + case MODE_RUNNING: return waveLED(COLOR_BLYNK, 5000); + case MODE_OTA_UPGRADE: return beatLED(COLOR_MAGENTA, (int[]){ 50, 50 }); + default: return beatLED(COLOR_RED, (int[]){ 80, 100, 80, 1000 } ); + } + } + +protected: + + /* + * LED drivers + */ + +#if defined(BOARD_LED_PIN_WS2812) // Addressable, NeoPixel RGB LED + + void initLED() { + rgb.begin(); + setRGB(COLOR_BLACK); + } + + void setRGB(uint32_t color) { + rgb.setPixelColor(0, color); + rgb.show(); + } + +#elif defined(BOARD_LED_PIN_R) // Normal RGB LED (common anode or common cathode) + + void initLED() { + ledcAttachPin(BOARD_LED_PIN_R, BOARD_LEDC_CHANNEL_1); + ledcAttachPin(BOARD_LED_PIN_G, BOARD_LEDC_CHANNEL_2); + ledcAttachPin(BOARD_LED_PIN_B, BOARD_LEDC_CHANNEL_3); + + ledcSetup(BOARD_LEDC_CHANNEL_1, BOARD_LEDC_BASE_FREQ, BOARD_LEDC_TIMER_BITS); + ledcSetup(BOARD_LEDC_CHANNEL_2, BOARD_LEDC_BASE_FREQ, BOARD_LEDC_TIMER_BITS); + ledcSetup(BOARD_LEDC_CHANNEL_3, BOARD_LEDC_BASE_FREQ, BOARD_LEDC_TIMER_BITS); + } + + void setRGB(uint32_t color) { + uint8_t r = (color & 0xFF0000) >> 16; + uint8_t g = (color & 0x00FF00) >> 8; + uint8_t b = (color & 0x0000FF); + #if BOARD_LED_INVERSE + ledcWrite(BOARD_LEDC_CHANNEL_1, TO_PWM(255 - r)); + ledcWrite(BOARD_LEDC_CHANNEL_2, TO_PWM(255 - g)); + ledcWrite(BOARD_LEDC_CHANNEL_3, TO_PWM(255 - b)); + #else + ledcWrite(BOARD_LEDC_CHANNEL_1, TO_PWM(r)); + ledcWrite(BOARD_LEDC_CHANNEL_2, TO_PWM(g)); + ledcWrite(BOARD_LEDC_CHANNEL_3, TO_PWM(b)); + #endif + } + +#elif defined(BOARD_LED_PIN) // Single color LED + + void initLED() { + ledcSetup(BOARD_LEDC_CHANNEL_1, BOARD_LEDC_BASE_FREQ, BOARD_LEDC_TIMER_BITS); + ledcAttachPin(BOARD_LED_PIN, BOARD_LEDC_CHANNEL_1); + } + + void setLED(uint32_t color) { + #if BOARD_LED_INVERSE + ledcWrite(BOARD_LEDC_CHANNEL_1, TO_PWM(255 - color)); + #else + ledcWrite(BOARD_LEDC_CHANNEL_1, TO_PWM(color)); + #endif + } + +#else + + #warning Invalid LED configuration. + + void initLED() { + } + + void setLED(uint32_t color) { + } + +#endif + + /* + * Animations + */ + + uint32_t skipLED() { + return 20; + } + +#if defined(BOARD_LED_IS_RGB) + + template + uint32_t beatLED(uint32_t onColor, const T& beat) { + const uint8_t cnt = sizeof(beat)/sizeof(beat[0]); + setRGB((m_Counter % 2 == 0) ? onColor : (uint32_t)COLOR_BLACK); + uint32_t next = beat[m_Counter % cnt]; + m_Counter = (m_Counter+1) % cnt; + return next; + } + + uint32_t waveLED(uint32_t colorMax, unsigned breathePeriod) { + uint8_t redMax = (colorMax & 0xFF0000) >> 16; + uint8_t greenMax = (colorMax & 0x00FF00) >> 8; + uint8_t blueMax = (colorMax & 0x0000FF); + + // Brightness will rise from 0 to 128, then fall back to 0 + uint8_t brightness = (m_Counter < 128) ? m_Counter : 255 - m_Counter; + + // Multiply our three colors by the brightness: + redMax *= ((float)brightness / 128.0); + greenMax *= ((float)brightness / 128.0); + blueMax *= ((float)brightness / 128.0); + // And turn the LED to that color: + setRGB((redMax << 16) | (greenMax << 8) | blueMax); + + // This function relies on the 8-bit, unsigned m_Counter rolling over. + m_Counter = (m_Counter+1) % 256; + return breathePeriod / 256; + } + +#else + + template + uint32_t beatLED(uint32_t, const T& beat) { + const uint8_t cnt = sizeof(beat)/sizeof(beat[0]); + setLED((m_Counter % 2 == 0) ? BOARD_LED_BRIGHTNESS : 0); + uint32_t next = beat[m_Counter % cnt]; + m_Counter = (m_Counter+1) % cnt; + return next; + } + + uint32_t waveLED(uint32_t, unsigned breathePeriod) { + uint32_t brightness = (m_Counter < 128) ? m_Counter : 255 - m_Counter; + + setLED(DIMM(brightness*2)); + + // This function relies on the 8-bit, unsigned m_Counter rolling over. + m_Counter = (m_Counter+1) % 256; + return breathePeriod / 256; + } + +#endif + +private: + uint8_t m_Counter; + State m_PrevState; +}; + +Indicator indicator; + +/* + * Animation timers + */ + +#if defined(USE_TICKER) + + #include + + Ticker blinker; + + void indicator_run() { + uint32_t returnTime = indicator.run(); + if (returnTime) { + blinker.attach_ms(returnTime, indicator_run); + } + } + + void indicator_init() { + indicator.init(); + blinker.attach_ms(100, indicator_run); + } + +#elif defined(USE_PTHREAD) + + #include + + pthread_t blinker; + + void* indicator_thread(void*) { + while (true) { + uint32_t returnTime = indicator.run(); + returnTime = BlynkMathClamp(returnTime, 1, 10000); + vTaskDelay(returnTime); + } + } + + void indicator_init() { + indicator.init(); + pthread_create(&blinker, NULL, indicator_thread, NULL); + } + +#elif defined(USE_TIMER_ONE) + + #include + + void indicator_run() { + uint32_t returnTime = indicator.run(); + if (returnTime) { + Timer1.initialize(returnTime*1000); + } + } + + void indicator_init() { + indicator.init(); + Timer1.initialize(100*1000); + Timer1.attachInterrupt(indicator_run); + } + +#elif defined(USE_TIMER_THREE) + + #include + + void indicator_run() { + uint32_t returnTime = indicator.run(); + if (returnTime) { + Timer3.initialize(returnTime*1000); + } + } + + void indicator_init() { + indicator.init(); + Timer3.initialize(100*1000); + Timer3.attachInterrupt(indicator_run); + } + +#elif defined(USE_TIMER_FIVE) + + #include // Library: https://github.com/michael71/Timer5 + + int indicator_counter = -1; + void indicator_run() { + indicator_counter -= 10; + if (indicator_counter < 0) { + indicator_counter = indicator.run(); + } + } + + void indicator_init() { + indicator.init(); + MyTimer5.begin(1000/10); + MyTimer5.attachInterrupt(indicator_run); + MyTimer5.start(); + } + +#else + + #warning LED indicator needs a functional timer! + + void indicator_run() {} + void indicator_init() {} + +#endif diff --git a/CO2-100/OTA.h b/CO2-100/OTA.h new file mode 100644 index 0000000..ae359aa --- /dev/null +++ b/CO2-100/OTA.h @@ -0,0 +1,74 @@ + +#include +#include +#include + +String overTheAirURL; + +extern BlynkTimer edgentTimer; + +BLYNK_WRITE(InternalPinOTA) { + overTheAirURL = param.asString(); + + edgentTimer.setTimeout(2000L, [](){ + // Start OTA + Blynk.logEvent("sys_ota", "OTA started"); + + // Disconnect, not to interfere with OTA process + Blynk.disconnect(); + + BlynkState::set(MODE_OTA_UPGRADE); + }); +} + +void enterOTA() { + BlynkState::set(MODE_OTA_UPGRADE); + + DEBUG_PRINT(String("Firmware update URL: ") + overTheAirURL); + + HTTPClient http; + http.begin(overTheAirURL); + + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + DEBUG_PRINT("HTTP response should be 200"); + BlynkState::set(MODE_ERROR); + return; + } + int contentLength = http.getSize(); + if (contentLength <= 0) { + DEBUG_PRINT("Content-Length not defined"); + BlynkState::set(MODE_ERROR); + return; + } + + bool canBegin = Update.begin(contentLength); + if (!canBegin) { + DEBUG_PRINT("Not enough space to begin OTA"); + BlynkState::set(MODE_ERROR); + return; + } + + Client& client = http.getStream(); + int written = Update.writeStream(client); + if (written != contentLength) { + DEBUG_PRINT(String("OTA written ") + written + " / " + contentLength + " bytes"); + BlynkState::set(MODE_ERROR); + return; + } + + if (!Update.end()) { + DEBUG_PRINT("Error #" + String(Update.getError())); + BlynkState::set(MODE_ERROR); + return; + } + + if (!Update.isFinished()) { + DEBUG_PRINT("Update failed."); + BlynkState::set(MODE_ERROR); + return; + } + + DEBUG_PRINT("=== Update successfully completed. Rebooting."); + restartMCU(); +} diff --git a/CO2-100/ResetButton.h b/CO2-100/ResetButton.h new file mode 100644 index 0000000..8c08304 --- /dev/null +++ b/CO2-100/ResetButton.h @@ -0,0 +1,42 @@ + +volatile bool g_buttonPressed = false; +volatile uint32_t g_buttonPressTime = -1; + +void button_action(void) +{ + BlynkState::set(MODE_RESET_CONFIG); +} + +void button_change(void) +{ +#if BOARD_BUTTON_ACTIVE_LOW + bool buttonState = !digitalRead(BOARD_BUTTON_PIN); +#else + bool buttonState = digitalRead(BOARD_BUTTON_PIN); +#endif + + if (buttonState && !g_buttonPressed) { + g_buttonPressTime = millis(); + g_buttonPressed = true; + DEBUG_PRINT("Hold the button for 10 seconds to reset configuration..."); + } else if (!buttonState && g_buttonPressed) { + g_buttonPressed = false; + uint32_t buttonHoldTime = millis() - g_buttonPressTime; + if (buttonHoldTime >= BUTTON_HOLD_TIME_ACTION) { + button_action(); + } else { + // User action + } + g_buttonPressTime = -1; + } +} + +void button_init() +{ +#if BOARD_BUTTON_ACTIVE_LOW + pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); +#else + pinMode(BOARD_BUTTON_PIN, INPUT_PULLDOWN); +#endif + attachInterrupt(BOARD_BUTTON_PIN, button_change, CHANGE); +} diff --git a/CO2-100/Settings.h b/CO2-100/Settings.h new file mode 100644 index 0000000..1bf6d8c --- /dev/null +++ b/CO2-100/Settings.h @@ -0,0 +1,83 @@ + +/* + * Board configuration (see examples below). + */ + +#if defined(USE_WROVER_BOARD) + + #define BOARD_BUTTON_PIN 15 + #define BOARD_BUTTON_ACTIVE_LOW true + + #define BOARD_LED_PIN_R 0 + #define BOARD_LED_PIN_G 2 + #define BOARD_LED_PIN_B 4 + #define BOARD_LED_INVERSE false + #define BOARD_LED_BRIGHTNESS 128 + +#elif defined(USE_TTGO_T7) + + // This board does not have a built-in button + // Connect a button to gpio0 <> GND + #define BOARD_BUTTON_PIN 0 + #define BOARD_BUTTON_ACTIVE_LOW true + + #define BOARD_LED_PIN 19 + #define BOARD_LED_INVERSE false + #define BOARD_LED_BRIGHTNESS 64 + +#else + + #warning "Custom board configuration is used" + + #define BOARD_BUTTON_PIN 0 // Pin where user button is attached + #define BOARD_BUTTON_ACTIVE_LOW true // true if button is "active-low" + + #define BOARD_LED_PIN 4 // Set LED pin - if you have a single-color LED attached + //#define BOARD_LED_PIN_R 15 // Set R,G,B pins - if your LED is PWM RGB + //#define BOARD_LED_PIN_G 12 + //#define BOARD_LED_PIN_B 13 + //#define BOARD_LED_PIN_WS2812 4 // Set if your LED is WS2812 RGB + #define BOARD_LED_INVERSE false // true if LED is common anode, false if common cathode + #define BOARD_LED_BRIGHTNESS 64 // 0..255 brightness control + +#endif + + +/* + * Advanced options + */ + +#define BUTTON_HOLD_TIME_INDICATION 3000 +#define BUTTON_HOLD_TIME_ACTION 10000 + +#define BOARD_PWM_MAX 1023 + +#define BOARD_LEDC_CHANNEL_1 1 +#define BOARD_LEDC_CHANNEL_2 2 +#define BOARD_LEDC_CHANNEL_3 3 +#define BOARD_LEDC_TIMER_BITS 10 +#define BOARD_LEDC_BASE_FREQ 12000 + +#define CONFIG_AP_URL "blynk.setup" +#define CONFIG_DEFAULT_SERVER "blynk.cloud" +#define CONFIG_DEFAULT_PORT 443 + +#define WIFI_NET_CONNECT_TIMEOUT 30000 +#define WIFI_CLOUD_CONNECT_TIMEOUT 30000 +#define WIFI_AP_IP IPAddress(192, 168, 4, 1) +#define WIFI_AP_Subnet IPAddress(255, 255, 255, 0) +//#define WIFI_CAPTIVE_PORTAL_ENABLE + +//#define USE_TICKER +//#define USE_TIMER_ONE +//#define USE_TIMER_THREE +//#define USE_TIMER_FIVE +#define USE_PTHREAD + +#define BLYNK_NO_DEFAULT_BANNER + +#if defined(APP_DEBUG) + #define DEBUG_PRINT(...) BLYNK_LOG1(__VA_ARGS__) +#else + #define DEBUG_PRINT(...) +#endif