diff --git a/Build-instructions-Bestway-WiFi-remote.pdf b/Build-instructions-Bestway-WiFi-remote.pdf index a39e2b8b..d23ab7c2 100644 Binary files a/Build-instructions-Bestway-WiFi-remote.pdf and b/Build-instructions-Bestway-WiFi-remote.pdf differ diff --git a/Code/4-wire-version/data/WebSocket.js b/Code/4-wire-version/data/WebSocket.js index 39ae93de..53036b65 100644 --- a/Code/4-wire-version/data/WebSocket.js +++ b/Code/4-wire-version/data/WebSocket.js @@ -95,7 +95,7 @@ function handlemsg(e) "CONNECT_BAD_CLIENT_ID", // 2 / the server rejected the client identifier "CONNECT_UNAVAILABLE", // 3 / the server was unable to accept the connection "CONNECT_BAD_CREDENTIALS", // 4 / the username/password were rejected - "CONNECT_UNAUTHORIZED" // 5 / e client was not authorized to connect + "CONNECT_UNAUTHORIZED" // 5 / the client was not authorized to connect ] document.getElementById('mqtt').innerHTML = "MQTT: " + mqtt_states[msgobj.MQTT + 4]; @@ -108,6 +108,9 @@ function handlemsg(e) document.getElementById('jetstitle').style.display = (jetsAvailable ? 'inherit' : 'none'); document.getElementById('jetsbutton').style.display = (jetsAvailable ? 'inherit' : 'none'); document.getElementById('jetstotals').style.display = (jetsAvailable ? 'inherit' : 'none'); + + document.getElementById('ciotx').innerHTML = 'CIO TX: ' + (msgobj.CIOTX ? 'Active' : 'Dead'); + document.getElementById('dsptx').innerHTML = 'DSP TX: ' + (msgobj.DSPTX ? 'Active' : 'Dead'); } if (msgobj.CONTENT == "STATES") diff --git a/Code/4-wire-version/data/index.html b/Code/4-wire-version/data/index.html index 054e127a..effa4538 100644 --- a/Code/4-wire-version/data/index.html +++ b/Code/4-wire-version/data/index.html @@ -176,6 +176,8 @@

Totals:

diff --git a/Code/4-wire-version/data/mqtt.html b/Code/4-wire-version/data/mqtt.html index b3c5102c..6fd591ff 100644 --- a/Code/4-wire-version/data/mqtt.html +++ b/Code/4-wire-version/data/mqtt.html @@ -66,6 +66,10 @@ Base Topic: + + Telemetry Interval (s): + + @@ -97,6 +101,7 @@ document.getElementById("mqttPassword").value = json.mqttPassword; document.getElementById("mqttClientId").value = json.mqttClientId; document.getElementById("mqttBaseTopic").value = json.mqttBaseTopic; + document.getElementById("mqttTelemetryInterval").value = json.mqttTelemetryInterval; } } } @@ -121,7 +126,8 @@ "mqttUsername":(document.getElementById("mqttUsername").value), "mqttPassword":(document.getElementById("mqttPassword").value), "mqttClientId":(document.getElementById("mqttClientId").value), - "mqttBaseTopic":(document.getElementById("mqttBaseTopic").value) + "mqttBaseTopic":(document.getElementById("mqttBaseTopic").value), + "mqttTelemetryInterval":(document.getElementById("mqttTelemetryInterval").value) }; console.log(json); Http.send(JSON.stringify(json)); diff --git a/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.cpp b/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.cpp index 06560916..a47c1bf6 100644 --- a/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.cpp +++ b/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.cpp @@ -9,16 +9,16 @@ void CIO::begin() { DSP_RX = D7 Devices are sending on their TX lines, so we read that with RX pins on the ESP */ - cio_serial.begin(9600, SWSERIAL_8N1, D2, D3, false, 64); - cio_serial.setTimeout(50); - dsp_serial.begin(9600, SWSERIAL_8N1, D6, D7, false, 64); - dsp_serial.setTimeout(50); + cio_serial.begin(9600, SWSERIAL_8N1, D2, D3, false, 63); + cio_serial.setTimeout(20); + dsp_serial.begin(9600, SWSERIAL_8N1, D6, D7, false, 63); + dsp_serial.setTimeout(20); states[TARGET] = 20; //Not used. Here for compatibility reasons states[LOCKEDSTATE] = false; states[POWERSTATE] = true; - states[UNITSTATE] = true; //Celsius + states[UNITSTATE] = true; //Celsius. can be changed by web gui states[CHAR1] = ' '; states[CHAR2] = ' '; states[CHAR3] = ' '; @@ -27,63 +27,102 @@ void CIO::begin() { } void CIO::loop(void) { + digitalWrite(D4, LOW); //LED off //check if CIO has sent a message int msglen = 0; - if(cio_serial.available()){ + if(cio_serial.available()) + { msglen = cio_serial.readBytes(from_CIO_buf, PAYLOADSIZE); - } - //pass message to display - if(msglen == PAYLOADSIZE){ - //discard message if checksum is wrong - uint8_t calculatedChecksum; - calculatedChecksum = from_CIO_buf[1]+from_CIO_buf[2]+from_CIO_buf[3]+from_CIO_buf[4]; - if(from_CIO_buf[CIO_CHECKSUMINDEX] == calculatedChecksum){ - for(int i = 0; i < PAYLOADSIZE; i++){ - if(to_DSP_buf[i] != from_CIO_buf[i]) dataAvailable = true; - to_DSP_buf[i] = from_CIO_buf[i]; + //copy from_CIO_buf -> to_DSP_buf + if(msglen == PAYLOADSIZE){ + //discard message if checksum is wrong + uint8_t calculatedChecksum; + calculatedChecksum = from_CIO_buf[1]+from_CIO_buf[2]+from_CIO_buf[3]+from_CIO_buf[4]; + if(from_CIO_buf[CIO_CHECKSUMINDEX] == calculatedChecksum){ + for(int i = 0; i < PAYLOADSIZE; i++){ + if(to_DSP_buf[i] != from_CIO_buf[i]) dataAvailable = true; + to_DSP_buf[i] = from_CIO_buf[i]; + } + } + states[TEMPERATURE] = from_CIO_buf[TEMPINDEX]; + states[ERROR] = from_CIO_buf[ERRORINDEX]; + cio_tx = true; //show the user that this line works (appears to work) + //check if cio send error msg + states[CHAR1] = ' '; + states[CHAR2] = ' '; + states[CHAR3] = ' '; + if(states[ERROR]){ + to_CIO_buf[COMMANDINDEX] = 0; //clear any commands + GODMODE = false; + states[CHAR1] = 'E'; + states[CHAR2] = (char)(48+(from_CIO_buf[ERRORINDEX]/10)); + states[CHAR3] = (char)(48+(from_CIO_buf[ERRORINDEX]%10)); + } + } else + { + digitalWrite(D4, HIGH); //LED on indicates bad message + } + /* debug + else + { + if(msglen) + { + dataAvailable = true; + for(int i = 0; i < msglen; i++) + { + dismissed_from_CIO_buf[i] = from_CIO_buf[i]; + } + dismissed_cio_len = msglen; } } - states[TEMPERATURE] = from_CIO_buf[TEMPINDEX]; - states[ERROR] = from_CIO_buf[ERRORINDEX]; - //do stuff here if you want to alter the message + */ + // Do stuff here if you want to alter the message + // Send last good message to DSP dsp_serial.write(to_DSP_buf, PAYLOADSIZE); - digitalWrite(D4, !digitalRead(D4)); //blink - cio_tx = true; //show the user that this line works (appears to work) - } - //check if cio send error msg - states[CHAR1] = ' '; - states[CHAR2] = ' '; - states[CHAR3] = ' '; - if(states[ERROR]){ - to_CIO_buf[COMMANDINDEX] = 0; //clear any commands - GODMODE = false; - states[CHAR1] = 'E'; - states[CHAR2] = 48+(to_CIO_buf[ERRORINDEX]/10); - states[CHAR3] = 48+(to_CIO_buf[ERRORINDEX]%10); } //check if display sent a message msglen = 0; - if(dsp_serial.available()){ + if(dsp_serial.available()) + { msglen = dsp_serial.readBytes(from_DSP_buf, PAYLOADSIZE); - } - //pass message to CIO - if(msglen == PAYLOADSIZE){ - //discard message if checksum is wrong - uint8_t calculatedChecksum; - calculatedChecksum = from_DSP_buf[1]+from_DSP_buf[2]+from_DSP_buf[3]+from_DSP_buf[4]; - if(from_DSP_buf[DSP_CHECKSUMINDEX] == calculatedChecksum){ - for(int i = 0; i < PAYLOADSIZE; i++){ - to_CIO_buf[i] = from_DSP_buf[i]; + //copy from_DSP_buf -> to_CIO_buf + if(msglen == PAYLOADSIZE){ + //discard message if checksum is wrong + uint8_t calculatedChecksum; + calculatedChecksum = from_DSP_buf[1]+from_DSP_buf[2]+from_DSP_buf[3]+from_DSP_buf[4]; + if(from_DSP_buf[DSP_CHECKSUMINDEX] == calculatedChecksum) + { + for(int i = 0; i < PAYLOADSIZE; i++) + { + to_CIO_buf[i] = from_DSP_buf[i]; + } + //Do stuff here to command the CIO + if(GODMODE){ + updatePayload(); + } else { + updateStates(); + } + dsp_tx = true; //show the user that this line works (appears to work) } + } else + { + digitalWrite(D4, HIGH); //LED on indicates bad message } - //Do stuff here to command the CIO - if(GODMODE){ - updatePayload(); - } else { - updateStates(); + /* debug + else + { + if(msglen) + { + dataAvailable = true; + for(int i = 0; i < msglen; i++) + { + dismissed_from_DSP_buf[i] = from_DSP_buf[i]; + } + dismissed_dsp_len = msglen; + } } + */ cio_serial.write(to_CIO_buf, PAYLOADSIZE); - dsp_tx = true; //show the user that this line works (appears to work) } } @@ -126,15 +165,15 @@ void CIO::updatePayload(){ //calc checksum -> byte5 //THIS NEEDS TO BE IMPROVED IF OTHER CHECKSUMS IS USED (FOR OTHER BYTES in different models) - to_CIO_buf[DSP_CHECKSUMINDEX] = to_CIO_buf[1] + to_CIO_buf[2] + to_CIO_buf[3] + to_CIO_buf[4]; - if(to_CIO_buf[DSP_CHECKSUMINDEX] != prevchksum) dataAvailable = true; - prevchksum = to_CIO_buf[DSP_CHECKSUMINDEX]; + to_CIO_buf[CIO_CHECKSUMINDEX] = to_CIO_buf[1] + to_CIO_buf[2] + to_CIO_buf[3] + to_CIO_buf[4]; + if(to_CIO_buf[CIO_CHECKSUMINDEX] != prevchksum) dataAvailable = true; + prevchksum = to_CIO_buf[CIO_CHECKSUMINDEX]; } void BWC::begin(void){ _cio.begin(); - _startNTP(); + //_startNTP(); this is done from main.cpp LittleFS.begin(); _loadSettings(); _loadCommandQueue(); @@ -166,13 +205,7 @@ void BWC::loop(){ //feed the dog ESP.wdtFeed(); ESP.wdtDisable(); - - if (!DateTime.isTimeValid()) { - //Serial.println("Failed to get time from server, retry."); - DateTime.begin(); - } - _timestamp = DateTime.now(); - + _timestamp = DateTime.now(); _updateTimes(); //feed the dog //ESP.wdtFeed(); @@ -430,6 +463,7 @@ String BWC::getJSONStates() { // Set the values in the document doc["CONTENT"] = "STATES"; + doc["TIME"] = _timestamp; doc["LCK"] = _cio.states[LOCKEDSTATE]; doc["PWR"] = _cio.states[POWERSTATE]; doc["UNT"] = _cio.states[UNITSTATE]; @@ -443,6 +477,7 @@ String BWC::getJSONStates() { doc["CH2"] = _cio.states[CHAR2]; doc["CH3"] = _cio.states[CHAR3]; doc["JET"] = _cio.states[JETSSTATE]; + doc["ERR"] = _cio.states[ERROR]; doc["GOD"] = _cio.GODMODE; // Serialize JSON to string @@ -559,6 +594,42 @@ String BWC::getJSONCommandQueue(){ return jsonmsg; } +String BWC::encodeBufferToString(uint8_t buf[7]){ + String str = String(); + for (unsigned long i = 0; i < 7; i++) { + str += String(buf[i], HEX); + str += " "; + } + return str; +} + +String BWC::getSerialBuffers(){ + ESP.wdtFeed(); + DynamicJsonDocument doc(512); + + // Set the values in the document + doc["CONTENT"] = "DEBUG"; + doc["TIME"] = _timestamp; + doc["FROMDSP"] = encodeBufferToString(_cio.from_DSP_buf); + doc["TOCIO"] = encodeBufferToString(_cio.to_CIO_buf); + doc["FROMCIO"] = encodeBufferToString(_cio.from_CIO_buf); + doc["TODSP"] = encodeBufferToString(_cio.to_DSP_buf); + doc["REBOOTINFO"] = ESP.getResetReason(); + doc["REBOOTTIME"] = DateTime.getBootTime(); + /* debug + doc["FROMDSPFAIL"] = encodeBufferToString(_cio.dismissed_from_DSP_buf); + doc["LENDSP"] = _cio.dismissed_dsp_len; + doc["FROMCIOFAIL"] = encodeBufferToString(_cio.dismissed_from_CIO_buf); + doc["LENCIO"] = _cio.dismissed_cio_len; + */ + // Serialize JSON to string + String json; + if (serializeJson(doc, json) == 0) { + json = "{\"error\": \"Failed to serialize message\"}"; + } + return json; +} + bool BWC::newData(){ bool result = _cio.dataAvailable; _cio.dataAvailable = false; @@ -631,6 +702,7 @@ void BWC::saveSettingsFlag(){ void BWC::saveSettings(){ //kill the dog + ESP.wdtFeed(); ESP.wdtDisable(); _saveSettingsNeeded = false; File file = LittleFS.open("settings.txt", "w"); @@ -674,7 +746,7 @@ void BWC::saveSettings(){ file.close(); //update clock //DateTime.setTimeZone(_timezone); //deprecated - DateTime.begin(); + //DateTime.begin(); //removed to lower risk of wdt reset. //revive the dog ESP.wdtEnable(0); @@ -713,6 +785,7 @@ void BWC::_loadCommandQueue(){ void BWC::_saveCommandQueue(){ //kill the dog + ESP.wdtFeed(); ESP.wdtDisable(); _saveCmdqNeeded = false; @@ -749,6 +822,7 @@ void BWC::_saveCommandQueue(){ void BWC::saveEventlog(){ _saveEventlogNeeded = false; //kill the dog + ESP.wdtFeed(); ESP.wdtDisable(); File file = LittleFS.open("eventlog.txt", "a"); if (!file) { diff --git a/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.h b/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.h index c400148b..2819f17e 100644 --- a/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.h +++ b/Code/4-wire-version/lib/BWC4W/BWC_8266_4w.h @@ -17,30 +17,36 @@ #include #include #include -#include "SoftwareSerial.h" +#include class CIO { public: void begin(); - void loop(void); - void updatePayload(); - void updateStates(); - - bool dataAvailable = false; - bool GODMODE = false; - uint8_t states[15]; - SoftwareSerial dsp_serial; - SoftwareSerial cio_serial; - - uint8_t from_CIO_buf[7]; //CIO to ESP. We will copy it straight to display, and getting the temperature - uint8_t to_DSP_buf[7]; //ESP to DSP - uint8_t from_DSP_buf[7]; //DSP to ESP. We can ignore this message and send our own when ESP is in charge. - uint8_t to_CIO_buf[7]; //Otherwise copy here. Buffer to send from ESP to CIO - bool cio_tx; //set to true when data received. Send to webinterface+serial for debugging - bool dsp_tx; //set to true when data received. Send to webinterface+serial for debugging - - uint8_t heatbitmask; + void loop(void); + void updatePayload(); + void updateStates(); + + bool dataAvailable = false; + bool GODMODE = false; + uint8_t states[15]; + SoftwareSerial dsp_serial; + SoftwareSerial cio_serial; + + uint8_t from_CIO_buf[7]; //CIO to ESP. We will copy it straight to display, and getting the temperature + uint8_t to_DSP_buf[7]; //ESP to DSP + uint8_t from_DSP_buf[7]; //DSP to ESP. We can ignore this message and send our own when ESP is in charge. + uint8_t to_CIO_buf[7]; //Otherwise copy here. Buffer to send from ESP to CIO + bool cio_tx; //set to true when data received. Send to webinterface+serial for debugging + bool dsp_tx; //set to true when data received. Send to webinterface+serial for debugging + + uint8_t heatbitmask; + + /* Debug */ + uint8_t dismissed_from_CIO_buf[7]; //CIO to ESP. We will copy it straight to display, and getting the temperature + uint8_t dismissed_from_DSP_buf[7]; //DSP to ESP. We can ignore this message and send our own when ESP is in charge. + uint8_t dismissed_cio_len; + uint8_t dismissed_dsp_len; private: @@ -67,6 +73,8 @@ class BWC { bool cio_tx; bool dsp_tx; void reloadCommandQueue(); + String encodeBufferToString(uint8_t buf[7]); + String getSerialBuffers(); private: CIO _cio; diff --git a/Code/4-wire-version/lib/BWC4W/model.h b/Code/4-wire-version/lib/BWC4W/model.h index 4e0147eb..7837b604 100644 --- a/Code/4-wire-version/lib/BWC4W/model.h +++ b/Code/4-wire-version/lib/BWC4W/model.h @@ -3,7 +3,7 @@ #define NO54138 //no heater or jets when bubbles are on. //#define NO54173 //this is same as 54138 but can run heater on 50% when bubbles are on. //#define NO54123 //not tested. ref https://github.com/mrQ000/layz-rc link to presentation slides -//#define NO54154 //no jets +//#define NO54154 //no jets. Works for Palm Springs 54129 //WARNING: DEVICES HAVE DIFFERENT PINOUTS!!! CHECK BEFORE USING diff --git a/Code/4-wire-version/platformio.ini b/Code/4-wire-version/platformio.ini index d0b88a2f..1013bb6d 100644 --- a/Code/4-wire-version/platformio.ini +++ b/Code/4-wire-version/platformio.ini @@ -13,7 +13,7 @@ src_dir = src data_dir = data [env:nodemcuv2] -platform = espressif8266@^2 +platform = espressif8266 board = nodemcuv2 framework = arduino lib_deps = @@ -22,17 +22,17 @@ lib_deps = links2004/WebSockets@^2.3.3 knolleary/PubSubClient@^2.8 tzapu/WiFiManager@^0.16 + plerup/EspSoftwareSerial@^6.15.2 board_build.filesystem = littlefs monitor_speed = 115200 ;Set upload speed to 115200 if you get transfer errors -upload_speed = 921600 +;upload_speed = 921600 ;board_build.f_cpu = 160000000L ; Uncomment the lines below by removing semicolons and edit IP for OTA upload. ; You have to upload via USB cable the first time. (upload_protocol = esptool) ; Make sure to use a data-USB cable. There is power only cables that wont work. upload_protocol = esptool -;uncomment and edit IP for OTA upload ;upload_protocol = espota ;upload_port = 192.168.4.121 ;upload_flags = diff --git a/Code/4-wire-version/src/config.h b/Code/4-wire-version/src/config.h index 31fefe0d..83d0dffd 100644 --- a/Code/4-wire-version/src/config.h +++ b/Code/4-wire-version/src/config.h @@ -53,8 +53,9 @@ const char *OTAPassword = "esp8266"; const bool enableWmApFallback = true; /** get the name for the WiFi configuration manager access point */ const char *wmApName = "Lay-Z-Spa Module"; -/** get the password for the WiFi configuration manager (min. 8, max. 63 chars) */ +/** get the password for the WiFi configuration manager (min. 8, max. 63 chars; NULL to disable) */ const char *wmApPassword = "layzspam0dule"; +//const char *wmApPassword = NULL; /* * WiFi Access Point @@ -108,3 +109,10 @@ String mqttPassword = "password"; String mqttClientId = LEGACY_NAME; /** get or set the MQTT topic name */ String mqttBaseTopic = LEGACY_NAME; +/** get or set the MQTT telemetry interval */ +int mqttTelemetryInterval = 600; + +/* only enable this when debugging communication + * This feature has not been tested yet + */ +bool DEBUGSERIAL = false; \ No newline at end of file diff --git a/Code/4-wire-version/src/main.cpp b/Code/4-wire-version/src/main.cpp index a2c9752f..f7732c09 100644 --- a/Code/4-wire-version/src/main.cpp +++ b/Code/4-wire-version/src/main.cpp @@ -28,9 +28,6 @@ void setup() // update webpage every 2 seconds. (will also be updated on state changes) updateWSTimer.attach(2.0, []{ sendWSFlag = true; }); - // update MQTT every 10 minutes. (will also be updated on state changes) - updateMqttTimer.attach(600, []{ sendMQTTFlag = true; }); - // needs to be loaded here for reading the wifi.json LittleFS.begin(); loadWifi(); @@ -40,7 +37,7 @@ void setup() startOTA(); startHttpServer(); startWebSocket(); - startMQTT(); + startMqtt(); pinMode(D4, OUTPUT); //built in LED for some feedback digitalWrite(D4, LOW); @@ -93,9 +90,12 @@ void loop() // run once after connection was established if (!wifiConnected) { - Serial.println("WiFi > Connected"); + Serial.println(F("WiFi > Connected")); Serial.println(" SSID: \"" + WiFi.SSID() + "\""); Serial.println(" IP: \"" + WiFi.localIP().toString() + "\""); + startOTA(); + startHttpServer(); + startWebSocket(); } // reset marker wifiConnected = true; @@ -107,7 +107,7 @@ void loop() // run once after connection was lost if (wifiConnected) { - Serial.println("WiFi > Lost connection. Trying to reconnect ..."); + Serial.println(F("WiFi > Lost connection. Trying to reconnect ...")); } // set marker wifiConnected = false; @@ -131,13 +131,13 @@ void loop() if (!DateTime.isTimeValid()) { Serial.println(F("NTP > Start synchronisation")); - DateTime.begin(); + DateTime.begin(5000); } - if (!mqttClient.loop()) + if (enableMqtt && !mqttClient.loop()) { - Serial.println(F("MQTT > Reconnecting")); - MQTT_Connect(); + Serial.println(F("MQTT > Not connected")); + mqttConnect(); } } } @@ -191,12 +191,19 @@ void sendWS() json = bwc.getJSONTimes(); webSocket.broadcastTXT(json); + if(DEBUGSERIAL) + { + json = bwc.getSerialBuffers(); + webSocket.broadcastTXT(json); + } + // send other info String other = String("{\"CONTENT\":\"OTHER\",\"MQTT\":") + String(mqttClient.state()) + - String(",\"CIOTX\":\"") + String(bwc.cio_tx) + - String("\",\"DSPTX\":") + String(bwc.dsp_tx) + String("}"); - + String(",\"CIOTX\":") + String(bwc.cio_tx) + + String(",\"DSPTX\":") + String(bwc.dsp_tx) + + String(",\"RSSI\":") + String(WiFi.RSSI()) + + String("}"); webSocket.broadcastTXT(other); } @@ -216,22 +223,22 @@ void sendMQTT() json = bwc.getJSONStates(); if (mqttClient.publish((String(mqttBaseTopic) + "/message").c_str(), String(json).c_str(), true)) { - //Serial.println(F("MQTT published")); + //Serial.println(F("MQTT > message published")); } else { - //Serial.println(F("MQTT not published")); + //Serial.println(F("MQTT > message not published")); } // send times json = bwc.getJSONTimes(); if (mqttClient.publish((String(mqttBaseTopic) + "/times").c_str(), String(json).c_str(), true)) { - //Serial.println(F("MQTT published")); + //Serial.println(F("MQTT > times published")); } else { - //Serial.println(F("MQTT not published")); + //Serial.println(F("MQTT > times not published")); } } @@ -260,7 +267,7 @@ void startWiFi() WiFi.begin(apSsid, apPwd); - Serial.print("WiFi > Trying to connect ..."); + Serial.print(F("WiFi > Trying to connect ...")); int maxTries = 10; int tryCount = 0; @@ -273,7 +280,7 @@ void startWiFi() if (tryCount >= maxTries) { Serial.println(""); - Serial.println("WiFi > NOT connected!"); + Serial.println(F("WiFi > NOT connected!")); if (enableWmApFallback) { // disable specific WiFi config @@ -301,7 +308,7 @@ void startWiFi() wifiConnected = true; - Serial.println("WiFi > Connected."); + Serial.println(F("WiFi > Connected.")); Serial.println(" SSID: \"" + WiFi.SSID() + "\""); Serial.println(" IP: \"" + WiFi.localIP().toString() + "\""); } @@ -316,14 +323,15 @@ void startWiFi() */ void startWiFiConfigPortal() { - Serial.println("WiFi > Using WiFiManager Config Portal"); + Serial.println(F("WiFi > Using WiFiManager Config Portal")); wm.autoConnect(wmApName, wmApPassword); - Serial.print("WiFi > Trying to connect ..."); + Serial.print(F("WiFi > Trying to connect ...")); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } + Serial.println(""); } @@ -334,7 +342,7 @@ void startWiFiConfigPortal() void startNTP() { DateTime.setServer("pool.ntp.org"); - DateTime.begin(3000); + DateTime.begin(5000); } @@ -348,17 +356,17 @@ void startOTA() ArduinoOTA.setPassword(OTAPassword); ArduinoOTA.onStart([]() { - Serial.println(F("OTA Start")); + Serial.println(F("OTA > Start")); //bwc.stop(); // TODO: 6-wire only(?) }); ArduinoOTA.onEnd([]() { - Serial.println(F("\r\nOTA End")); + Serial.println(F("OTA > End")); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + Serial.printf("OTA > Progress: %u%%\r\n", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); + Serial.printf("OTA > Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println(F("Auth Failed")); else if (error == OTA_BEGIN_ERROR) Serial.println(F("Begin Failed")); else if (error == OTA_CONNECT_ERROR) Serial.println(F("Connect Failed")); @@ -376,6 +384,8 @@ void startOTA() */ void startWebSocket() { + // In case we are already running + webSocket.close(); webSocket.begin(); webSocket.onEvent(webSocketEvent); Serial.println(F("WebSocket > server started")); @@ -391,14 +401,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) { // if the websocket is disconnected case WStype_DISCONNECTED: - Serial.printf("WebSocket > [%u] Disconnected!\n", num); + Serial.printf("WebSocket > [%u] Disconnected!\r\n", num); break; // if a new websocket connection is established case WStype_CONNECTED: { IPAddress ip = webSocket.remoteIP(num); - Serial.printf("WebSocket > [%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + Serial.printf("WebSocket > [%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0], ip[1], ip[2], ip[3], payload); sendWS(); } break; @@ -406,7 +416,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) // if new text data is received case WStype_TEXT: { - Serial.printf("WebSocket > [%u] get Text: %s\n", num, payload); + Serial.printf("WebSocket > [%u] get Text: %s\r\n", num, payload); DynamicJsonDocument doc(256); DeserializationError error = deserializeJson(doc, payload); if (error) @@ -437,6 +447,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) */ void startHttpServer() { + // In case we are already running + server.stop(); server.on(F("/getconfig/"), handleGetConfig); server.on(F("/setconfig/"), handleSetConfig); server.on(F("/getcommands/"), handleGetCommandQueue); @@ -868,6 +880,7 @@ void loadMqtt() mqttPassword = doc["mqttPassword"].as(); mqttClientId = doc["mqttClientId"].as(); mqttBaseTopic = doc["mqttBaseTopic"].as(); + mqttTelemetryInterval = doc["mqttTelemetryInterval"]; } /** @@ -894,6 +907,7 @@ void saveMqtt() doc["mqttPassword"] = mqttPassword; doc["mqttClientId"] = mqttClientId; doc["mqttBaseTopic"] = mqttBaseTopic; + doc["mqttTelemetryInterval"] = mqttTelemetryInterval; if (serializeJson(doc, file) == 0) { @@ -926,6 +940,7 @@ void handleGetMqtt() } doc["mqttClientId"] = mqttClientId; doc["mqttBaseTopic"] = mqttBaseTopic; + doc["mqttTelemetryInterval"] = mqttTelemetryInterval; String json; if (serializeJson(doc, json) == 0) @@ -963,11 +978,12 @@ void handleSetMqtt() mqttPassword = doc["mqttPassword"].as(); mqttClientId = doc["mqttClientId"].as(); mqttBaseTopic = doc["mqttBaseTopic"].as(); + mqttTelemetryInterval = doc["mqttTelemetryInterval"]; server.send(200, "text/plain", ""); saveMqtt(); - startMQTT(); + startMqtt(); } @@ -983,7 +999,7 @@ void handleDir() { Serial.println(root.fileName()); mydir += root.fileName() + F(" \t Size: "); - mydir += String(root.fileSize()) + F(" Bytes\n"); + mydir += String(root.fileSize()) + F(" Bytes\r\n"); } server.send(200, "text/plain", mydir); } @@ -1096,7 +1112,6 @@ void handleFileRemove() void handleRestart() { server.send(200, F("text/html"), F("ESP restart ...")); - Serial.println(F("ESP restart ...")); server.sendHeader("Location", "/"); server.send(303); @@ -1108,7 +1123,7 @@ void handleRestart() //bwc.stop(); // TODO: 6-wire only(?) bwc.saveSettings(); - Serial.println("ESP restart ..."); + Serial.println(F("ESP restart ...")); ESP.restart(); } @@ -1118,7 +1133,7 @@ void handleRestart() * MQTT setup and connect * @author 877dev */ -void startMQTT() +void startMqtt() { // load mqtt credential file if it exists, and update default strings loadMqtt(); @@ -1136,21 +1151,18 @@ void startMQTT() mqttClient.setSocketTimeout(30); // set callback details // this function is called automatically whenever a message arrives on a subscribed topic. - mqttClient.setCallback(MQTTcallback); + mqttClient.setCallback(mqttCallback); // Connect to MQTT broker, publish Status/MAC/count, and subscribe to keypad topic. - if (enableMqtt) - { - MQTT_Connect(); - } + mqttConnect(); } /** * MQTT callback function * @author 877dev */ -void MQTTcallback(char* topic, byte* payload, unsigned int length) +void mqttCallback(char* topic, byte* payload, unsigned int length) { - Serial.print(F("Message arrived [")); + Serial.print(F("MQTT > Message arrived [")); Serial.print(topic); Serial.print("] "); for (unsigned int i = 0; i < length; i++) @@ -1179,63 +1191,45 @@ void MQTTcallback(char* topic, byte* payload, unsigned int length) /** * Connect to MQTT broker, publish Status/MAC/count, and subscribe to keypad topic. */ -void MQTT_Connect() +void mqttConnect() { - Serial.print(F("Connecting to MQTT... ")); - // We'll connect with a Retained Last Will that updates the '.../Status' topic with "Dead" when the device goes offline... - // Attempt to connect... - /* - MQTT Connection syntax: - boolean connect (client_id, username, password, willTopic, willQoS, willRetain, willMessage) - Connects the client with a Will message, username and password specified. - Parameters - client_id : the client ID to use when connecting to the server. - username : the username to use. If NULL, no username or password is used (const char[]) - password : the password to use. If NULL, no password is used (const char[]) - willTopic : the topic to be used by the will message (const char[]) - willQoS : the quality of service to be used by the will message (int : 0,1 or 2) - willRetain : whether the will should be published with the retain flag (int : 0 or 1) - willMessage : the payload of the will message (const char[]) - Returns - false - connection failed. - true - connection succeeded - */ - if (mqttClient.connect(mqttClientId.c_str(), mqttUsername.c_str(), mqttPassword.c_str(), (String(mqttBaseTopic) + "/Status").c_str(), 0, 1, "Dead")) + // do not connect if MQTT is not enabled + if (!enableMqtt) { - // We get here if the connection was successful... + return; + } + + Serial.print(F("MQTT > Connecting ... ")); + // We'll connect with a Retained Last Will that updates the 'Status' topic with "Dead" when the device goes offline... + if (mqttClient.connect( + mqttClientId.c_str(), // client_id : the client ID to use when connecting to the server. + mqttUsername.c_str(), // username : the username to use. If NULL, no username or password is used (const char[]) + mqttPassword.c_str(), // password : the password to use. If NULL, no password is used (const char[]) + (String(mqttBaseTopic) + "/Status").c_str(), // willTopic : the topic to be used by the will message (const char[]) + 0, // willQoS : the quality of service to be used by the will message (int : 0,1 or 2) + 1, // willRetain : whether the will should be published with the retain flag (int : 0 or 1) + "Dead")) // willMessage : the payload of the will message (const char[]) + { + Serial.println(F("success!")); mqtt_connect_count++; - Serial.println(F("CONNECTED!")); - // Once connected, publish some announcements... + + // update MQTT every X seconds. (will also be updated on state changes) + updateMqttTimer.attach(mqttTelemetryInterval, []{ sendMQTTFlag = true; }); + // These all have the Retained flag set to true, so that the value is stored on the server and can be retrieved at any point - // Check the .../Status topic to see that the device is still online before relying on the data from these retained topics + // Check the 'Status' topic to see that the device is still online before relying on the data from these retained topics mqttClient.publish((String(mqttBaseTopic) + "/Status").c_str(), "Alive", true); mqttClient.publish((String(mqttBaseTopic) + "/MAC_Address").c_str(), WiFi.macAddress().c_str(), true); // Device MAC Address mqttClient.publish((String(mqttBaseTopic) + "/MQTT_Connect_Count").c_str(), String(mqtt_connect_count).c_str(), true); // MQTT Connect Count mqttClient.loop(); - // ... and then re/subscribe to the watched topics - mqttClient.subscribe((String(mqttBaseTopic) + "/command").c_str()); // Watch the .../command topic for incoming MQTT messages + // Watch the 'command' topic for incoming MQTT messages + mqttClient.subscribe((String(mqttBaseTopic) + "/command").c_str()); mqttClient.loop(); - // Add other watched topics in here... } else { - // We get here if the connection failed... - Serial.print(F("MQTT Connection FAILED, Return Code = ")); - Serial.println(mqttClient.state()); - Serial.println(); - /* - mqttClient.state return code meanings... - -4 : MQTT_CONNECTION_TIMEOUT - the server didn't respond within the keepalive time - -3 : MQTT_CONNECTION_LOST - the network connection was broken - -2 : MQTT_CONNECT_FAILED - the network connection failed - -1 : MQTT_DISCONNECTED - the client is disconnected cleanly - 0 : MQTT_CONNECTED - the client is connected - 1 : MQTT_CONNECT_BAD_PROTOCOL - the server doesn't support the requested version of MQTT - 2 : MQTT_CONNECT_BAD_CLIENT_ID - the server rejected the client identifier - 3 : MQTT_CONNECT_UNAVAILABLE - the server was unable to accept the connection - 4 : MQTT_CONNECT_BAD_CREDENTIALS - the username/password were rejected - 5 : MQTT_CONNECT_UNAUTHORIZED - the client was not authorized to connect * - */ + Serial.print(F("failed, Return Code = ")); + Serial.println(mqttClient.state()); // states explained in WebSocket.js } } diff --git a/Code/4-wire-version/src/main.h b/Code/4-wire-version/src/main.h index c90cada8..56b2d113 100644 --- a/Code/4-wire-version/src/main.h +++ b/Code/4-wire-version/src/main.h @@ -101,6 +101,6 @@ void handleFileUpload(); void handleFileRemove(); void handleRestart(); -void startMQTT(); -void MQTTcallback(char* topic, byte* payload, unsigned int length); -void MQTT_Connect(); +void startMqtt(); +void mqttCallback(char* topic, byte* payload, unsigned int length); +void mqttConnect(); diff --git a/Code/6-wire-version/data/WebSocket.js b/Code/6-wire-version/data/WebSocket.js index c795f9af..1d9bc31c 100644 --- a/Code/6-wire-version/data/WebSocket.js +++ b/Code/6-wire-version/data/WebSocket.js @@ -94,7 +94,7 @@ function handlemsg(e) "CONNECT_BAD_CLIENT_ID", // 2 / the server rejected the client identifier "CONNECT_UNAVAILABLE", // 3 / the server was unable to accept the connection "CONNECT_BAD_CREDENTIALS", // 4 / the username/password were rejected - "CONNECT_UNAUTHORIZED" // 5 / e client was not authorized to connect + "CONNECT_UNAUTHORIZED" // 5 / the client was not authorized to connect ] document.getElementById('mqtt').innerHTML = "MQTT: " + mqtt_states[msgobj.MQTT + 4]; diff --git a/Code/6-wire-version/data/mqtt.html b/Code/6-wire-version/data/mqtt.html index b3c5102c..6fd591ff 100644 --- a/Code/6-wire-version/data/mqtt.html +++ b/Code/6-wire-version/data/mqtt.html @@ -66,6 +66,10 @@ Base Topic: + + Telemetry Interval (s): + + @@ -97,6 +101,7 @@ document.getElementById("mqttPassword").value = json.mqttPassword; document.getElementById("mqttClientId").value = json.mqttClientId; document.getElementById("mqttBaseTopic").value = json.mqttBaseTopic; + document.getElementById("mqttTelemetryInterval").value = json.mqttTelemetryInterval; } } } @@ -121,7 +126,8 @@ "mqttUsername":(document.getElementById("mqttUsername").value), "mqttPassword":(document.getElementById("mqttPassword").value), "mqttClientId":(document.getElementById("mqttClientId").value), - "mqttBaseTopic":(document.getElementById("mqttBaseTopic").value) + "mqttBaseTopic":(document.getElementById("mqttBaseTopic").value), + "mqttTelemetryInterval":(document.getElementById("mqttTelemetryInterval").value) }; console.log(json); Http.send(JSON.stringify(json)); diff --git a/Code/6-wire-version/lib/BWC/BWC_8266.cpp b/Code/6-wire-version/lib/BWC/BWC_8266.cpp index 9a70b91f..30208bde 100644 --- a/Code/6-wire-version/lib/BWC/BWC_8266.cpp +++ b/Code/6-wire-version/lib/BWC/BWC_8266.cpp @@ -298,6 +298,8 @@ void DSP::updateDSP(uint8_t brightness) { void DSP::textOut(String txt) { int len = txt.length(); + //Set CMD3 (address 00H) + payload[0] = 0xC0; if (len >= 3) { for (int i = 0; i < len - 2; i++) { payload[DGT1_IDX] = _getCode(txt.charAt(i)); @@ -433,16 +435,16 @@ void BWC::begin2(){ _clinterval = 14; _audio = true; _restoreStatesOnStart = false; - _dsp.textOut(F(" hello ")); -//_startNTP(); - LittleFS.begin(); - _loadSettings(); - _loadCommandQueue(); - _saveRebootInfo(); + _dsp.textOut(F(" hello ")); + //_startNTP(); + LittleFS.begin(); + _loadSettings(); + _loadCommandQueue(); + _saveRebootInfo(); _restoreStates(); - if(_audio) _dsp.playIntro(); - _dsp.LEDshow(); - saveSettingsTimer.attach(3600.0, std::bind(&BWC::saveSettingsFlag, this)); + if(_audio) _dsp.playIntro(); + //_dsp.LEDshow(); + saveSettingsTimer.attach(3600.0, std::bind(&BWC::saveSettingsFlag, this)); _tttt = 0; _tttt_calculated = 0; _tttt_time0 = DateTime.now()-3600; @@ -460,12 +462,12 @@ void BWC::loop(){ ESP.wdtFeed(); ESP.wdtDisable(); - _timestamp = DateTime.now(); + _timestamp = DateTime.now(); //update DSP payload (memcpy(dest*, source*, len)) //memcpy(&_dsp.payload[0], &_cio.payload[0], 11); for(int i = 0; i < 11; i++){ - _dsp.payload[i] = _cio.payload[i]; + _dsp.payload[i] = _cio.payload[i]; } _dsp.updateDSP(_dspBrightness); _updateTimes(); @@ -482,7 +484,7 @@ void BWC::loop(){ _cio.stateChanged = false; } if(_saveStatesNeeded) _saveStates(); - //if set target command overshot we need to correct that + //if set target command missed we need to correct that if( (_cio.states[TARGET] != _latestTarget) && (_qButtonLen == 0) && (_latestTarget != 0) && (_sliderPrio) ) qCommand(SETTARGET, _latestTarget, 0, 0); //if target temp is unknown, find out. if( (_cio.states[TARGET] == 0) && (_qButtonLen == 0) ) qCommand(GETTARGET, (uint32_t)' ', 0, 0); @@ -535,19 +537,26 @@ int BWC::_CodeToButton(uint16_t val){ return 0; } -void BWC::_qButton(uint32_t btn, uint32_t state, uint32_t value, uint32_t maxduration) { +void BWC::_qButton(uint32_t btn, uint32_t state, uint32_t value, int32_t maxduration) { if(_qButtonLen == MAXBUTTONS) return; //maybe textout an error message if queue is full? _buttonQ[_qButtonLen][0] = btn; _buttonQ[_qButtonLen][1] = state; _buttonQ[_qButtonLen][2] = value; - _buttonQ[_qButtonLen][3] = maxduration + millis(); + _buttonQ[_qButtonLen][3] = maxduration; _qButtonLen++; } void BWC::_handleButtonQ(void) { + static uint32_t prevMillis = millis(); + static uint32_t elapsedTime = 0; + + elapsedTime = millis() - prevMillis; + prevMillis = millis(); if(_qButtonLen > 0){ + // First subtract elapsed time from maxduration + _buttonQ[0][3] -= elapsedTime; //check if state is as desired, or duration is up. If so - remove row. Else set BTNCODE - if( (_cio.states[_buttonQ[0][1]] == _buttonQ[0][2]) || (millis() > _buttonQ[0][3]) ){ + if( (_cio.states[_buttonQ[0][1]] == _buttonQ[0][2]) || (_buttonQ[0][3] <= 0) ){ if(_buttonQ[0][0] == UP || _buttonQ[0][0] == DOWN) maxeffort = false; //remove row for(int i = 0; i < _qButtonLen-1; i++){ @@ -569,13 +578,13 @@ void BWC::_handleButtonQ(void) { uint16_t pressedButton = _dsp.getButton(); int index = _CodeToButton(pressedButton); //if button is not enabled, NOBTN will result (buttoncodes[0]) - _cio.button = ButtonCodes[index*EnabledButtons[index]]; + _cio.button = ButtonCodes[index * EnabledButtons[index]]; //prioritize manual temp setting by not competing with the set target command if (pressedButton == ButtonCodes[UP] || pressedButton == ButtonCodes[DOWN]) _sliderPrio = false; //make noise if(_audio) { - if((index*EnabledButtons[index]) & (prevbtn == ButtonCodes[NOBTN])) + if((index && EnabledButtons[index]) && (prevbtn == ButtonCodes[NOBTN])) { _dsp.beep2(); } @@ -633,20 +642,35 @@ void BWC::_handleCommandQ(void) { _qButton(LOCK, LOCKEDSTATE, 0, 5000); //press LOCK button until states[LOCKEDSTATE] is 0 switch (_commandQ[0][0]) { case SETTARGET: - _latestTarget = _commandQ[0][1]; - //Fiddling with the hardware buttons is ignored while this command executes. - _sliderPrio = true; - //choose which direction to go (up or down) - if(_cio.states[TARGET] == 0 ) - { - _qButton(UP, TARGET, _commandQ[0][1], 10000); - _qButton(DOWN, TARGET, _commandQ[0][1], 10000); - } - if(_cio.states[TARGET] > _commandQ[0][1]) _qButton(DOWN, TARGET, _commandQ[0][1], 10000); - if(_cio.states[TARGET] < _commandQ[0][1]) _qButton(UP, TARGET, _commandQ[0][1], 10000); - break; + { + _latestTarget = _commandQ[0][1]; + //Fiddling with the hardware buttons is ignored while this command executes. + _sliderPrio = true; + //Press up/down appropriate number of times. We need to time this well. + int diff = (int)_commandQ[0][1] - (int)_cio.states[TARGET]; + //First press will just show current target temp + int pushtime = 500; //how fast can we do this??******************* + int releasetime = 300; + _qButton(UP, TARGET, _commandQ[0][1], pushtime); + _qButton(NOBTN, TARGET, _commandQ[0][1], releasetime); + uint32_t updown; + diff<0 ? updown = DOWN : updown = UP; + for(int i = 0; i < abs(diff); i++) + { + _qButton(updown, CHAR1, 0xFF, pushtime); + _qButton(NOBTN, CHAR1, 0xFF, releasetime); + } + //Old method overshoots target too often: + //choose which direction to go (up or down) + // if(_cio.states[TARGET] > _commandQ[0][1]) _qButton(DOWN, TARGET, _commandQ[0][1], 10000); + // if(_cio.states[TARGET] < _commandQ[0][1]) _qButton(UP, TARGET, _commandQ[0][1], 10000); + break; + } case SETUNIT: _qButton(UNIT, UNITSTATE, _commandQ[0][1], 5000); + _qButton(NOBTN, CHAR3, 0xFF, 700); + _qButton(UP, CHAR3, _commandQ[0][1], 700); + _latestTarget = 0; break; case SETBUBBLES: _qButton(BUBBLES, BUBBLESSTATE, _commandQ[0][1], 5000); @@ -720,6 +744,7 @@ String BWC::getJSONStates() { // Set the values in the document doc["CONTENT"] = "STATES"; + doc["TIME"] = _timestamp; doc["LCK"] = _cio.states[LOCKEDSTATE]; doc["PWR"] = _cio.states[POWERSTATE]; doc["UNT"] = _cio.states[UNITSTATE]; diff --git a/Code/6-wire-version/lib/BWC/BWC_8266.h b/Code/6-wire-version/lib/BWC/BWC_8266.h index 04cc3d6a..d57d621b 100644 --- a/Code/6-wire-version/lib/BWC/BWC_8266.h +++ b/Code/6-wire-version/lib/BWC/BWC_8266.h @@ -129,7 +129,7 @@ class BWC { uint8_t _dspBrightness; uint32_t _commandQ[MAXCOMMANDS][4]; int _qCommandLen = 0; //length of commandQ - uint32_t _buttonQ[MAXBUTTONS][4]; + int32_t _buttonQ[MAXBUTTONS][4]; int _qButtonLen = 0; //length of buttonQ uint32_t _timestamp; bool _newData = false; @@ -166,7 +166,7 @@ class BWC { int _tttt; //time to target temperature after subtracting running time since last calculation int _tttt_calculated; //constant between calculations - void _qButton(uint32_t btn, uint32_t state, uint32_t value, uint32_t maxduration); + void _qButton(uint32_t btn, uint32_t state, uint32_t value, int32_t maxduration); void _handleCommandQ(void); void _handleButtonQ(void); void _startNTP(); diff --git a/Code/6-wire-version/lib/BWC/BWC_8266_globals.h b/Code/6-wire-version/lib/BWC/BWC_8266_globals.h index 926d94c9..7f6287d5 100644 --- a/Code/6-wire-version/lib/BWC/BWC_8266_globals.h +++ b/Code/6-wire-version/lib/BWC/BWC_8266_globals.h @@ -1,15 +1,9 @@ //only declarations here please! Definitions belong in the cpp-file - +#include "model.h" #ifndef BWC_8266_globals_H #define BWC_8266_globals_H -//uncomment your model and comment out the rest -#define PRE2021 //the older one, no hydrojets -//#define MIAMI2021 //no hydrojets -//#define MALDIVES2021 //hydrojets - - //LSB const uint8_t DSP_CMD2_DATAREAD = 0x42; const uint8_t DSP_CMD1_MODE6_11_7 = 0x01; //real CIO is sending 0x01 which is illegal according to datasheet @@ -103,12 +97,21 @@ const uint16_t ButtonCodes[] = }; const bool HASJETS = false; -#else +#elif defined(MALDIVES2021) const uint16_t ButtonCodes[] = { 0x1B1B, 0x0100, 0x0300, 0x1212, 0x0a09, 0x1012, 0x1312, 0x0809, 0x0200, 0x0000, 0x1112 }; const bool HASJETS = true; + +#else +//Make compiler happy. Will not be used. +const uint16_t ButtonCodes[] = +{ + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 +}; +const bool HASJETS = false; + #endif enum States: byte @@ -150,7 +153,7 @@ enum Commands: byte }; const int MAXCOMMANDS = 11; -const int MAXBUTTONS = 33; +const int MAXBUTTONS = 200; //direct port manipulation memory adresses. diff --git a/Code/6-wire-version/lib/BWC/model.h b/Code/6-wire-version/lib/BWC/model.h new file mode 100644 index 00000000..18d34c67 --- /dev/null +++ b/Code/6-wire-version/lib/BWC/model.h @@ -0,0 +1,6 @@ +// Uncomment your model and comment out the rest + +//#define MODEL54149E //Paris airjet 54149E +#define PRE2021 //the older one, no hydrojets +//#define MIAMI2021 //no hydrojets +//#define MALDIVES2021 //hydrojets \ No newline at end of file diff --git a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.cpp b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.cpp index 8131fce8..6ac8f709 100644 --- a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.cpp +++ b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.cpp @@ -518,14 +518,21 @@ void BWC::_qButton(uint32_t btn, uint32_t state, uint32_t value, uint32_t maxdur _buttonQ[_qButtonLen][0] = btn; _buttonQ[_qButtonLen][1] = state; _buttonQ[_qButtonLen][2] = value; - _buttonQ[_qButtonLen][3] = maxduration + millis(); + _buttonQ[_qButtonLen][3] = maxduration; _qButtonLen++; } void BWC::_handleButtonQ(void) { + static uint32_t prevMillis = millis(); + static uint32_t elapsedTime = 0; + + elapsedTime = millis() - prevMillis; + prevMillis = millis(); if(_qButtonLen > 0){ + // First subtract elapsed time from maxduration + _buttonQ[0][3] -= elapsedTime; //check if state is as desired, or duration is up. If so - remove row. Else set BTNCODE - if( (_cio.states[_buttonQ[0][1]] == _buttonQ[0][2]) || (millis() > _buttonQ[0][3]) ){ + if( (_cio.states[_buttonQ[0][1]] == _buttonQ[0][2]) || (_buttonQ[0][3] <= 0) ){ if(_buttonQ[0][0] == UP || _buttonQ[0][0] == DOWN) maxeffort = false; //remove row for(int i = 0; i < _qButtonLen-1; i++){ @@ -547,13 +554,13 @@ void BWC::_handleButtonQ(void) { uint16_t pressedButton = _dsp.getButton(); int index = _CodeToButton(pressedButton); //if button is not enabled, NOBTN will result (buttoncodes[0]) - _cio.button = ButtonCodes[index*EnabledButtons[index]]; + _cio.button = ButtonCodes[index * EnabledButtons[index]]; //prioritize manual temp setting by not competing with the set target command if (pressedButton == ButtonCodes[UP] || pressedButton == ButtonCodes[DOWN]) _sliderPrio = false; //make noise if(_audio) { - if((index*EnabledButtons[index]) & (prevbtn == ButtonCodes[NOBTN])) + if((index && EnabledButtons[index]) && (prevbtn == ButtonCodes[NOBTN])) { _dsp.beep2(); } @@ -611,20 +618,35 @@ void BWC::_handleCommandQ(void) { _qButton(LOCK, LOCKEDSTATE, 0, 5000); //press LOCK button until states[LOCKEDSTATE] is 0 switch (_commandQ[0][0]) { case SETTARGET: - _latestTarget = _commandQ[0][1]; - //Fiddling with the hardware buttons is ignored while this command executes. - _sliderPrio = true; - //choose which direction to go (up or down) - if(_cio.states[TARGET] == 0 ) - { - _qButton(UP, TARGET, _commandQ[0][1], 10000); - _qButton(DOWN, TARGET, _commandQ[0][1], 10000); - } - if(_cio.states[TARGET] > _commandQ[0][1]) _qButton(DOWN, TARGET, _commandQ[0][1], 10000); - if(_cio.states[TARGET] < _commandQ[0][1]) _qButton(UP, TARGET, _commandQ[0][1], 10000); - break; + { + _latestTarget = _commandQ[0][1]; + //Fiddling with the hardware buttons is ignored while this command executes. + _sliderPrio = true; + //Press up/down appropriate number of times. We need to time this well. + int diff = (int)_commandQ[0][1] - (int)_cio.states[TARGET]; + //First press will just show current target temp + int pushtime = 500; //how fast can we do this??******************* + int releasetime = 300; + _qButton(UP, TARGET, _commandQ[0][1], pushtime); + _qButton(NOBTN, TARGET, _commandQ[0][1], releasetime); + uint32_t updown; + diff<0 ? updown = DOWN : updown = UP; + for(int i = 0; i < abs(diff); i++) + { + _qButton(updown, CHAR1, 0xFF, pushtime); + _qButton(NOBTN, CHAR1, 0xFF, releasetime); + } + //Old method overshoots target too often: + //choose which direction to go (up or down) + // if(_cio.states[TARGET] > _commandQ[0][1]) _qButton(DOWN, TARGET, _commandQ[0][1], 10000); + // if(_cio.states[TARGET] < _commandQ[0][1]) _qButton(UP, TARGET, _commandQ[0][1], 10000); + break; + } case SETUNIT: _qButton(UNIT, UNITSTATE, _commandQ[0][1], 5000); + _qButton(NOBTN, CHAR3, 0xFF, 700); + _qButton(UP, CHAR3, _commandQ[0][1], 700); + _latestTarget = 0; break; case SETBUBBLES: _qButton(BUBBLES, BUBBLESSTATE, _commandQ[0][1], 5000); @@ -698,6 +720,7 @@ String BWC::getJSONStates() { // Set the values in the document doc["CONTENT"] = "STATES"; + doc["TIME"] = _timestamp; doc["LCK"] = _cio.states[LOCKEDSTATE]; doc["PWR"] = _cio.states[POWERSTATE]; doc["UNT"] = _cio.states[UNITSTATE]; diff --git a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.h b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.h index 1fe9d27f..3a8aee3c 100644 --- a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.h +++ b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266.h @@ -127,7 +127,7 @@ class BWC { uint8_t _dspBrightness; uint32_t _commandQ[MAXCOMMANDS][4]; int _qCommandLen = 0; //length of commandQ - uint32_t _buttonQ[MAXBUTTONS][4]; + int32_t _buttonQ[MAXBUTTONS][4]; int _qButtonLen = 0; //length of buttonQ uint32_t _timestamp; bool _newData = false; diff --git a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266_globals.h b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266_globals.h index 6c24caac..c12180c0 100644 --- a/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266_globals.h +++ b/Code/6-wire-version/lib/BWC54149E/BWC54149E_8266_globals.h @@ -1,13 +1,9 @@ //only declarations here please! Definitions belong in the cpp-file - +#include "model.h" #ifndef BWC54149E_8266_globals_H #define BWC54149E_8266_globals_H -//uncomment your model and comment out the rest -#define NO54149E - - //LSB const uint8_t DSP_DIM_BASE = 0x80; const uint8_t DSP_DIM_ON = 0x8; @@ -89,14 +85,18 @@ const String ButtonNames[] = { "HYDROJETS" //not existing }; -#ifdef NO54149E - +#ifdef MODEL54149E +const uint16_t ButtonCodes[] = +{ + 0, 1<<7, 1<<6, 1<<5, 1<<4, 1<<3, 1<<2, 1<<1, 1<<0, 1<<8, 1<<9 +}; +const bool HASJETS = false; +#else //keep compiler happy with this dummy const uint16_t ButtonCodes[] = { 0, 1<<7, 1<<6, 1<<5, 1<<4, 1<<3, 1<<2, 1<<1, 1<<0, 1<<8, 1<<9 }; const bool HASJETS = false; - #endif enum States: byte @@ -138,7 +138,7 @@ enum Commands: byte }; const int MAXCOMMANDS = 11; -const int MAXBUTTONS = 33; +const int MAXBUTTONS = 200; //direct port manipulation memory adresses. diff --git a/Code/6-wire-version/platformio.ini b/Code/6-wire-version/platformio.ini index 72ac4d23..23b918fb 100644 --- a/Code/6-wire-version/platformio.ini +++ b/Code/6-wire-version/platformio.ini @@ -25,14 +25,14 @@ lib_deps = board_build.filesystem = littlefs monitor_speed = 115200 ;Set upload speed to 115200 if you get transfer errors -upload_speed = 921600 +;upload_speed = 921600 ;board_build.f_cpu = 160000000L ; Uncomment the lines below by removing semicolons and edit IP for OTA upload. ; You have to upload via USB cable the first time. (upload_protocol = esptool) ; Make sure to use a data-USB cable. There is power only cables that wont work. upload_protocol = esptool -;upload_protocol = espota -;upload_port = 192.168.4.121 -;upload_flags = -; --auth=esp8266 +; upload_protocol = espota +; upload_port = 192.168.4.121 +; upload_flags = +; --auth=esp8266 diff --git a/Code/6-wire-version/src/config.h b/Code/6-wire-version/src/config.h index 31e0e44a..a9d92483 100644 --- a/Code/6-wire-version/src/config.h +++ b/Code/6-wire-version/src/config.h @@ -3,9 +3,6 @@ #define LEGACY_NAME "layzspa" -//uncomment this define if you have model Paris airjet 54149E -//#define MODEL54149E - /* * Miscellaneous */ @@ -56,8 +53,9 @@ const char *OTAPassword = "esp8266"; const bool enableWmApFallback = true; /** get the name for the WiFi configuration manager access point */ const char *wmApName = "Lay-Z-Spa Module"; -/** get the password for the WiFi configuration manager (min. 8, max. 63 chars) */ +/** get the password for the WiFi configuration manager (min. 8, max. 63 chars; NULL to disable) */ const char *wmApPassword = "layzspam0dule"; +//const char *wmApPassword = NULL; /* * WiFi Access Point @@ -111,3 +109,5 @@ String mqttPassword = "password"; String mqttClientId = LEGACY_NAME; /** get or set the MQTT topic name */ String mqttBaseTopic = LEGACY_NAME; +/** get or set the MQTT telemetry interval */ +int mqttTelemetryInterval = 600; diff --git a/Code/6-wire-version/src/main.cpp b/Code/6-wire-version/src/main.cpp index 6a2588da..d234dab1 100644 --- a/Code/6-wire-version/src/main.cpp +++ b/Code/6-wire-version/src/main.cpp @@ -28,9 +28,6 @@ void setup() // update webpage every 2 seconds. (will also be updated on state changes) updateWSTimer.attach(2.0, []{ sendWSFlag = true; }); - // update MQTT every 10 minutes. (will also be updated on state changes) - updateMqttTimer.attach(600, []{ sendMQTTFlag = true; }); - // needs to be loaded here for reading the wifi.json LittleFS.begin(); loadWifi(); @@ -40,7 +37,7 @@ void setup() startOTA(); startHttpServer(); startWebSocket(); - startMQTT(); + startMqtt(); bwc.print(WiFi.localIP().toString()); Serial.println(F("End of setup()")); @@ -99,9 +96,12 @@ void loop() // run once after connection was established if (!wifiConnected) { - Serial.println("WiFi > Connected"); + Serial.println(F("WiFi > Connected")); Serial.println(" SSID: \"" + WiFi.SSID() + "\""); Serial.println(" IP: \"" + WiFi.localIP().toString() + "\""); + startOTA(); + startHttpServer(); + startWebSocket(); } // reset marker wifiConnected = true; @@ -113,7 +113,7 @@ void loop() // run once after connection was lost if (wifiConnected) { - Serial.println("WiFi > Lost connection. Trying to reconnect ..."); + Serial.println(F("WiFi > Lost connection. Trying to reconnect ...")); } // set marker wifiConnected = false; @@ -140,10 +140,10 @@ void loop() DateTime.begin(); } - if (!mqttClient.loop()) + if (enableMqtt && !mqttClient.loop()) { - Serial.println(F("MQTT > Reconnecting")); - MQTT_Connect(); + Serial.println(F("MQTT > Not connected")); + mqttConnect(); } } } @@ -222,22 +222,22 @@ void sendMQTT() json = bwc.getJSONStates(); if (mqttClient.publish((String(mqttBaseTopic) + "/message").c_str(), String(json).c_str(), true)) { - //Serial.println(F("MQTT published")); + //Serial.println(F("MQTT > message published")); } else { - //Serial.println(F("MQTT not published")); + //Serial.println(F("MQTT > message not published")); } // send times json = bwc.getJSONTimes(); if (mqttClient.publish((String(mqttBaseTopic) + "/times").c_str(), String(json).c_str(), true)) { - //Serial.println(F("MQTT published")); + //Serial.println(F("MQTT > times published")); } else { - //Serial.println(F("MQTT not published")); + //Serial.println(F("MQTT > times not published")); } } @@ -266,7 +266,7 @@ void startWiFi() WiFi.begin(apSsid, apPwd); - Serial.print("WiFi > Trying to connect ..."); + Serial.print(F("WiFi > Trying to connect ...")); int maxTries = 10; int tryCount = 0; @@ -279,7 +279,7 @@ void startWiFi() if (tryCount >= maxTries) { Serial.println(""); - Serial.println("WiFi > NOT connected!"); + Serial.println(F("WiFi > NOT connected!")); if (enableWmApFallback) { // disable specific WiFi config @@ -307,13 +307,13 @@ void startWiFi() wifiConnected = true; - Serial.println("WiFi > Connected."); + Serial.println(F("WiFi > Connected.")); Serial.println(" SSID: \"" + WiFi.SSID() + "\""); Serial.println(" IP: \"" + WiFi.localIP().toString() + "\""); } else { - Serial.println("WiFi > Connection failed. Retrying in a while ..."); + Serial.println(F("WiFi > Connection failed. Retrying in a while ...")); } } @@ -322,14 +322,15 @@ void startWiFi() */ void startWiFiConfigPortal() { - Serial.println("WiFi > Using WiFiManager Config Portal"); + Serial.println(F("WiFi > Using WiFiManager Config Portal")); wm.autoConnect(wmApName, wmApPassword); - Serial.print("WiFi > Trying to connect ..."); + Serial.print(F("WiFi > Trying to connect ...")); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } + Serial.println(""); } @@ -354,17 +355,17 @@ void startOTA() ArduinoOTA.setPassword(OTAPassword); ArduinoOTA.onStart([]() { - Serial.println(F("OTA Start")); + Serial.println(F("OTA > Start")); bwc.stop(); }); ArduinoOTA.onEnd([]() { - Serial.println(F("\r\nOTA End")); + Serial.println(F("OTA > End")); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + Serial.printf("OTA > Progress: %u%%\r\n", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); + Serial.printf("OTA > Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println(F("Auth Failed")); else if (error == OTA_BEGIN_ERROR) Serial.println(F("Begin Failed")); else if (error == OTA_CONNECT_ERROR) Serial.println(F("Connect Failed")); @@ -382,6 +383,8 @@ void startOTA() */ void startWebSocket() { + // In case we are already running + webSocket.close(); webSocket.begin(); webSocket.onEvent(webSocketEvent); Serial.println(F("WebSocket > server started")); @@ -397,14 +400,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) { // if the websocket is disconnected case WStype_DISCONNECTED: - Serial.printf("WebSocket > [%u] Disconnected!\n", num); + Serial.printf("WebSocket > [%u] Disconnected!\r\n", num); break; // if a new websocket connection is established case WStype_CONNECTED: { IPAddress ip = webSocket.remoteIP(num); - Serial.printf("WebSocket > [%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + Serial.printf("WebSocket > [%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0], ip[1], ip[2], ip[3], payload); sendWS(); } break; @@ -412,7 +415,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) // if new text data is received case WStype_TEXT: { - Serial.printf("WebSocket > [%u] get Text: %s\n", num, payload); + Serial.printf("WebSocket > [%u] get Text: %s\r\n", num, payload); DynamicJsonDocument doc(256); DeserializationError error = deserializeJson(doc, payload); if (error) @@ -443,6 +446,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len) */ void startHttpServer() { + // In case we are already running + server.stop(); server.on(F("/getconfig/"), handleGetConfig); server.on(F("/setconfig/"), handleSetConfig); server.on(F("/getcommands/"), handleGetCommandQueue); @@ -873,6 +878,7 @@ void loadMqtt() mqttPassword = doc["mqttPassword"].as(); mqttClientId = doc["mqttClientId"].as(); mqttBaseTopic = doc["mqttBaseTopic"].as(); + mqttTelemetryInterval = doc["mqttTelemetryInterval"]; } /** @@ -899,6 +905,7 @@ void saveMqtt() doc["mqttPassword"] = mqttPassword; doc["mqttClientId"] = mqttClientId; doc["mqttBaseTopic"] = mqttBaseTopic; + doc["mqttTelemetryInterval"] = mqttTelemetryInterval; if (serializeJson(doc, file) == 0) { @@ -931,6 +938,7 @@ void handleGetMqtt() } doc["mqttClientId"] = mqttClientId; doc["mqttBaseTopic"] = mqttBaseTopic; + doc["mqttTelemetryInterval"] = mqttTelemetryInterval; String json; if (serializeJson(doc, json) == 0) @@ -968,11 +976,12 @@ void handleSetMqtt() mqttPassword = doc["mqttPassword"].as(); mqttClientId = doc["mqttClientId"].as(); mqttBaseTopic = doc["mqttBaseTopic"].as(); + mqttTelemetryInterval = doc["mqttTelemetryInterval"]; server.send(200, "text/plain", ""); saveMqtt(); - startMQTT(); + startMqtt(); } /** @@ -987,7 +996,7 @@ void handleDir() { Serial.println(root.fileName()); mydir += root.fileName() + F(" \t Size: "); - mydir += String(root.fileSize()) + F(" Bytes\n"); + mydir += String(root.fileSize()) + F(" Bytes\r\n"); } server.send(200, "text/plain", mydir); } @@ -1099,7 +1108,6 @@ void handleFileRemove() void handleRestart() { server.send(200, F("text/html"), F("ESP restart ...")); - Serial.println(F("ESP restart ...")); server.sendHeader("Location", "/"); server.send(303); @@ -1111,7 +1119,7 @@ void handleRestart() bwc.stop(); bwc.saveSettings(); - Serial.println("ESP restart ..."); + Serial.println(F("ESP restart ...")); ESP.restart(); } @@ -1121,7 +1129,7 @@ void handleRestart() * MQTT setup and connect * @author 877dev */ -void startMQTT() +void startMqtt() { // load mqtt credential file if it exists, and update default strings loadMqtt(); @@ -1133,27 +1141,24 @@ void startMQTT() // set buffer for larger messages, new to library 2.8.0 if (mqttClient.setBufferSize(1024)) { - Serial.println(F("MQTT buffer size successfully increased")); + Serial.println(F("MQTT > buffer size successfully increased")); } mqttClient.setKeepAlive(60); mqttClient.setSocketTimeout(30); // set callback details // this function is called automatically whenever a message arrives on a subscribed topic. - mqttClient.setCallback(MQTTcallback); + mqttClient.setCallback(mqttCallback); // Connect to MQTT broker, publish Status/MAC/count, and subscribe to keypad topic. - if (enableMqtt) - { - MQTT_Connect(); - } + mqttConnect(); } /** * MQTT callback function * @author 877dev */ -void MQTTcallback(char* topic, byte* payload, unsigned int length) +void mqttCallback(char* topic, byte* payload, unsigned int length) { - Serial.print(F("Message arrived [")); + Serial.print(F("MQTT > Message arrived [")); Serial.print(topic); Serial.print("] "); for (unsigned int i = 0; i < length; i++) @@ -1182,63 +1187,45 @@ void MQTTcallback(char* topic, byte* payload, unsigned int length) /** * Connect to MQTT broker, publish Status/MAC/count, and subscribe to keypad topic. */ -void MQTT_Connect() +void mqttConnect() { - Serial.print(F("Connecting to MQTT... ")); - // We'll connect with a Retained Last Will that updates the '.../Status' topic with "Dead" when the device goes offline... - // Attempt to connect... - /* - MQTT Connection syntax: - boolean connect (client_id, username, password, willTopic, willQoS, willRetain, willMessage) - Connects the client with a Will message, username and password specified. - Parameters - client_id : the client ID to use when connecting to the server. - username : the username to use. If NULL, no username or password is used (const char[]) - password : the password to use. If NULL, no password is used (const char[]) - willTopic : the topic to be used by the will message (const char[]) - willQoS : the quality of service to be used by the will message (int : 0,1 or 2) - willRetain : whether the will should be published with the retain flag (int : 0 or 1) - willMessage : the payload of the will message (const char[]) - Returns - false - connection failed. - true - connection succeeded - */ - if (mqttClient.connect(mqttClientId.c_str(), mqttUsername.c_str(), mqttPassword.c_str(), (String(mqttBaseTopic) + "/Status").c_str(), 0, 1, "Dead")) + // do not connect if MQTT is not enabled + if (!enableMqtt) { - // We get here if the connection was successful... + return; + } + + Serial.print(F("MQTT > Connecting ... ")); + // We'll connect with a Retained Last Will that updates the 'Status' topic with "Dead" when the device goes offline... + if (mqttClient.connect( + mqttClientId.c_str(), // client_id : the client ID to use when connecting to the server. + mqttUsername.c_str(), // username : the username to use. If NULL, no username or password is used (const char[]) + mqttPassword.c_str(), // password : the password to use. If NULL, no password is used (const char[]) + (String(mqttBaseTopic) + "/Status").c_str(), // willTopic : the topic to be used by the will message (const char[]) + 0, // willQoS : the quality of service to be used by the will message (int : 0,1 or 2) + 1, // willRetain : whether the will should be published with the retain flag (int : 0 or 1) + "Dead")) // willMessage : the payload of the will message (const char[]) + { + Serial.println(F("success!")); mqtt_connect_count++; - Serial.println(F("CONNECTED!")); - // Once connected, publish some announcements... + + // update MQTT every X seconds. (will also be updated on state changes) + updateMqttTimer.attach(mqttTelemetryInterval, []{ sendMQTTFlag = true; }); + // These all have the Retained flag set to true, so that the value is stored on the server and can be retrieved at any point - // Check the .../Status topic to see that the device is still online before relying on the data from these retained topics + // Check the 'Status' topic to see that the device is still online before relying on the data from these retained topics mqttClient.publish((String(mqttBaseTopic) + "/Status").c_str(), "Alive", true); mqttClient.publish((String(mqttBaseTopic) + "/MAC_Address").c_str(), WiFi.macAddress().c_str(), true); // Device MAC Address mqttClient.publish((String(mqttBaseTopic) + "/MQTT_Connect_Count").c_str(), String(mqtt_connect_count).c_str(), true); // MQTT Connect Count mqttClient.loop(); - // ... and then re/subscribe to the watched topics - mqttClient.subscribe((String(mqttBaseTopic) + "/command").c_str()); // Watch the .../command topic for incoming MQTT messages + // Watch the 'command' topic for incoming MQTT messages + mqttClient.subscribe((String(mqttBaseTopic) + "/command").c_str()); mqttClient.loop(); - // Add other watched topics in here... } else { - // We get here if the connection failed... - Serial.print(F("MQTT Connection FAILED, Return Code = ")); - Serial.println(mqttClient.state()); - Serial.println(); - /* - mqttClient.state return code meanings... - -4 : MQTT_CONNECTION_TIMEOUT - the server didn't respond within the keepalive time - -3 : MQTT_CONNECTION_LOST - the network connection was broken - -2 : MQTT_CONNECT_FAILED - the network connection failed - -1 : MQTT_DISCONNECTED - the client is disconnected cleanly - 0 : MQTT_CONNECTED - the client is connected - 1 : MQTT_CONNECT_BAD_PROTOCOL - the server doesn't support the requested version of MQTT - 2 : MQTT_CONNECT_BAD_CLIENT_ID - the server rejected the client identifier - 3 : MQTT_CONNECT_UNAVAILABLE - the server was unable to accept the connection - 4 : MQTT_CONNECT_BAD_CREDENTIALS - the username/password were rejected - 5 : MQTT_CONNECT_UNAUTHORIZED - the client was not authorized to connect * - */ + Serial.print(F("failed, Return Code = ")); + Serial.println(mqttClient.state()); // states explained in WebSocket.js } } diff --git a/Code/6-wire-version/src/main.h b/Code/6-wire-version/src/main.h index 0187be35..aafc2b40 100644 --- a/Code/6-wire-version/src/main.h +++ b/Code/6-wire-version/src/main.h @@ -1,6 +1,7 @@ #include #include #include "config.h" +#include "model.h" #ifdef MODEL54149E #include "BWC54149E_8266.h" @@ -107,6 +108,6 @@ void handleFileUpload(); void handleFileRemove(); void handleRestart(); -void startMQTT(); -void MQTTcallback(char* topic, byte* payload, unsigned int length); -void MQTT_Connect(); +void startMqtt(); +void mqttCallback(char* topic, byte* payload, unsigned int length); +void mqttConnect(); diff --git a/Code/Home assistant config/4-wire-error-sensor.yaml b/Code/Home assistant config/4-wire-error-sensor.yaml new file mode 100644 index 00000000..1f9905a2 --- /dev/null +++ b/Code/Home assistant config/4-wire-error-sensor.yaml @@ -0,0 +1,9 @@ +# Only for use with the 4-wire models to track pump ERROR. +- platform: mqtt + name: spa error + state_topic: "layzspa/message" + value_template: "{{ value_json.ERR }}" + expire_after: 700 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" diff --git a/Code/Home assistant config/4w-automation-error.yaml b/Code/Home assistant config/4w-automation-error.yaml new file mode 100644 index 00000000..bab0a8c8 --- /dev/null +++ b/Code/Home assistant config/4w-automation-error.yaml @@ -0,0 +1,13 @@ +- id: 'spa error notify' + alias: spa error warning + description: '' + trigger: + - platform: numeric_state + entity_id: sensor.spa_error + above: '0' + condition: [] + action: + - service: notify.notify + data: + message: Pump error {{ trigger.state }} + mode: single diff --git a/Code/Home assistant config/automations.yaml b/Code/Home assistant config/automations.yaml index d4622ea8..f33961f0 100644 --- a/Code/Home assistant config/automations.yaml +++ b/Code/Home assistant config/automations.yaml @@ -57,6 +57,6 @@ action: - service: mqtt.publish data: - topic: BW_2.0.0/command + topic: layzspa/command payload_template: '{CMD:0,VALUE:{{states(''input_number.spa_target_temp'')|int}},XTIME:0,INTERVAL:0}' mode: single diff --git a/Code/Home assistant config/binary-sensors.yaml b/Code/Home assistant config/binary-sensors.yaml deleted file mode 100644 index e058f468..00000000 --- a/Code/Home assistant config/binary-sensors.yaml +++ /dev/null @@ -1,61 +0,0 @@ -- platform: mqtt - name: spa lock - state_topic: "BW_2.0.0/message" - value_template: |- #"{{ value_json.LCK }}" - {% if value_json.LCK == 1 %} - ON - {% else %} - OFF - {% endif %} - expire_after: 700 - icon: mdi:lock - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - device_class: lock - -- platform: mqtt - name: spa heater element - state_topic: "BW_2.0.0/message" - value_template: |- #"{{ value_json.RED }}" - {% if value_json.RED == 1 %} - ON - {% else %} - OFF - {% endif %} - icon: mdi:fire - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - expire_after: 700 - -- platform: mqtt - name: spa ready - state_topic: "BW_2.0.0/message" - value_template: |- #"{{ value_json.RED }}" - {% if value_json.TMP > 30 %} - {% if value_json.TMP >= value_json.TGT-1 %} - ON - {% else %} - OFF - {% endif %} - - {% else %} - OFF - {% endif %} - icon: mdi:hot-tub - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - expire_after: 700 - -- platform: mqtt - name: Spa Status - device_class: connectivity - entity_category: diagnostic - state_topic: "BW_2.0.0/Status" - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - payload_on: "Alive" - payload_off: "Dead" diff --git a/Code/Home assistant config/buttons.yaml b/Code/Home assistant config/buttons.yaml deleted file mode 100644 index d1eec7af..00000000 --- a/Code/Home assistant config/buttons.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- platform: mqtt - name: spa reset CL timer - command_topic: "BW_2.0.0/command" - icon: mdi:restart - payload_press: '{CMD:9,VALUE:true,XTIME:0,INTERVAL:0}' - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa reset filter timer - command_topic: "BW_2.0.0/command" - icon: mdi:restart - payload_press: '{CMD:10,VALUE:true,XTIME:0,INTERVAL:0}' - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" diff --git a/Code/Home assistant config/layzspa.yaml b/Code/Home assistant config/layzspa.yaml new file mode 100644 index 00000000..f9314a3a --- /dev/null +++ b/Code/Home assistant config/layzspa.yaml @@ -0,0 +1,349 @@ +# Home Assistant package file for https://github.com/visualapproach/WiFi-remote-for-Bestway-Lay-Z-SPA +# Save in packages directory in your Home Assistant configuration directory. Create it if it doesn't already exist. +# Add the following to configuration.yaml: +#homeassistant: +# packages: !include_dir_named packages + + +## Sensors ## +sensor: + +# Displays the current button being pushed on the control panel. + - platform: mqtt + name: Spa Button + unique_id: sensor.spa_button + state_topic: "layzspa/button" + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Number of times MQTT has reconnected + - platform: mqtt + name: Spa Connect Count + unique_id: sensor.spa_connect_count + state_topic: "layzspa/MQTT_Connect_Count" + +# Spa temperature + - platform: mqtt + name: Spa Temperature + unique_id: sensor.spa_temperature + state_topic: "layzspa/message" + unit_of_measurement: '°C' + value_template: "{{ value_json.TMP }}" + expire_after: 700 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Spa target temperature + - platform: mqtt + name: Spa Target Temperature + unique_id: sensor.spa_target_temperature + unit_of_measurement: '°C' + state_topic: "layzspa/message" + value_template: "{{ value_json.TGT }}" + expire_after: 700 + +# Estimated time until target temperature is reached. Needs two+ temperature changes to make calculation + - platform: mqtt + name: Spa Time to Target + unique_id: sensor.spa_time_to_target + icon: mdi:clock + unit_of_measurement: 'hours' + state_topic: "layzspa/times" + value_template: "{{ (value_json.TTTT / 3600 | float) | round(2) }}" + expire_after: 700 + +# Estimated spa energy usage + - platform: mqtt + name: Spa Energy + unique_id: sensor.spa_energy + device_class: energy + state_class: total_increasing + unit_of_measurement: kWh + icon: mdi:flash + state_topic: "layzspa/times" + value_template: "{{ value_json.KWH }}" + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + expire_after: 700 + +# Time when Chlorine last was added to water + - platform: mqtt + name: Spa Cl Added + unique_id: sensor.spa_cl_added + unit_of_measurement: days + icon: hass:hand-coin-outline + state_topic: "layzspa/times" + value_template: "{{ ( ( (now().timestamp()|int) - value_json.CLTIME|int)/3600/24) | round(2) }}" + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + expire_after: 700 + +# Time when filter last was changed + - platform: mqtt + name: Spa Filter Changed + unique_id: sensor.spa_filter_changed + unit_of_measurement: days + icon: hass:air-filter + state_topic: "layzspa/times" + value_template: "{{ ( ( (now().timestamp()|int) - value_json.FTIME|int)/3600/24) | round(2) }}" + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + expire_after: 700 + +## Only for use with the 4-wire models to track pump ERROR. +# - platform: mqtt +# name: Spa Error +# unique_id: sensor.spa_error +# state_topic: "layzspa/message" +# value_template: "{{ value_json.ERR }}" +# expire_after: 700 +# availability_topic: "layzspa/Status" +# payload_available: "Alive" +# payload_not_available: "Dead" + + +## Binary sensors ## +binary_sensor: + +# Status of keypad lock + - platform: mqtt + name: Spa Lock + unique_id: binary_sensor.spa_lock + state_topic: "layzspa/message" + value_template: |- #"{{ value_json.LCK }}" + {% if value_json.LCK == 1 %} + ON + {% else %} + OFF + {% endif %} + expire_after: 700 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + device_class: lock + +# Status of heater (heating/not heating) + - platform: mqtt + name: Spa Heater Element + unique_id: binary_sensor.spa_heater_element + state_topic: "layzspa/message" + value_template: |- #"{{ value_json.RED }}" + {% if value_json.RED == 1 %} + ON + {% else %} + OFF + {% endif %} + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + expire_after: 700 + device_class: heat + +# On when temperature has reached a certain threshold + - platform: mqtt + name: Spa Ready + unique_id: binary_sensor.spa_ready + state_topic: "layzspa/message" + value_template: |- #"{{ value_json.RED }}" + {% if value_json.TMP > 30 %} + {% if value_json.TMP >= value_json.TGT-1 %} + ON + {% else %} + OFF + {% endif %} + + {% else %} + OFF + {% endif %} + icon: mdi:hot-tub + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + expire_after: 700 + +# Spa connection + - platform: mqtt + name: Spa Status + unique_id: binary_sensor.spa_status + device_class: connectivity + state_topic: "layzspa/Status" + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + payload_on: "Alive" + payload_off: "Dead" + +## 4 Wire Error State based on character display [E00] +# - platform: mqtt +# name: Spa Error +# unique_id: binary_sensor.spa_error +# device_class: problem +# state_topic: "layzspa/message" +# value_template: "{{ value_json.CH1 }}" +# availability_topic: "layzspa/Status" +# payload_available: "Alive" +# payload_not_available: "Dead" +# payload_on: 69 +# payload_off: 32 + + +## Switches ## +switch: + +# Switch for heater + - platform: mqtt + name: Spa Heat Regulation + unique_id: switch.spa_heat_regulation + command_topic: "layzspa/command" + state_topic: "layzspa/message" + value_template: |- + {% if value_json.RED == 1 %} + 1 + {% elif value_json.GRN == 1 %} + 1 + {% else %} + 0 + {% endif %} + icon: mdi:radiator + state_on: 1 + state_off: 0 + payload_on: '{CMD:3,VALUE:true,XTIME:0,INTERVAL:0}' + payload_off: '{CMD:3,VALUE:false,XTIME:0,INTERVAL:0}' + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Switch for bubbles + - platform: mqtt + name: Spa Airbubbles + unique_id: switch.spa_airbubbles + command_topic: "layzspa/command" + state_topic: "layzspa/message" + value_template: "{{ value_json.AIR }}" + icon: mdi:chart-bubble + payload_on: '{CMD:2,VALUE:true,XTIME:0,INTERVAL:0}' + payload_off: '{CMD:2,VALUE:false,XTIME:0,INTERVAL:0}' + state_on: 1 + state_off: 0 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Switch for pump + - platform: mqtt + name: Spa Pump + unique_id: switch.spa_pump + command_topic: "layzspa/command" + state_topic: "layzspa/message" + value_template: "{{ value_json.FLT }}" + icon: mdi:pump + payload_on: '{CMD:4,VALUE:true,XTIME:0,INTERVAL:0}' + payload_off: '{CMD:4,VALUE:false,XTIME:0,INTERVAL:0}' + state_on: 1 + state_off: 0 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Switch for changing temperature unit + - platform: mqtt + name: Spa Temperature Unit + unique_id: switch.spa_temperature_unit + command_topic: "layzspa/command" + state_topic: "layzspa/message" + value_template: "{{ value_json.UNT }}" + icon: mdi:temperature-celsius + payload_on: '{CMD:1,VALUE:true,XTIME:0,INTERVAL:0}' + payload_off: '{CMD:1,VALUE:false,XTIME:0,INTERVAL:0}' + state_on: 1 + state_off: 0 + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + + +## Buttons ## +button: + +# Command to reset chlorine timer + - platform: mqtt + name: Spa Reset Cl Timer + unique_id: button.spa_reset_cl_timer + command_topic: "layzspa/command" + icon: mdi:restart + payload_press: '{CMD:9,VALUE:true,XTIME:0,INTERVAL:0}' + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Command to reset filter timer + - platform: mqtt + name: Spa Reset Filter Timer + unique_id: button.spa_reset_filter_timer + command_topic: "layzspa/command" + icon: mdi:restart + payload_press: '{CMD:10,VALUE:true,XTIME:0,INTERVAL:0}' + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + +# Command to restart ESP + - platform: mqtt + name: Spa Restart ESP + unique_id: button.spa_restart_esp + device_class: restart + command_topic: "layzspa/command" + icon: mdi:restart + payload_press: '{CMD:6,VALUE:true,XTIME:0,INTERVAL:0}' + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" + + +## Climate control ## +climate: + - platform: mqtt + name: Spa + unique_id: climate.spa + max_temp: 40 + min_temp: 20 + precision: 1.0 + temperature_unit: "C" + modes: + - "off" + - "heat" + mode_state_topic: "layzspa/message" + mode_state_template: >- + {% if value_json.RED == 1 %} + heat + {% elif value_json.GRN == 1 %} + heat + {% else %} + off + {% endif %} + action_topic: "layzspa/message" + action_template: >- + {% if value_json.RED == 1 %} + heating + {% elif value_json.GRN == 1 %} + idle + {% else %} + off + {% endif %} + temperature_state_topic: "layzspa/message" + temperature_state_template: "{{ value_json.TGT }}" + current_temperature_topic: "layzspa/message" + current_temperature_template: "{{ value_json.TMP }}" + temperature_command_topic : "layzspa/command" + temperature_command_template: '{"CMD":0,"VALUE":{{ value|int }},"XTIME":0,"INTERVAL":0}' + power_command_topic: "layzspa/command" + payload_on: '{"CMD":3,"VALUE":1,"XTIME":0,"INTERVAL":0}' + payload_off: '{"CMD":3,"VALUE":0,"XTIME":0,"INTERVAL":0}' + availability_topic: "layzspa/Status" + payload_available: "Alive" + payload_not_available: "Dead" diff --git a/Code/Home assistant config/make these helpers.txt b/Code/Home assistant config/make these helpers.txt index 9a8944b9..f1e7879e 100644 --- a/Code/Home assistant config/make these helpers.txt +++ b/Code/Home assistant config/make these helpers.txt @@ -8,4 +8,5 @@ automation: !include automations.yaml scene: !include scenes.yaml sensor: !include sensors.yaml binary_sensor: !include binary-sensors.yaml -switch: !include switches.yaml \ No newline at end of file +switch: !include switches.yaml +button: !include buttons.yaml \ No newline at end of file diff --git a/Code/Home assistant config/readme.md b/Code/Home assistant config/readme.md new file mode 100644 index 00000000..f3e91f10 --- /dev/null +++ b/Code/Home assistant config/readme.md @@ -0,0 +1,13 @@ +Instructions: + +Add the following to you `configuration.yaml`: +``` +homeassistant: + packages: !include_dir_named packages +``` +Create `packages` directory in your Home Assistant configuration directory (where your `configuration.yaml`is located) and save `layzspa.yaml` inside. +Restart Home Assistant. + +If you make any changes in `layzspa.yaml` in the future, you can reload your changes without restarting by navigating to `Configuration -> Settings` and then click on `Manually configured MQTT entities`. + +There are two 4-wire error state sensors which must be uncommented in `layzspa.yaml` in order to use. diff --git a/Code/Home assistant config/sensors.yaml b/Code/Home assistant config/sensors.yaml deleted file mode 100644 index 0ae0bfe4..00000000 --- a/Code/Home assistant config/sensors.yaml +++ /dev/null @@ -1,69 +0,0 @@ -- platform: mqtt - name: spa button - state_topic: "BW_2.0.0/button" - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa connect count - state_topic: "BW_2.0.0/MQTT_Connect_Count" - -- platform: mqtt - name: spa temperature - state_topic: "BW_2.0.0/message" - unit_of_measurement: '°C' - value_template: "{{ value_json.TMP }}" - expire_after: 700 - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa target temperature - unit_of_measurement: '°C' - state_topic: "BW_2.0.0/message" - value_template: "{{ value_json.TGT }}" - expire_after: 700 - -- platform: mqtt - name: spa time to target - unit_of_measurement: 'hours' - state_topic: "BW_2.0.0/times" - value_template: "{{ (value_json.TTTT / 3600 | float) | round(2) }}" - expire_after: 700 - -- platform: mqtt - name: spa energy - device_class: energy - state_class: total_increasing - unit_of_measurement: kWh - icon: mdi:flash - state_topic: "BW_2.0.0/times" - value_template: "{{ value_json.KWH }}" - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - expire_after: 700 - -- platform: mqtt - name: spa cl added - unit_of_measurement: days - icon: hass:hand-coin-outline - state_topic: "BW_2.0.0/times" - value_template: "{{ ( ( (now().timestamp()|int) - value_json.CLTIME|int)/3600/24) | round(2) }}" - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - expire_after: 700 - -- platform: mqtt - name: spa filter changed - unit_of_measurement: days - icon: hass:air-filter - state_topic: "BW_2.0.0/times" - value_template: "{{ ( ( (now().timestamp()|int) - value_json.FTIME|int)/3600/24) | round(2) }}" - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - expire_after: 700 \ No newline at end of file diff --git a/Code/Home assistant config/switches.yaml b/Code/Home assistant config/switches.yaml deleted file mode 100644 index a5578556..00000000 --- a/Code/Home assistant config/switches.yaml +++ /dev/null @@ -1,62 +0,0 @@ -- platform: mqtt - name: spa heat regulation - command_topic: "BW_2.0.0/command" - state_topic: "BW_2.0.0/message" - value_template: |- - {% if value_json.RED == 1 %} - 1 - {% elif value_json.GRN == 1 %} - 1 - {% else %} - 0 - {% endif %} - icon: mdi:radiator - state_on: 1 - state_off: 0 - payload_on: '{CMD:3,VALUE:true,XTIME:0,INTERVAL:0}' - payload_off: '{CMD:3,VALUE:false,XTIME:0,INTERVAL:0}' - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa airbubbles - command_topic: "BW_2.0.0/command" - state_topic: "BW_2.0.0/message" - value_template: "{{ value_json.AIR }}" - icon: mdi:chart-bubble - payload_on: '{CMD:2,VALUE:true,XTIME:0,INTERVAL:0}' - payload_off: '{CMD:2,VALUE:false,XTIME:0,INTERVAL:0}' - state_on: 1 - state_off: 0 - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa pump - command_topic: "BW_2.0.0/command" - state_topic: "BW_2.0.0/message" - value_template: "{{ value_json.FLT }}" - icon: mdi:pump - payload_on: '{CMD:4,VALUE:true,XTIME:0,INTERVAL:0}' - payload_off: '{CMD:4,VALUE:false,XTIME:0,INTERVAL:0}' - state_on: 1 - state_off: 0 - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" - -- platform: mqtt - name: spa temperature unit - command_topic: "BW_2.0.0/command" - state_topic: "BW_2.0.0/message" - value_template: "{{ value_json.UNT }}" - icon: mdi:temperature-celsius - payload_on: '{CMD:1,VALUE:true,XTIME:0,INTERVAL:0}' - payload_off: '{CMD:1,VALUE:false,XTIME:0,INTERVAL:0}' - state_on: 1 - state_off: 0 - availability_topic: "BW_2.0.0/Status" - payload_available: "Alive" - payload_not_available: "Dead" \ No newline at end of file diff --git a/README.md b/README.md index adabace4..334353c1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ WiFi-remote-for-Bestway-Lay-Z-SPA ================================= - ESP8266 hack to use as WiFi remote control for Bestway Lay-Z-Spa Whirlpools (including 2021 year models) - [Features](#features)