diff --git a/Firmware/RTK_Everywhere/Developer.ino b/Firmware/RTK_Everywhere/Developer.ino index e2a60abba..e95a63362 100644 --- a/Firmware/RTK_Everywhere/Developer.ino +++ b/Firmware/RTK_Everywhere/Developer.ino @@ -107,6 +107,7 @@ bool wifiEspNowOn(const char * fileName, uint32_t lineNumber) {return false;} void wifiEspNowChannelSet(WIFI_CHANNEL_t channel) {} int wifiNetworkCount() {return 0;} void wifiResetTimeout() {} +void wifiSettingsClone() {} IPAddress wifiSoftApGetBroadcastIpAddress() {return IPAddress((uint32_t)0);} IPAddress wifiSoftApGetIpAddress() {return IPAddress((uint32_t)0);} const char * wifiSoftApGetSsid() {return "";} @@ -301,9 +302,14 @@ void webServerStop() {} void webServerUpdate() {} void webServerVerifyTables() {} bool wifiAfterCommand(int cmdIndex){return false;} -void wifiSettingsClone() {} bool webServerIsRunning() {return false;} +//---------------------------------------- +// Web Sockets +//---------------------------------------- + +bool webSocketsIsConnected() (return false;} + #endif // COMPILE_AP //====================================================================== diff --git a/Firmware/RTK_Everywhere/RTK_Everywhere.ino b/Firmware/RTK_Everywhere/RTK_Everywhere.ino index e72b087e3..06ed8e5ea 100644 --- a/Firmware/RTK_Everywhere/RTK_Everywhere.ino +++ b/Firmware/RTK_Everywhere/RTK_Everywhere.ino @@ -430,7 +430,7 @@ bool savePossibleSettings = true; // Save possible vs. available settings. See r //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #ifdef COMPILE_WIFI int packetRSSI; -RTK_WIFI wifi(false); // wifi(false); is non-verbose. For verbose, change to wifi(true); +RTK_WIFI wifi(false); // wifi(false); is non-verbose. For verbose, change to wifi(true); #endif // COMPILE_WIFI // WiFi Globals - For other module direct access @@ -708,7 +708,6 @@ char *incomingSettings; int incomingSettingsSpot; unsigned long timeSinceLastIncomingSetting; unsigned long lastDynamicDataUpdate; -bool websocketConnected = false; #ifdef COMPILE_WIFI #ifdef COMPILE_AP diff --git a/Firmware/RTK_Everywhere/States.ino b/Firmware/RTK_Everywhere/States.ino index 7e6da7c1d..dd7002da8 100644 --- a/Firmware/RTK_Everywhere/States.ino +++ b/Firmware/RTK_Everywhere/States.ino @@ -171,7 +171,7 @@ void stateUpdate() | Set fixedBase true | STATE_BASE_NOT_STARTED falls into | STATE_BASE_FIXED_NOT_STARTED - | + | V .-----------------------------------. startBase() | STATE_BASE_NOT_STARTED | @@ -489,7 +489,7 @@ void stateUpdate() // Confirm receipt so the web interface stops sending the config blob if (settings.debugWebServer == true) systemPrintln("Sending receipt confirmation of settings"); - sendStringToWebsocket("confirmDataReceipt,1,"); + webSocketsSendString("confirmDataReceipt,1,"); // Disallow new data to flow from websocket while we are parsing the current data currentlyParsingData = true; @@ -521,27 +521,19 @@ void stateUpdate() #ifdef COMPILE_WIFI #ifdef COMPILE_AP // Handle dynamic requests coming from web config page - if (websocketConnected == true) + if (webSocketsIsConnected() == true) { // Update the coordinates on the AP page if ((millis() - lastDynamicDataUpdate) > 1000) { lastDynamicDataUpdate = millis(); - createDynamicDataString(settingsCSV); - - sendStringToWebsocket(settingsCSV); + webSocketsSendSettings(); } // If a firmware version was requested, and obtained, report it back to the web page if (strlen(otaReportedVersion) > 0) { - createFirmwareVersionString(settingsCSV); - - if (settings.debugWebServer) - systemPrintf("WebServer: Firmware version requested. Sending: %s\r\n", settingsCSV); - - sendStringToWebsocket(settingsCSV); - + webSocketsSendFirmwareVersion(); otaReportedVersion[0] = '\0'; // Zero out the reported version } } diff --git a/Firmware/RTK_Everywhere/WebServer.ino b/Firmware/RTK_Everywhere/WebServer.ino index 5091f0149..9f09d3b13 100644 --- a/Firmware/RTK_Everywhere/WebServer.ino +++ b/Firmware/RTK_Everywhere/WebServer.ino @@ -55,16 +55,11 @@ static uint8_t webServerState; // Once connected to the access point for WiFi Config, the ESP32 sends current setting values in one long string to // websocket After user clicks 'save', data is validated via main.js and a long string of values is returned. -static httpd_handle_t wsserver; static WebServer *webServer; -// httpd_req_t *last_ws_req; -static int last_ws_fd; - static TaskHandle_t updateWebServerTaskHandle; static const uint8_t updateWebServerTaskPriority = 0; // 3 being the highest, and 0 being the lowest static const int webServerTaskStackSize = 1024 * 4; // Needs to be large enough to hold the file manager file list -static const int webSocketStackSize = 1024 * 20; // Needs to be large enough to hold the full settingsCSV // Inspired by: // https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/examples/MultiHomedServers/MultiHomedServers.ino @@ -78,104 +73,6 @@ static const int webSocketStackSize = 1024 * 20; // Needs to be large enoug // https://www.youtube.com/watch?v=15X0WvGaVg8 // https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/examples/WebServer/WebServer.ino -//---------------------------------------- -// Create a csv string with the dynamic data to update (current coordinates, battery level, etc) -//---------------------------------------- -void createDynamicDataString(char *settingsCSV) -{ - settingsCSV[0] = '\0'; // Erase current settings string - - // Current coordinates come from HPPOSLLH call back - stringRecord(settingsCSV, "geodeticLat", gnss->getLatitude(), haeNumberOfDecimals); - stringRecord(settingsCSV, "geodeticLon", gnss->getLongitude(), haeNumberOfDecimals); - stringRecord(settingsCSV, "geodeticAlt", gnss->getAltitude(), 3); - - double ecefX = 0; - double ecefY = 0; - double ecefZ = 0; - - geodeticToEcef(gnss->getLatitude(), gnss->getLongitude(), gnss->getAltitude(), &ecefX, &ecefY, &ecefZ); - - stringRecord(settingsCSV, "ecefX", ecefX, 3); - stringRecord(settingsCSV, "ecefY", ecefY, 3); - stringRecord(settingsCSV, "ecefZ", ecefZ, 3); - - if (online.batteryFuelGauge == false) // Product has no battery - { - stringRecord(settingsCSV, "batteryIconFileName", (char *)"src/BatteryBlank.png"); - stringRecord(settingsCSV, "batteryPercent", (char *)" "); - } - else - { - // Determine battery icon - int iconLevel = 0; - if (batteryLevelPercent < 25) - iconLevel = 0; - else if (batteryLevelPercent < 50) - iconLevel = 1; - else if (batteryLevelPercent < 75) - iconLevel = 2; - else // batt level > 75 - iconLevel = 3; - - char batteryIconFileName[sizeof("src/Battery2_Charging.png__")]; // sizeof() includes 1 for \0 termination - - if (isCharging()) - snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d_Charging.png", iconLevel); - else - snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d.png", iconLevel); - - stringRecord(settingsCSV, "batteryIconFileName", batteryIconFileName); - - // Limit batteryLevelPercent to sane levels - if (batteryLevelPercent > 100) - batteryLevelPercent = 100; - - // Determine battery percent - char batteryPercent[sizeof("+100%__")]; - if (isCharging()) - snprintf(batteryPercent, sizeof(batteryPercent), "+%d%%", batteryLevelPercent); - else - snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", batteryLevelPercent); - stringRecord(settingsCSV, "batteryPercent", batteryPercent); - } - - strcat(settingsCSV, "\0"); -} - -//---------------------------------------- -// Report back to the web config page with a CSV that contains the either CURRENT or -// the latest version as obtained by the OTA state machine -//---------------------------------------- -void createFirmwareVersionString(char *settingsCSV) -{ - char newVersionCSV[100]; - - settingsCSV[0] = '\0'; // Erase current settings string - - // Create a string of the unit's current firmware version - char currentVersion[21]; - firmwareVersionGet(currentVersion, sizeof(currentVersion), enableRCFirmware); - - // Compare the unit's version against the reported version from OTA - if (firmwareVersionIsReportedNewer(otaReportedVersion, currentVersion) == true) - { - if (settings.debugWebServer == true) - systemPrintln("New version detected"); - snprintf(newVersionCSV, sizeof(newVersionCSV), "%s,", otaReportedVersion); - } - else - { - if (settings.debugWebServer == true) - systemPrintln("No new firmware available"); - snprintf(newVersionCSV, sizeof(newVersionCSV), "CURRENT,"); - } - - stringRecord(settingsCSV, "newFirmwareVersion", newVersionCSV); - - strcat(settingsCSV, "\0"); -} - //---------------------------------------- // When called, responds with the messages supported on this platform // Message name and current rate are formatted in CSV, formatted to html by JS @@ -239,18 +136,6 @@ void getFileList(String &returnText) String fileSize; stringHumanReadableSize(fileSize, file.fileSize()); returnText += "fmName," + String(fileName) + ",fmSize," + fileSize + ","; - - const int maxFiles = 20; //40 is too much - const int fileNameLength = 50; - const int maxStringLength = maxFiles * fileNameLength; - // It is not uncommon to have SD cards with 100+ files on them. String can get huge. - // Here we arbitrarily limit it. - // This could be larger but, left unchecked, it will absolutely explode the stack. - if(returnText.length() > maxStringLength) - { - systemPrintf("Limiting file list to %d characters\r\n", maxStringLength); - break; - } } } @@ -376,7 +261,7 @@ static void handleFileManager() managerFileOpen = false; - sendStringToWebsocket("fmNext,1,"); // Tell browser to send next file if needed + webSocketsSendString("fmNext,1,"); // Tell browser to send next file if needed } dataAvailable -= sending; @@ -468,7 +353,7 @@ static void handleFirmwareFileUpload() // See issue #811 // The Update.write seems to upload the whole file in one go // This code never gets called... - + binBytesSent = upload.currentSize; // Send an update to browser every 100k @@ -484,7 +369,7 @@ static void handleFirmwareFileUpload() bytesSentMsg); // Convert to "firmwareUploadMsg,11214 bytes sent," systemPrintf("msg: %s\r\n", statusMsg); - sendStringToWebsocket(statusMsg); + webSocketsSendString(statusMsg); } } } @@ -499,7 +384,7 @@ static void handleFirmwareFileUpload() } else { - sendStringToWebsocket("firmwareUploadComplete,1,"); + webSocketsSendString("firmwareUploadComplete,1,"); systemPrintln("Firmware update complete. Restarting"); delay(500); ESP.restart(); @@ -715,45 +600,6 @@ bool parseIncomingSettings() return (true); } -//---------------------------------------- -// Send a string to the browser using the web socket -//---------------------------------------- -void sendStringToWebsocket(const char *stringToSend) -{ - if (!websocketConnected) - { - systemPrintf("sendStringToWebsocket: not connected - could not send: %s\r\n", stringToSend); - return; - } - - // To send content to the webServer, we would call: webServer->sendContent(stringToSend); - // But here we want to send content to the websocket (wsserver)... - - httpd_ws_frame_t ws_pkt; - memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); - ws_pkt.payload = (uint8_t *)stringToSend; - ws_pkt.len = strlen(stringToSend); - ws_pkt.type = HTTPD_WS_TYPE_TEXT; - - // If we use httpd_ws_send_frame, it requires a req. - // esp_err_t ret = httpd_ws_send_frame(last_ws_req, &ws_pkt); - // if (ret != ESP_OK) { - // ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret); - //} - - // If we use httpd_ws_send_frame_async, it requires a fd. - esp_err_t ret = httpd_ws_send_frame_async(wsserver, last_ws_fd, &ws_pkt); - if (ret != ESP_OK) - { - systemPrintf("httpd_ws_send_frame failed with %d\r\n", ret); - } - else - { - if (settings.debugWebServer == true) - systemPrintf("sendStringToWebsocket: %s\r\n", stringToSend); - } -} - //---------------------------------------- // Stop the web server //---------------------------------------- @@ -991,7 +837,7 @@ bool webServerAssignResources(int httpPort = 80) reportHeapNow(false); // Start the web socket server on port 81 using - if (websocketServerStart() == false) + if (webSocketsStart() == false) { if (settings.debugWebServer == true) systemPrintln("Web Sockets failed to start"); @@ -1052,7 +898,7 @@ void webServerReleaseResources() online.webServer = false; - webServerStopSockets(); // Release socket resources + webSocketsStop(); // Release socket resources if (webServer != nullptr) { @@ -1074,22 +920,6 @@ void webServerReleaseResources() } } -//---------------------------------------- -//---------------------------------------- -void webServerStopSockets() -{ - websocketConnected = false; - - if (wsserver != nullptr) - { - // Stop the httpd server - esp_err_t status = httpd_stop(wsserver); - if (status != ESP_OK) - systemPrintf("ERROR: wsserver failed to stop, status: %s!\r\n", esp_err_to_name(status)); - wsserver = nullptr; - } -} - //---------------------------------------- // Set the next webconfig state //---------------------------------------- @@ -1272,139 +1102,6 @@ void webServerVerifyTables() reportFatalError("Fix webServerStateNames to match WebServerState"); } -//---------------------------------------- -//---------------------------------------- -static esp_err_t ws_handler(httpd_req_t *req) -{ - // Log the req, so we can reuse it for httpd_ws_send_frame - // TODO: do we need to be cleverer about this? - // last_ws_req = req; - - if (req->method == HTTP_GET) - { - // Log the fd, so we can reuse it for httpd_ws_send_frame_async - // TODO: do we need to be cleverer about this? - last_ws_fd = httpd_req_to_sockfd(req); - - if (settings.debugWebServer == true) - systemPrintf("Handshake done, the new ws connection was opened with fd %d\r\n", last_ws_fd); - - websocketConnected = true; - lastDynamicDataUpdate = millis(); - sendStringToWebsocket(settingsCSV); - - return ESP_OK; - } - - httpd_ws_frame_t ws_pkt; - uint8_t *buf = NULL; - memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); - ws_pkt.type = HTTPD_WS_TYPE_TEXT; - /* Set max_len = 0 to get the frame len */ - esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); - if (ret != ESP_OK) - { - systemPrintf("httpd_ws_recv_frame failed to get frame len with %d\r\n", ret); - return ret; - } - if (settings.debugWebServer == true) - systemPrintf("frame len is %d\r\n", ws_pkt.len); - if (ws_pkt.len) - { - /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ - buf = (uint8_t *)rtkMalloc(ws_pkt.len + 1, "Payload buffer (buf)"); - if (buf == NULL) - { - systemPrintln("Failed to malloc memory for buf"); - return ESP_ERR_NO_MEM; - } - ws_pkt.payload = buf; - /* Set max_len = ws_pkt.len to get the frame payload */ - ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); - if (ret != ESP_OK) - { - systemPrintf("httpd_ws_recv_frame failed with %d\r\n", ret); - rtkFree(buf, "Payload buffer (buf)"); - return ret; - } - } - if (settings.debugWebServer == true) - { - const char *pktType; - size_t length = ws_pkt.len; - switch (ws_pkt.type) - { - default: - pktType = nullptr; - break; - case HTTPD_WS_TYPE_CONTINUE: - pktType = "HTTPD_WS_TYPE_CONTINUE"; - break; - case HTTPD_WS_TYPE_TEXT: - pktType = "HTTPD_WS_TYPE_TEXT"; - break; - case HTTPD_WS_TYPE_BINARY: - pktType = "HTTPD_WS_TYPE_BINARY"; - break; - case HTTPD_WS_TYPE_CLOSE: - pktType = "HTTPD_WS_TYPE_CLOSE"; - break; - case HTTPD_WS_TYPE_PING: - pktType = "HTTPD_WS_TYPE_PING"; - break; - case HTTPD_WS_TYPE_PONG: - pktType = "HTTPD_WS_TYPE_PONG"; - break; - } - systemPrintf("Packet: %p, %d bytes, type: %d%s%s%s\r\n", ws_pkt.payload, length, ws_pkt.type, - pktType ? " (" : "", pktType ? pktType : "", pktType ? ")" : ""); - if (length > 0x40) - length = 0x40; - dumpBuffer(ws_pkt.payload, length); - } - - if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) - { - if (currentlyParsingData == false) - { - for (int i = 0; i < ws_pkt.len; i++) - { - incomingSettings[incomingSettingsSpot++] = ws_pkt.payload[i]; - if (incomingSettingsSpot == AP_CONFIG_SETTING_SIZE) - systemPrintln("incomingSettings wrap-around. Increase AP_CONFIG_SETTING_SIZE"); - incomingSettingsSpot %= AP_CONFIG_SETTING_SIZE; - } - timeSinceLastIncomingSetting = millis(); - } - else - { - if (settings.debugWebServer == true) - systemPrintln("Ignoring packet due to parsing block"); - } - } - else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) - { - if (settings.debugWebServer == true) - systemPrintln("Client closed or refreshed the web page"); - - createSettingsString(settingsCSV); - websocketConnected = false; - } - - rtkFree(buf, "Payload buffer (buf)"); - return ret; -} - -//---------------------------------------- -//---------------------------------------- -static const httpd_uri_t ws = {.uri = "/ws", - .method = HTTP_GET, - .handler = ws_handler, - .user_ctx = NULL, - .is_websocket = true, - .handle_ws_control_frames = true, - .supported_subprotocol = NULL}; - //---------------------------------------- // Display the HTTPD configuration //---------------------------------------- @@ -1467,43 +1164,4 @@ void httpdDisplayConfig(struct httpd_config *config) systemPrintf("%p: uri_match_fn\r\n", (void *)config->uri_match_fn); } -//---------------------------------------- -//---------------------------------------- -bool websocketServerStart(void) -{ - esp_err_t status; - - // Gete the configuration object - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - - // Use different ports for websocket and webServer - use port 81 for the websocket - also defined in main.js - config.server_port = 81; - - // Increase the stack size from 4K to handle page processing (settingsCSV) - config.stack_size = webSocketStackSize; - - // Start the httpd server - if (settings.debugWebServer == true) - systemPrintf("Starting wsserver on port: %d\r\n", config.server_port); - - if (settings.debugWebServer == true) - { - httpdDisplayConfig(&config); - reportHeapNow(true); - } - status = httpd_start(&wsserver, &config); - if (status == ESP_OK) - { - // Registering the ws handler - if (settings.debugWebServer == true) - systemPrintln("Registering URI handlers"); - httpd_register_uri_handler(wsserver, &ws); - return true; - } - - // Display the failure to start - systemPrintf("ERROR: wsserver failed to start, status: %s!\r\n", esp_err_to_name(status)); - return false; -} - #endif // COMPILE_AP diff --git a/Firmware/RTK_Everywhere/WebSockets.ino b/Firmware/RTK_Everywhere/WebSockets.ino new file mode 100644 index 000000000..e186ea649 --- /dev/null +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -0,0 +1,492 @@ +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +WebSockets.ino + + Web socket support +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*/ + +#ifdef COMPILE_AP + +//---------------------------------------- +// Constants +//---------------------------------------- + +static const int webSocketsStackSize = 1024 * 20; // Needs to be large enough to hold the full settingsCSV + +//---------------------------------------- +// New types +//---------------------------------------- + +typedef struct _WEB_SOCKETS_CLIENT +{ + struct _WEB_SOCKETS_CLIENT * _flink; + struct _WEB_SOCKETS_CLIENT * _blink; + httpd_req_t * _request; + int _socketFD; +} WEB_SOCKETS_CLIENT; + +//---------------------------------------- +// Locals +//---------------------------------------- + +static WEB_SOCKETS_CLIENT * webSocketsClientListHead; +static WEB_SOCKETS_CLIENT * webSocketsClientListTail; +static httpd_handle_t webSocketsHandle; +static SemaphoreHandle_t webSocketsMutex; + +//---------------------------------------- +// Create a csv string with the dynamic data to update (current coordinates, +// battery level, etc) +//---------------------------------------- +void webSocketsCreateDynamicDataString(char *settingsCSV) +{ + settingsCSV[0] = '\0'; // Erase current settings string + + // Current coordinates come from HPPOSLLH call back + stringRecord(settingsCSV, "geodeticLat", gnss->getLatitude(), haeNumberOfDecimals); + stringRecord(settingsCSV, "geodeticLon", gnss->getLongitude(), haeNumberOfDecimals); + stringRecord(settingsCSV, "geodeticAlt", gnss->getAltitude(), 3); + + double ecefX = 0; + double ecefY = 0; + double ecefZ = 0; + + geodeticToEcef(gnss->getLatitude(), gnss->getLongitude(), gnss->getAltitude(), &ecefX, &ecefY, &ecefZ); + + stringRecord(settingsCSV, "ecefX", ecefX, 3); + stringRecord(settingsCSV, "ecefY", ecefY, 3); + stringRecord(settingsCSV, "ecefZ", ecefZ, 3); + + if (online.batteryFuelGauge == false) // Product has no battery + { + stringRecord(settingsCSV, "batteryIconFileName", (char *)"src/BatteryBlank.png"); + stringRecord(settingsCSV, "batteryPercent", (char *)" "); + } + else + { + // Determine battery icon + int iconLevel = 0; + if (batteryLevelPercent < 25) + iconLevel = 0; + else if (batteryLevelPercent < 50) + iconLevel = 1; + else if (batteryLevelPercent < 75) + iconLevel = 2; + else // batt level > 75 + iconLevel = 3; + + char batteryIconFileName[sizeof("src/Battery2_Charging.png__")]; // sizeof() includes 1 for \0 termination + + if (isCharging()) + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d_Charging.png", iconLevel); + else + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d.png", iconLevel); + + stringRecord(settingsCSV, "batteryIconFileName", batteryIconFileName); + + // Limit batteryLevelPercent to sane levels + if (batteryLevelPercent > 100) + batteryLevelPercent = 100; + + // Determine battery percent + char batteryPercent[sizeof("+100%__")]; + if (isCharging()) + snprintf(batteryPercent, sizeof(batteryPercent), "+%d%%", batteryLevelPercent); + else + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", batteryLevelPercent); + stringRecord(settingsCSV, "batteryPercent", batteryPercent); + } + + strcat(settingsCSV, "\0"); +} + +//---------------------------------------- +// Report back to the web config page with a CSV that contains the either CURRENT or +// the latest version as obtained by the OTA state machine +//---------------------------------------- +void webSocketsCreateFirmwareVersionString(char *settingsCSV) +{ + char newVersionCSV[100]; + + settingsCSV[0] = '\0'; // Erase current settings string + + // Create a string of the unit's current firmware version + char currentVersion[21]; + firmwareVersionGet(currentVersion, sizeof(currentVersion), enableRCFirmware); + + // Compare the unit's version against the reported version from OTA + if (firmwareVersionIsReportedNewer(otaReportedVersion, currentVersion) == true) + { + if (settings.debugWebServer == true) + systemPrintln("WebSockets: New firmware version detected"); + snprintf(newVersionCSV, sizeof(newVersionCSV), "%s,", otaReportedVersion); + } + else + { + if (settings.debugWebServer == true) + systemPrintln("No new firmware available"); + snprintf(newVersionCSV, sizeof(newVersionCSV), "CURRENT,"); + } + + stringRecord(settingsCSV, "newFirmwareVersion", newVersionCSV); + + strcat(settingsCSV, "\0"); +} + +//---------------------------------------- +// Handler for web sockets requests +//---------------------------------------- +static esp_err_t webSocketsHandler(httpd_req_t *req) +{ + WEB_SOCKETS_CLIENT * client; + WEB_SOCKETS_CLIENT * entry; + + // Log the req, so we can reuse it for httpd_ws_send_frame + // TODO: do we need to be cleverer about this? + // last_ws_req = req; + + if (req->method == HTTP_GET) + { + // Allocate a WEB_SOCKETS_CLIENT structure + client = (WEB_SOCKETS_CLIENT *)rtkMalloc(sizeof(WEB_SOCKETS_CLIENT), "WEB_SOCKETS_CLIENT"); + if (client == nullptr) + { + if (settings.debugWebServer == true) + systemPrintf("ERROR: Failed to allocate WEB_SOCKETS_CLIENT!\r\n"); + return ESP_FAIL; + } + + // Save the client context + client->_request = req; + client->_socketFD = httpd_req_to_sockfd(req); + + // Single thread access to the list of clients; + xSemaphoreTake(webSocketsMutex, portMAX_DELAY); + + // ListHead -> ... -> client (flink) -> nullptr; + // ListTail -> client (blink) -> ... -> nullptr; + // Add this client to the list + client->_flink = nullptr; + entry = webSocketsClientListTail; + client->_blink = entry; + if (entry) + entry->_flink = client; + else + webSocketsClientListHead = client; + webSocketsClientListTail = client; + + // Release the synchronization + xSemaphoreGive(webSocketsMutex); + + if (settings.debugWebServer == true) + systemPrintf("webSockets: Added client, _request: %p, _socketFD: %d\r\n", + client->_request, client->_socketFD); + + lastDynamicDataUpdate = millis(); + webSocketsSendString(settingsCSV); + + return ESP_OK; + } + + httpd_ws_frame_t ws_pkt; + uint8_t *buf = NULL; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + /* Set max_len = 0 to get the frame len */ + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) + { + systemPrintf("WebSockets: httpd_ws_recv_frame failed to get frame len with %d\r\n", ret); + return ret; + } + if (settings.debugWebServer == true) + systemPrintf("WebSockets: frame len is %d\r\n", ws_pkt.len); + if (ws_pkt.len) + { + /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ + buf = (uint8_t *)rtkMalloc(ws_pkt.len + 1, "Payload buffer (buf)"); + if (buf == NULL) + { + systemPrintln("WebSockets: Failed to malloc memory for buf"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + /* Set max_len = ws_pkt.len to get the frame payload */ + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) + { + systemPrintf("WebSockets: httpd_ws_recv_frame failed with %d\r\n", ret); + rtkFree(buf, "Payload buffer (buf)"); + return ret; + } + } + if (settings.debugWebServer == true) + { + const char *pktType; + size_t length = ws_pkt.len; + switch (ws_pkt.type) + { + default: + pktType = nullptr; + break; + case HTTPD_WS_TYPE_CONTINUE: + pktType = "HTTPD_WS_TYPE_CONTINUE"; + break; + case HTTPD_WS_TYPE_TEXT: + pktType = "HTTPD_WS_TYPE_TEXT"; + break; + case HTTPD_WS_TYPE_BINARY: + pktType = "HTTPD_WS_TYPE_BINARY"; + break; + case HTTPD_WS_TYPE_CLOSE: + pktType = "HTTPD_WS_TYPE_CLOSE"; + break; + case HTTPD_WS_TYPE_PING: + pktType = "HTTPD_WS_TYPE_PING"; + break; + case HTTPD_WS_TYPE_PONG: + pktType = "HTTPD_WS_TYPE_PONG"; + break; + } + systemPrintf("WebSockets: Packet: %p, %d bytes, type: %d%s%s%s\r\n", ws_pkt.payload, length, ws_pkt.type, + pktType ? " (" : "", pktType ? pktType : "", pktType ? ")" : ""); + if (length > 0x40) + length = 0x40; + dumpBuffer(ws_pkt.payload, length); + } + + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) + { + if (currentlyParsingData == false) + { + for (int i = 0; i < ws_pkt.len; i++) + { + incomingSettings[incomingSettingsSpot++] = ws_pkt.payload[i]; + if (incomingSettingsSpot == AP_CONFIG_SETTING_SIZE) + systemPrintln("WebSockets: incomingSettings wrap-around. Increase AP_CONFIG_SETTING_SIZE"); + incomingSettingsSpot %= AP_CONFIG_SETTING_SIZE; + } + timeSinceLastIncomingSetting = millis(); + } + else + { + if (settings.debugWebServer == true) + systemPrintln("WebSockets: Ignoring packet due to parsing block"); + } + } + else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) + { + if (settings.debugWebServer == true) + systemPrintln("WebSockets: Client closed or refreshed the web page"); + + createSettingsString(settingsCSV); + } + + rtkFree(buf, "Payload buffer (buf)"); + return ret; +} + +//---------------------------------------- +// Determine if webSockets is connected to a client +//---------------------------------------- +bool webSocketsIsConnected() +{ + return (webSocketsClientListHead != nullptr); +} + +//---------------------------------------- +// Send the formware version via web sockets +//---------------------------------------- +void webSocketsSendFirmwareVersion(void) +{ + webSocketsCreateFirmwareVersionString(settingsCSV); + + if (settings.debugWebServer) + systemPrintf("WebSockets: Firmware version requested. Sending: %s\r\n", settingsCSV); + + webSocketsSendString(settingsCSV); +} + +//---------------------------------------- +// Send the current settings via web sockets +//---------------------------------------- +void webSocketsSendSettings(void) +{ + webSocketsCreateDynamicDataString(settingsCSV); + webSocketsSendString(settingsCSV); +} + +//---------------------------------------- +// Send a string to the browser using the web socket +//---------------------------------------- +void webSocketsSendString(const char *stringToSend) +{ + WEB_SOCKETS_CLIENT * client; + + if (!webSocketsIsConnected()) + { + systemPrintf("webSocketsSendString: not connected - could not send: %s\r\n", stringToSend); + return; + } + + // Describe the packet to send + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = (uint8_t *)stringToSend; + ws_pkt.len = strlen(stringToSend); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + // Single thread access to the list of clients; + xSemaphoreTake(webSocketsMutex, portMAX_DELAY); + + // Send this message to each of the clients + client = webSocketsClientListHead; + while (client) + { + // Get the next client + WEB_SOCKETS_CLIENT * nextClient = client->_flink; + + // Send the string to to the client browser + esp_err_t ret = httpd_ws_send_frame_async(webSocketsHandle, + client->_socketFD, + &ws_pkt); + + // Check for message send failure + if (ret != ESP_OK) + { + systemPrintf("WebSockets: httpd_ws_send_frame failed with %d for client request: %x\r\n", + ret, client->_request); + + // Remove this client + WEB_SOCKETS_CLIENT * previousClient = client->_blink; + if (previousClient) + previousClient->_flink = nextClient; + else + webSocketsClientListHead = nextClient; + if (nextClient) + nextClient->_blink = previousClient; + else + webSocketsClientListTail = previousClient; + + // Done with this client + rtkFree(client, "WEB_SOCKETS_CLINET"); + } + + // Successfully sent the message + else if (settings.debugWebServer == true) + systemPrintf("webSocketsSendString: %s\r\n", stringToSend); + + // Get the next client + client = nextClient; + } + + // Release the synchronization + xSemaphoreGive(webSocketsMutex); +} + +//---------------------------------------- +// Web page description +//---------------------------------------- +static const httpd_uri_t webSocketsPage = {.uri = "/ws", + .method = HTTP_GET, + .handler = webSocketsHandler, + .user_ctx = NULL, + .is_websocket = true, + .handle_ws_control_frames = true, + .supported_subprotocol = NULL}; + +//---------------------------------------- +// Start the web sockets layer +//---------------------------------------- +bool webSocketsStart(void) +{ + esp_err_t status; + + // Get the configuration object + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Use different ports for websocket and webServer - use port 81 for the websocket - also defined in main.js + config.server_port = 81; + + // Increase the stack size from 4K to handle page processing (settingsCSV) + config.stack_size = webSocketsStackSize; + + // Start the httpd server + if (settings.debugWebServer == true) + systemPrintf("webSockets starting on port: %d\r\n", config.server_port); + + if (settings.debugWebServer == true) + { + httpdDisplayConfig(&config); + reportHeapNow(true); + } + + // Allocate the mutex + if (webSocketsMutex == nullptr) + { + webSocketsMutex = xSemaphoreCreateMutex(); + if (webSocketsMutex == nullptr) + { + if (settings.debugWebServer) + systemPrintf("ERROR: webSockets failed to allocate the mutex!\r\n"); + return false; + } + } + + status = httpd_start(&webSocketsHandle, &config); + if (status == ESP_OK) + { + // Registering the ws handler + if (settings.debugWebServer == true) + systemPrintln("webSockets registering URI handlers"); + httpd_register_uri_handler(webSocketsHandle, &webSocketsPage); + return true; + } + + // Display the failure to start + if (settings.debugWebServer) + systemPrintf("ERROR: webSockets failed to start, status: %s!\r\n", esp_err_to_name(status)); + return false; +} + +//---------------------------------------- +// Stop the web sockets layer +//---------------------------------------- +void webSocketsStop() +{ + WEB_SOCKETS_CLIENT * client; + + if (webSocketsHandle != nullptr) + { + // Single thread access to the list of clients; + xSemaphoreTake(webSocketsMutex, portMAX_DELAY); + + // ListHead -> ... -> client (flink) -> nullptr; + // ListTail -> client (blink) -> ... -> nullptr; + // Discard the clients + while (webSocketsClientListHead) + { + // Remove this client + client = webSocketsClientListHead; + webSocketsClientListHead = client->_flink; + + // Discard this client + rtkFree(client, "WEB_SOCKETS_CLIENT"); + } + webSocketsClientListTail = nullptr; + + // ListHead -> nullptr; + // ListTail -> nullptr; + // Release the synchronization + xSemaphoreGive(webSocketsMutex); + + // Stop the httpd server + esp_err_t status = httpd_stop(webSocketsHandle); + if (status == ESP_OK) + systemPrintf("webSockets stopped\r\n"); + else + systemPrintf("ERROR: webSockets failed to stop, status: %s!\r\n", esp_err_to_name(status)); + webSocketsHandle = nullptr; + } +} + +#endif // COMPILE_AP diff --git a/Firmware/RTK_Everywhere/menuCommands.ino b/Firmware/RTK_Everywhere/menuCommands.ino index f866f3f23..995d91148 100644 --- a/Firmware/RTK_Everywhere/menuCommands.ino +++ b/Firmware/RTK_Everywhere/menuCommands.ino @@ -1171,7 +1171,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting if (settings.debugWebServer == true) systemPrintln("Sending reset confirmation"); - sendStringToWebsocket((char *)"confirmReset,1,"); + webSocketsSendString((char *)"confirmReset,1,"); delay(500); // Allow for delivery systemPrintln("Reset after AP Config"); @@ -1191,17 +1191,20 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting loadSettings(); // Send new settings to browser. Re-use settingsCSV to avoid stack. - memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array + if (settingsCSV) + { + memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array - createSettingsString(settingsCSV); + createSettingsString(settingsCSV); - if (settings.debugWebServer == true) - { - systemPrintf("Sending profile %d\r\n", settingValue); - systemPrintf("Profile contents: %s\r\n", settingsCSV); - } + if (settings.debugWebServer == true) + { + systemPrintf("Sending profile %d\r\n", settingValue); + systemPrintf("Profile contents: %s\r\n", settingsCSV); + } - sendStringToWebsocket(settingsCSV); + webSocketsSendString(settingsCSV); + } knownSetting = true; } @@ -1229,17 +1232,20 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting activeProfiles = loadProfileNames(); // Send new settings to browser. Re-use settingsCSV to avoid stack. - memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array + if (settingsCSV) + { + memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array - createSettingsString(settingsCSV); + createSettingsString(settingsCSV); - if (settings.debugWebServer == true) - { - systemPrintf("Sending reset profile %d\r\n", settingValue); - systemPrintf("Profile contents: %s\r\n", settingsCSV); - } + if (settings.debugWebServer == true) + { + systemPrintf("Sending reset profile %d\r\n", settingValue); + systemPrintf("Profile contents: %s\r\n", settingsCSV); + } - sendStringToWebsocket(settingsCSV); + webSocketsSendString(settingsCSV); + } knownSetting = true; } @@ -1273,7 +1279,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting char newFileNameCSV[sizeof("logFileName,") + sizeof(logFileName) + 1]; snprintf(newFileNameCSV, sizeof(newFileNameCSV), "logFileName,%s,", logFileName); - sendStringToWebsocket(newFileNameCSV); // Tell the config page the name of the file we just created + webSocketsSendString(newFileNameCSV); // Tell the config page the name of the file we just created } knownSetting = true; } @@ -1282,7 +1288,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting if (settings.debugWebServer == true) systemPrintln("Checking for new OTA Pull firmware"); - sendStringToWebsocket((char *)"checkingNewFirmware,1,"); // Tell the config page we received their request + webSocketsSendString((char *)"checkingNewFirmware,1,"); // Tell the config page we received their request knownSetting = true; @@ -1294,7 +1300,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting if (settings.debugWebServer == true) systemPrintln("Getting new OTA Pull firmware"); - sendStringToWebsocket((char *)"gettingNewFirmware,1,"); + webSocketsSendString((char *)"gettingNewFirmware,1,"); // Let the OTA state machine know it needs to report its progress to the websocket apConfigFirmwareUpdateInProcess = true; diff --git a/Firmware/RTK_Everywhere/menuFirmware.ino b/Firmware/RTK_Everywhere/menuFirmware.ino index 5a8562e9c..0f48ae452 100644 --- a/Firmware/RTK_Everywhere/menuFirmware.ino +++ b/Firmware/RTK_Everywhere/menuFirmware.ino @@ -618,7 +618,7 @@ void otaDisplayPercentage(int bytesWritten, int totalLength, bool alwaysDisplay) { char myProgress[50]; snprintf(myProgress, sizeof(myProgress), "otaFirmwareStatus,%d,", percent); - sendStringToWebsocket(myProgress); + webSocketsSendString(myProgress); } previousPercent = percent; @@ -884,10 +884,10 @@ void otaUpdate() // is requesting the firmware update via those interfaces, thus we attempt an update // only once, stopping the state machine on failure - if (websocketConnected) + if (webSocketsIsConnected()) { // Report failed connection to web client - sendStringToWebsocket((char *)"newFirmwareVersion,NO_INTERNET,"); + webSocketsSendString((char *)"newFirmwareVersion,NO_INTERNET,"); otaUpdateStop(); } @@ -941,12 +941,12 @@ void otaUpdate() { otaRequestFirmwareVersionCheck = false; - if (websocketConnected) + if (webSocketsIsConnected()) { char newVersionCSV[40]; snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,%s,", otaReportedVersion); - sendStringToWebsocket(newVersionCSV); + webSocketsSendString(newVersionCSV); } if (bluetoothCommandIsConnected()) @@ -966,8 +966,8 @@ void otaUpdate() else { systemPrintln("Version Check: Firmware is up to date. No new firmware available."); - if (websocketConnected) - sendStringToWebsocket((char *)"newFirmwareVersion,CURRENT,"); + if (webSocketsIsConnected()) + webSocketsSendString((char *)"newFirmwareVersion,CURRENT,"); otaUpdateStop(); } @@ -976,8 +976,8 @@ void otaUpdate() { // Failed to get version number systemPrintln("Failed to get version number from server."); - if (websocketConnected) - sendStringToWebsocket((char *)"newFirmwareVersion,NO_SERVER,"); + if (webSocketsIsConnected()) + webSocketsSendString((char *)"newFirmwareVersion,NO_SERVER,"); // Report failure over the CLI if (bluetoothCommandIsConnected()) @@ -995,8 +995,8 @@ void otaUpdate() { otaUpdateStop(); - if (websocketConnected) - sendStringToWebsocket((char *)"gettingNewFirmware,ERROR,"); + if (webSocketsIsConnected()) + webSocketsSendString((char *)"gettingNewFirmware,ERROR,"); // Report failure over the CLI if (bluetoothCommandIsConnected()) @@ -1009,8 +1009,8 @@ void otaUpdate() otaUpdateFirmware(); // Update triggers ESP.restart(). If we get this far, the firmware update has failed - if (websocketConnected) - sendStringToWebsocket((char *)"gettingNewFirmware,ERROR,"); + if (webSocketsIsConnected()) + webSocketsSendString((char *)"gettingNewFirmware,ERROR,"); // Report failure over the CLI if (bluetoothCommandIsConnected()) @@ -1057,7 +1057,7 @@ void otaUpdateFirmware() if (apConfigFirmwareUpdateInProcess) // Tell AP page to display reset info - sendStringToWebsocket("confirmReset,1,"); + webSocketsSendString("confirmReset,1,"); ESP.restart(); } else if (response == ESP32OTAPull::NO_UPDATE_AVAILABLE)