diff --git a/Network.cpp b/Network.cpp index 5b9649c..937c6f7 100644 --- a/Network.cpp +++ b/Network.cpp @@ -47,17 +47,101 @@ bool Network::setup() { sockEmit.begin(); return true; } +conn_types_t Network::preferredConnType() { + switch(settings.connType) { + case conn_types_t::wifi: + case conn_types_t::unset: + case conn_types_t::ap: + return strlen(settings.WIFI.ssid) > 0 ? conn_types_t::wifi : conn_types_t::ap; + case conn_types_t::ethernetpref: + return strlen(settings.WIFI.ssid) > 0 && !ETH.linkUp() ? conn_types_t::wifi : conn_types_t::ethernet; + case conn_types_t::ethernet: + return ETH.linkUp() ? conn_types_t::ethernet : conn_types_t::ap; + default: + return settings.connType; + } +} void Network::loop() { - this->connect(); - if(!this->connected() || this->connecting()) return; + // ORDER OF OPERATIONS: + // ---------------------------------------------- + // 1. If we are in the middle of a connection process we need to simply bail after the connect method. The + // connect method will take care of our target connection for us. + // 2. Check to see what type of target connection we need. + // a. If this is an ethernet target then the connection needs to perform a fallback if applicable. + // b. If this is a wifi target then we need to first check to see if the SSID is available. + // c. If an SSID has not been set then we need to turn on the Soft AP. + // 3. If the Soft AP is open and the target is either wifi, ethernet, or ethernetpref then + // we need to shut it down if there are no connections and the preferred connection is available. + // a. Ethernet: Check for an active ethernet connection. We cannot rely on linkup because the PHY will + // report that the link is up when no IP address is being served. + // b. WiFi: Perform synchronous scan for APs related to the SSID. If the SSID can be found then perform + // the connection process for the WiFi connection. + // c. SoftAP: This condition retains the Soft AP because no other connection method is available. + + this->connect(); // Connection timeout handled in connect function as well as the opening of the Soft AP if needed. + if(this->connecting()) return; // If we are currently attempting to connect to something then we need to bail here. + conn_types_t ctype = this->preferredConnType(); + if(this->softAPOpened && WiFi.softAPgetStationNum() == 0) { + // If the Soft AP is opened and there are no clients connected then we need to scan for an AP. If + // our target exists we will exit out of the Soft AP and start that connection. + if(ctype == conn_types_t::wifi) { + // Scan for an AP. + if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { + _apScanning = true; + } + } + } + else if(this->connected() && ctype == conn_types_t::wifi && settings.WIFI.roaming) { + // Periodically look for an AP. + if(millis() > SSID_SCAN_INTERVAL + this->lastWifiScan) { + //Serial.println("Started scan for access points"); + if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { + _apScanning = true; + this->lastWifiScan = millis(); + } + } + } + if(_apScanning) { + if(!settings.WIFI.roaming || settings.connType != conn_types_t::wifi || (this->softAPOpened && WiFi.softAPgetStationNum() != 0)) _apScanning = false; + else { + uint16_t n = WiFi.scanComplete(); + if( n > 0) { + uint8_t bssid[6]; + int32_t channel = 0; + if(this->getStrongestAP(settings.WIFI.ssid, bssid, &channel)) { + if(!WiFi.BSSID() || memcmp(bssid, WiFi.BSSID(), sizeof(bssid)) != 0) { + Serial.printf("Found stronger AP %d %02X:%02X:%02X:%02X:%02X:%02X\n", channel, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + if(this->softAPOpened) { + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_STA); + } + this->changeAP(bssid, channel); + } + } + _apScanning = false; + } + } + } if(millis() - this->lastEmit > 1500) { + // Post our connection status if needed. this->lastEmit = millis(); if(this->connected()) { this->emitSockets(); this->lastEmit = millis(); } + esp_task_wdt_reset(); // Make sure we do not reboot here. } sockEmit.loop(); + mqtt.loop(); + if(settings.ssdpBroadcast) { + if(!SSDP.isStarted) SSDP.begin(); + if(SSDP.isStarted) SSDP.loop(); + } + else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end(); + +/* +// --------------------------- + if(this->softAPOpened) { // If the softAP has been opened check to see if there are any clients connected. If there is not // then we need to scan for the SSID. @@ -66,6 +150,7 @@ void Network::loop() { // then we will not start another scan. if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { _apScanning = true; + this->lastWifiScan = millis(); } } } @@ -79,10 +164,22 @@ void Network::loop() { // 1. If there is currently a waiting scan don't do anything if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { _apScanning = true; + this->lastWifiScan = millis(); } } this->lastMDNS = millis(); } + else { + //if(!this->connected() || this->connecting()) return; + if(millis() - this->lastEmit > 1500) { + this->lastEmit = millis(); + if(this->connected()) { + this->emitSockets(); + this->lastEmit = millis(); + } + } + sockEmit.loop(); + } if(_apScanning) { if(!settings.WIFI.roaming || settings.connType != conn_types_t::wifi || (this->softAPOpened && WiFi.softAPgetStationNum() != 0)) _apScanning = false; else { @@ -110,12 +207,13 @@ void Network::loop() { } else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end(); mqtt.loop(); + */ } bool Network::changeAP(const uint8_t *bssid, const int32_t channel) { esp_task_wdt_reset(); // Make sure we do not reboot here. if(SSDP.isStarted) SSDP.end(); mqtt.disconnect(); - sockEmit.end(); + //sockEmit.end(); WiFi.disconnect(false, true); WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid); this->connectStart = millis(); @@ -340,15 +438,15 @@ bool Network::connectWired() { if(ETH.linkUp()) { // If the ethernet link is re-established then we need to shut down wifi. if(WiFi.status() == WL_CONNECTED) { - sockEmit.end(); + //sockEmit.end(); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } if(this->connType != conn_types_t::ethernet) this->setConnected(conn_types_t::ethernet); - this->wifiFallback = false; return true; } else if(this->ethStarted) { + // There is no wired connection so we need to fallback if appropriate. if(settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0') return this->connectWiFi(); } @@ -362,34 +460,34 @@ bool Network::connectWired() { this->connTarget = conn_types_t::ethernet; this->connType = conn_types_t::unset; if(!this->ethStarted) { - this->ethStarted = true; - WiFi.mode(WIFI_OFF); - if(settings.hostname[0] != '\0') - ETH.setHostname(settings.hostname); - else - ETH.setHostname("ESPSomfy-RTS"); - - Serial.print("Set hostname to:"); - Serial.println(ETH.getHostname()); - if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) { - Serial.println("Ethernet Begin failed"); - this->ethStarted = false; - if(settings.connType == conn_types_t::ethernetpref) { - this->wifiFallback = true; - return connectWiFi(); - } - return false; + // Currently the ethernet module will leak memory if you call begin more than once. + this->ethStarted = true; + WiFi.mode(WIFI_OFF); + if(settings.hostname[0] != '\0') + ETH.setHostname(settings.hostname); + else + ETH.setHostname("ESPSomfy-RTS"); + Serial.print("Set hostname to:"); + Serial.println(ETH.getHostname()); + if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) { + Serial.println("Ethernet Begin failed"); + this->ethStarted = false; + if(settings.connType == conn_types_t::ethernetpref) { + this->wifiFallback = true; + return connectWiFi(); } - else { - if(!settings.IP.dhcp) { - if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) { - Serial.println("Unable to configure static IP address...."); - ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); - } + return false; + } + else { + if(!settings.IP.dhcp) { + if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) { + Serial.println("Unable to configure static IP address...."); + ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } - else - ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } + else + ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); + } } this->connectStart = millis(); return true; @@ -413,6 +511,8 @@ void Network::updateHostname() { } bool Network::connectWiFi() { if(this->softAPOpened && WiFi.softAPgetStationNum() > 0) { + // There is a client connected to the soft AP. We do not want to close out the connection. While both the + // Soft AP and a wifi connection can coexist on ESP32 the performance is abysmal. WiFi.disconnect(false); this->_connecting = false; this->connType = conn_types_t::unset; @@ -439,10 +539,8 @@ bool Network::connectWiFi() { Serial.println("dbm) "); } else Serial.println("Connecting to AP"); - // If the soft AP is currently opened then we do not want to kill it. WiFi.setSleep(false); WiFi.disconnect(false); - //WiFi.mode(WIFI_MODE_NULL); if(!settings.IP.dhcp) { if(!WiFi.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); @@ -454,7 +552,6 @@ bool Network::connectWiFi() { if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); Serial.print("Set hostname to:"); Serial.println(WiFi.getHostname()); - //WiFi.mode(WIFI_STA); WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); uint8_t bssid[6]; @@ -470,42 +567,55 @@ bool Network::connectWiFi() { return true; } bool Network::connect() { + esp_task_wdt_reset(); if(this->connecting()) { - // We are currently connecting and this flag is triggered while there is an attempt - // to connect to the network. If the connection type is set then we need to - // finish the connection. If it is not then we need to fall back to AP or in - // the case where the target was originally ethernet then we need to open the softAP. + // CHECK FOR CONNECTION TIMEOUT + // ------------------------------------- + // We are currently connecting and this flag is triggered while there is an attempt to connect to the network. + // If the connection type is set then we need to finish the connection. If it is not then we need to fall back to AP or in + // the case where the target was originally ethernetpref then we need to open the Soft AP. if(this->connType == conn_types_t::unset) { - // If we reached our timeout for the connection then we need to open the soft ap. + // If we reached our timeout for the connection then we need to fall back to wifi or open the Soft Ap. if(millis() > this->connectStart + CONNECT_TIMEOUT) { - esp_task_wdt_reset(); - if(this->connTarget == conn_types_t::ethernet && settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0') + this->_connecting = false; + if(this->connTarget == conn_types_t::ethernet && + settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0') // We timed out with the Wired connection. this->connectWiFi(); else if(this->softAPOpened) { + // Our connection has timed out and the Soft AP is already opened. We are simply going to keep trying + // from the beginning until a connection can be made. if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) this->connectWired(); else if(settings.connType == conn_types_t::wifi && strlen(settings.WIFI.ssid) > 0) this->connectWiFi(); } else { - //Serial.println("Fell into timeout"); + // We have exhausted all attempts to connect. Fall back to the Soft AP this->openSoftAP(); } } } else + // A connection has been established and we need to now set up the rest of our connectivity. this->setConnected(this->connTarget); } + else if(this->softAPOpened) { + // If the Soft AP is currently open then we will let the passive scanning or Ethernet link layer + // do its thing to connect or reconnect. + return false; + } else if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) this->connectWired(); else if(settings.connType == conn_types_t::wifi && strlen(settings.WIFI.ssid) > 0) this->connectWiFi(); else + // We do not currently have a connection method set. this->openSoftAP(); if(this->softAPOpened && this->connected() && WiFi.softAPgetStationNum() == 0) { - Serial.println("Closing uneeded SoftAP"); - WiFi.softAPdisconnect(true); - if(this->connType == conn_types_t::wifi) WiFi.mode(WIFI_STA); + // We have a connnection and the AP is still open. Kill it. + Serial.println("Closing uneeded SoftAP"); + WiFi.softAPdisconnect(true); + if(this->connType == conn_types_t::wifi) WiFi.mode(WIFI_STA); } return true; } @@ -517,7 +627,6 @@ uint32_t Network::getChipId() { } return chipId; } - bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) { // The new AP must be at least 10dbm greater. int32_t strength = this->connected() ? WiFi.RSSI() + 10 : -127; @@ -558,13 +667,14 @@ bool Network::connected() { else return this->connType != conn_types_t::unset; return false; } -bool Network::connecting() { - return this->_connecting; -} +bool Network::connecting() { return this->_connecting; } void Network::networkEvent(WiFiEvent_t event) { switch(event) { case ARDUINO_EVENT_WIFI_READY: Serial.println("(evt) WiFi interface ready"); break; - case ARDUINO_EVENT_WIFI_SCAN_DONE: Serial.println("(evt) Completed scan for access points"); break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + Serial.println("(evt) Completed scan for access points"); + net.lastWifiScan = millis(); + break; case ARDUINO_EVENT_WIFI_STA_START: Serial.println("WiFi station mode started"); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); @@ -587,6 +697,13 @@ void Network::networkEvent(WiFiEvent_t event) { Serial.println(ETH.localIP()); net.connectTime = millis(); net.connType = conn_types_t::ethernet; + if(settings.IP.dhcp) { + settings.IP.ip = ETH.localIP(); + settings.IP.subnet = ETH.subnetMask(); + settings.IP.gateway = ETH.gatewayIP(); + settings.IP.dns1 = ETH.dnsIP(0); + settings.IP.dns2 = ETH.dnsIP(1); + } break; case ARDUINO_EVENT_ETH_CONNECTED: Serial.print("(evt) Ethernet Connected "); @@ -611,7 +728,7 @@ void Network::networkEvent(WiFiEvent_t event) { net.softAPOpened = true; break; case ARDUINO_EVENT_WIFI_AP_STOP: - Serial.println("(evt) WiFi SoftAP Stopped"); + if(!net.openingSoftAP) Serial.println("(evt) WiFi SoftAP Stopped"); //if(net.softAPOpened) net.openingSoftAP = false; net.softAPOpened = false; break; diff --git a/Network.h b/Network.h index 9f4e443..5345ce3 100644 --- a/Network.h +++ b/Network.h @@ -6,6 +6,7 @@ //enum class conn_types_t : byte; #define CONNECT_TIMEOUT 20000 +#define SSID_SCAN_INTERVAL 60000 class Network { protected: unsigned long lastEmit = 0; @@ -15,6 +16,7 @@ class Network { int linkSpeed = 0; bool _connecting = false; public: + unsigned long lastWifiScan = 0; bool ethStarted = false; bool wifiFallback = false; bool softAPOpened = false; @@ -24,6 +26,7 @@ class Network { conn_types_t connTarget = conn_types_t::unset; bool connected(); bool connecting(); + conn_types_t preferredConnType(); String ssid; String mac; int channel; diff --git a/Sockets.cpp b/Sockets.cpp index 6d098a8..51765aa 100644 --- a/Sockets.cpp +++ b/Sockets.cpp @@ -43,6 +43,9 @@ bool room_t::leave(uint8_t num) { } return true; } +void room_t::clear() { + memset(this->clients, 255, sizeof(this->clients)); +} uint8_t room_t::activeClients() { uint8_t n = 0; for(uint8_t i = 0; i < sizeof(this->clients); i++) { @@ -77,7 +80,7 @@ void SocketEmitter::begin() { sockServer.enableHeartbeat(20000, 10000, 3); sockServer.onEvent(this->wsEvent); Serial.println("Socket Server Started..."); - settings.printAvailHeap(); + //settings.printAvailHeap(); } void SocketEmitter::loop() { this->initClients(); @@ -126,7 +129,11 @@ void SocketEmitter::delayInit(uint8_t num) { } } } -void SocketEmitter::end() { sockServer.close(); } +void SocketEmitter::end() { + sockServer.close(); + for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) + this->rooms[i].clear(); +} void SocketEmitter::disconnect() { sockServer.disconnect(); } void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { switch(type) { diff --git a/Sockets.h b/Sockets.h index f6acf46..d76ab93 100644 --- a/Sockets.h +++ b/Sockets.h @@ -12,6 +12,7 @@ struct room_t { bool isJoined(uint8_t num); bool join(uint8_t num); bool leave(uint8_t num); + void clear(); }; class SocketEmitter { protected: diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin index 8a509b1..991e6cc 100644 Binary files a/SomfyController.ino.esp32.bin and b/SomfyController.ino.esp32.bin differ diff --git a/SomfyController.ino.esp32s3.bin b/SomfyController.ino.esp32s3.bin index 9780134..0145167 100644 Binary files a/SomfyController.ino.esp32s3.bin and b/SomfyController.ino.esp32s3.bin differ diff --git a/SomfyController.littlefs.bin b/SomfyController.littlefs.bin index cb3dbe5..b3b44cc 100644 Binary files a/SomfyController.littlefs.bin and b/SomfyController.littlefs.bin differ diff --git a/WResp.cpp b/WResp.cpp index 3b158ca..e44e3ba 100644 --- a/WResp.cpp +++ b/WResp.cpp @@ -21,7 +21,7 @@ void JsonSockEvent::endEvent(uint8_t num) { else this->server->sendTXT(num, this->buff); } void JsonSockEvent::_safecat(const char *val, bool escape) { - size_t len = strlen(val) + strlen(this->buff); + size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { Serial.printf("Socket exceeded buffer size %d - %d\n", this->buffSize, len); @@ -29,7 +29,8 @@ void JsonSockEvent::_safecat(const char *val, bool escape) { return; } if(escape) strcat(this->buff, "\""); - strcat(this->buff, val); + if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); + else strcat(this->buff, val); if(escape) strcat(this->buff, "\""); } void JsonResponse::beginResponse(WebServer *server, char *buff, size_t buffSize) { @@ -52,13 +53,14 @@ void JsonResponse::send() { this->_headersSent = true; } void JsonResponse::_safecat(const char *val, bool escape) { - size_t len = strlen(val) + strlen(this->buff); + size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { this->send(); } if(escape) strcat(this->buff, "\""); - strcat(this->buff, val); + if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); + else strcat(this->buff, val); if(escape) strcat(this->buff, "\""); } @@ -133,13 +135,70 @@ void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_nu void JsonFormatter::addElem(const char *name, bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(name); } void JsonFormatter::_safecat(const char *val, bool escape) { - size_t len = strlen(val) + strlen(this->buff); + size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { return; } if(escape) strcat(this->buff, "\""); - strcat(this->buff, val); + if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); + else strcat(this->buff, val); if(escape) strcat(this->buff, "\""); } void JsonFormatter::_appendNumber(const char *name) { this->appendElem(name); this->_safecat(this->_numbuff); } +uint32_t JsonFormatter::calcEscapedLength(const char *raw) { + uint32_t len = 0; + for(size_t i = strlen(raw); i > 0; i--) { + switch(raw[i]) { + case '"': + case '/': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\\': + len += 2; + break; + default: + len++; + break; + } + } + return len; +} +void JsonFormatter::escapeString(const char *raw, char *escaped) { + for(uint32_t i = 0; i < strlen(raw); i++) { + switch(raw[i]) { + case '"': + strcat(escaped, "\\\""); + break; + case '/': + strcat(escaped, "\\/"); + break; + case '\b': + strcat(escaped, "\\b"); + break; + case '\f': + strcat(escaped, "\\f"); + break; + case '\n': + strcat(escaped, "\\n"); + break; + case '\r': + strcat(escaped, "\\r"); + break; + case '\t': + strcat(escaped, "\\t"); + break; + case '\\': + strcat(escaped, "\\\\"); + break; + default: + size_t len = strlen(escaped); + escaped[len] = raw[i]; + escaped[len+1] = 0x00; + break; + } + } +} diff --git a/WResp.h b/WResp.h index f1a7f2c..4bda5d5 100644 --- a/WResp.h +++ b/WResp.h @@ -16,6 +16,8 @@ class JsonFormatter { virtual void _safecat(const char *val, bool escape = false); void _appendNumber(const char *name); public: + void escapeString(const char *raw, char *escaped); + uint32_t calcEscapedLength(const char *raw); void beginObject(const char *name = nullptr); void endObject(); void beginArray(const char *name = nullptr); diff --git a/data/index.js b/data/index.js index 99b1100..f66dbe0 100644 --- a/data/index.js +++ b/data/index.js @@ -3691,12 +3691,36 @@ class Somfy { html += '
  • If the shade does not jog, press the prog button again until the shade jogs.
  • '; html += ''; html += `
    `; - html += ``; + html += ``; html += ``; html += ``; html += `
    `; div.innerHTML = html; + let fnRepeatProg = (err, shade) => { + if (this.btnTimer) { + clearTimeout(this.btnTimer); + this.btnTimer = null; + } + if (err) return; + if (mouseDown) { + somfy.sendCommandRepeat(shadeId, 'prog', null, fnRepeatProg); + } + } document.getElementById('somfyShade').appendChild(div); + let btn = document.getElementById('btnSendUnpairing'); + btn.addEventListener('mousedown', (event) => { + console.log(this); + console.log(event); + console.log('mousedown'); + somfy.sendCommand(shadeId, 'prog', null, (err, shade) => { fnRepeatProg(err, shade); }); + }, true); + btn.addEventListener('touchstart', (event) => { + console.log(this); + console.log(event); + console.log('touchstart'); + somfy.sendCommand(shadeId, 'prog', null, (err, shade) => { fnRepeatProg(err, shade); }); + }, true); + return div; } sendCommand(shadeId, command, repeat, cb) {