From d30f29520ee4e3d820a441d67c362182a96e1b2f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 10:07:31 -1000 Subject: [PATCH 1/7] Fix build with COMPILE_AP commented out --- Firmware/RTK_Everywhere/Developer.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/RTK_Everywhere/Developer.ino b/Firmware/RTK_Everywhere/Developer.ino index e2a60abba..c286b7fae 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,7 +302,6 @@ void webServerStop() {} void webServerUpdate() {} void webServerVerifyTables() {} bool wifiAfterCommand(int cmdIndex){return false;} -void wifiSettingsClone() {} bool webServerIsRunning() {return false;} #endif // COMPILE_AP From d9a487dae7efe8fda52f6996b8a7f888e2622199 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 2 Dec 2025 10:51:47 -1000 Subject: [PATCH 2/7] WebSockets: Split out webSockets from webServer --- Firmware/RTK_Everywhere/States.ino | 16 +- Firmware/RTK_Everywhere/WebServer.ino | 342 +-------------------- Firmware/RTK_Everywhere/WebSockets.ino | 375 +++++++++++++++++++++++ Firmware/RTK_Everywhere/menuCommands.ino | 12 +- Firmware/RTK_Everywhere/menuFirmware.ino | 16 +- 5 files changed, 399 insertions(+), 362 deletions(-) create mode 100644 Firmware/RTK_Everywhere/WebSockets.ino diff --git a/Firmware/RTK_Everywhere/States.ino b/Firmware/RTK_Everywhere/States.ino index 7e6da7c1d..006cf3188 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; @@ -527,21 +527,13 @@ void stateUpdate() 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..7c1511810 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 @@ -376,7 +273,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 +365,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 +381,7 @@ static void handleFirmwareFileUpload() bytesSentMsg); // Convert to "firmwareUploadMsg,11214 bytes sent," systemPrintf("msg: %s\r\n", statusMsg); - sendStringToWebsocket(statusMsg); + webSocketsSendString(statusMsg); } } } @@ -499,7 +396,7 @@ static void handleFirmwareFileUpload() } else { - sendStringToWebsocket("firmwareUploadComplete,1,"); + webSocketsSendString("firmwareUploadComplete,1,"); systemPrintln("Firmware update complete. Restarting"); delay(500); ESP.restart(); @@ -715,45 +612,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 +849,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 +910,7 @@ void webServerReleaseResources() online.webServer = false; - webServerStopSockets(); // Release socket resources + webSocketsStop(); // Release socket resources if (webServer != nullptr) { @@ -1074,22 +932,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 +1114,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 +1176,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..ed7e24ed2 --- /dev/null +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -0,0 +1,375 @@ +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +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 + +//---------------------------------------- +// Locals +//---------------------------------------- + +static int last_ws_fd; +// httpd_req_t *last_ws_req; +static httpd_handle_t webSocketsHandle; + +//---------------------------------------- +// 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("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"); +} + +//---------------------------------------- +// Handler for web sockets requests +//---------------------------------------- +static esp_err_t webSocketsHandler(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(); + 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("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; +} + +//---------------------------------------- +// Send the formware version via web sockets +//---------------------------------------- +void webSocketsSendFirmwareVersion(void) +{ + webSocketsCreateFirmwareVersionString(settingsCSV); + + if (settings.debugWebServer) + systemPrintf("WebServer: 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) +{ + if (!websocketConnected) + { + systemPrintf("webSocketsSendString: 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 (webSocketsHandle)... + + 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(webSocketsHandle, 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("webSocketsSendString: %s\r\n", stringToSend); + } +} + +//---------------------------------------- +// 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("Starting webSockets on port: %d\r\n", config.server_port); + + if (settings.debugWebServer == true) + { + httpdDisplayConfig(&config); + reportHeapNow(true); + } + status = httpd_start(&webSocketsHandle, &config); + if (status == ESP_OK) + { + // Registering the ws handler + if (settings.debugWebServer == true) + systemPrintln("Registering URI handlers"); + httpd_register_uri_handler(webSocketsHandle, &webSocketsPage); + return true; + } + + // Display the failure to start + systemPrintf("ERROR: webSockets failed to start, status: %s!\r\n", esp_err_to_name(status)); + return false; +} + +//---------------------------------------- +// Stop the web sockets layer +//---------------------------------------- +void webSocketsStop() +{ + websocketConnected = false; + + if (webSocketsHandle != nullptr) + { + // Stop the httpd server + esp_err_t status = httpd_stop(webSocketsHandle); + if (status != ESP_OK) + 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..029f4ae41 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"); @@ -1201,7 +1201,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting systemPrintf("Profile contents: %s\r\n", settingsCSV); } - sendStringToWebsocket(settingsCSV); + webSocketsSendString(settingsCSV); knownSetting = true; } @@ -1239,7 +1239,7 @@ SettingValueResponse updateSettingWithValue(bool inCommands, const char *setting systemPrintf("Profile contents: %s\r\n", settingsCSV); } - sendStringToWebsocket(settingsCSV); + webSocketsSendString(settingsCSV); knownSetting = true; } @@ -1273,7 +1273,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 +1282,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 +1294,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..e24246d27 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; @@ -887,7 +887,7 @@ void otaUpdate() if (websocketConnected) { // Report failed connection to web client - sendStringToWebsocket((char *)"newFirmwareVersion,NO_INTERNET,"); + webSocketsSendString((char *)"newFirmwareVersion,NO_INTERNET,"); otaUpdateStop(); } @@ -946,7 +946,7 @@ void otaUpdate() char newVersionCSV[40]; snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,%s,", otaReportedVersion); - sendStringToWebsocket(newVersionCSV); + webSocketsSendString(newVersionCSV); } if (bluetoothCommandIsConnected()) @@ -967,7 +967,7 @@ void otaUpdate() { systemPrintln("Version Check: Firmware is up to date. No new firmware available."); if (websocketConnected) - sendStringToWebsocket((char *)"newFirmwareVersion,CURRENT,"); + webSocketsSendString((char *)"newFirmwareVersion,CURRENT,"); otaUpdateStop(); } @@ -977,7 +977,7 @@ void otaUpdate() // Failed to get version number systemPrintln("Failed to get version number from server."); if (websocketConnected) - sendStringToWebsocket((char *)"newFirmwareVersion,NO_SERVER,"); + webSocketsSendString((char *)"newFirmwareVersion,NO_SERVER,"); // Report failure over the CLI if (bluetoothCommandIsConnected()) @@ -996,7 +996,7 @@ void otaUpdate() otaUpdateStop(); if (websocketConnected) - sendStringToWebsocket((char *)"gettingNewFirmware,ERROR,"); + webSocketsSendString((char *)"gettingNewFirmware,ERROR,"); // Report failure over the CLI if (bluetoothCommandIsConnected()) @@ -1010,7 +1010,7 @@ void otaUpdate() // Update triggers ESP.restart(). If we get this far, the firmware update has failed if (websocketConnected) - sendStringToWebsocket((char *)"gettingNewFirmware,ERROR,"); + 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) From 8375ee1affda5d56c4c7493e793fbbfdfa7a4ddf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 09:58:44 -1000 Subject: [PATCH 3/7] WebSockets: Use webSocketsIsConnected() instead of websocketConnected --- Firmware/RTK_Everywhere/Developer.ino | 6 ++++++ Firmware/RTK_Everywhere/RTK_Everywhere.ino | 3 +-- Firmware/RTK_Everywhere/States.ino | 2 +- Firmware/RTK_Everywhere/WebSockets.ino | 17 +++++++++++++---- Firmware/RTK_Everywhere/menuFirmware.ino | 12 ++++++------ 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Firmware/RTK_Everywhere/Developer.ino b/Firmware/RTK_Everywhere/Developer.ino index c286b7fae..e95a63362 100644 --- a/Firmware/RTK_Everywhere/Developer.ino +++ b/Firmware/RTK_Everywhere/Developer.ino @@ -304,6 +304,12 @@ void webServerVerifyTables() {} bool wifiAfterCommand(int cmdIndex){return false;} 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 006cf3188..dd7002da8 100644 --- a/Firmware/RTK_Everywhere/States.ino +++ b/Firmware/RTK_Everywhere/States.ino @@ -521,7 +521,7 @@ 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) diff --git a/Firmware/RTK_Everywhere/WebSockets.ino b/Firmware/RTK_Everywhere/WebSockets.ino index ed7e24ed2..13725a3b9 100644 --- a/Firmware/RTK_Everywhere/WebSockets.ino +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -18,6 +18,7 @@ static const int webSocketsStackSize = 1024 * 20; // Needs to be large enough static int last_ws_fd; // httpd_req_t *last_ws_req; +static bool webSocketsConnected; static httpd_handle_t webSocketsHandle; //---------------------------------------- @@ -137,7 +138,7 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) if (settings.debugWebServer == true) systemPrintf("Handshake done, the new ws connection was opened with fd %d\r\n", last_ws_fd); - websocketConnected = true; + webSocketsConnected = true; lastDynamicDataUpdate = millis(); webSocketsSendString(settingsCSV); @@ -236,13 +237,21 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) systemPrintln("Client closed or refreshed the web page"); createSettingsString(settingsCSV); - websocketConnected = false; + webSocketsConnected = false; } rtkFree(buf, "Payload buffer (buf)"); return ret; } +//---------------------------------------- +// Determine if webSockets is connected to a client +//---------------------------------------- +bool webSocketsIsConnected() +{ + return webSocketsConnected; +} + //---------------------------------------- // Send the formware version via web sockets //---------------------------------------- @@ -270,7 +279,7 @@ void webSocketsSendSettings(void) //---------------------------------------- void webSocketsSendString(const char *stringToSend) { - if (!websocketConnected) + if (!webSocketsConnected) { systemPrintf("webSocketsSendString: not connected - could not send: %s\r\n", stringToSend); return; @@ -360,7 +369,7 @@ bool webSocketsStart(void) //---------------------------------------- void webSocketsStop() { - websocketConnected = false; + webSocketsConnected = false; if (webSocketsHandle != nullptr) { diff --git a/Firmware/RTK_Everywhere/menuFirmware.ino b/Firmware/RTK_Everywhere/menuFirmware.ino index e24246d27..0f48ae452 100644 --- a/Firmware/RTK_Everywhere/menuFirmware.ino +++ b/Firmware/RTK_Everywhere/menuFirmware.ino @@ -884,7 +884,7 @@ 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 webSocketsSendString((char *)"newFirmwareVersion,NO_INTERNET,"); @@ -941,7 +941,7 @@ void otaUpdate() { otaRequestFirmwareVersionCheck = false; - if (websocketConnected) + if (webSocketsIsConnected()) { char newVersionCSV[40]; snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,%s,", @@ -966,7 +966,7 @@ void otaUpdate() else { systemPrintln("Version Check: Firmware is up to date. No new firmware available."); - if (websocketConnected) + if (webSocketsIsConnected()) webSocketsSendString((char *)"newFirmwareVersion,CURRENT,"); otaUpdateStop(); @@ -976,7 +976,7 @@ void otaUpdate() { // Failed to get version number systemPrintln("Failed to get version number from server."); - if (websocketConnected) + if (webSocketsIsConnected()) webSocketsSendString((char *)"newFirmwareVersion,NO_SERVER,"); // Report failure over the CLI @@ -995,7 +995,7 @@ void otaUpdate() { otaUpdateStop(); - if (websocketConnected) + if (webSocketsIsConnected()) webSocketsSendString((char *)"gettingNewFirmware,ERROR,"); // Report failure over the CLI @@ -1009,7 +1009,7 @@ void otaUpdate() otaUpdateFirmware(); // Update triggers ESP.restart(). If we get this far, the firmware update has failed - if (websocketConnected) + if (webSocketsIsConnected()) webSocketsSendString((char *)"gettingNewFirmware,ERROR,"); // Report failure over the CLI From f1b9d2291591e29d17a5b9158f29e520b7576dfe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 10:36:10 -1000 Subject: [PATCH 4/7] WebSockets: Update start messages --- Firmware/RTK_Everywhere/WebSockets.ino | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Firmware/RTK_Everywhere/WebSockets.ino b/Firmware/RTK_Everywhere/WebSockets.ino index 13725a3b9..c762f7e94 100644 --- a/Firmware/RTK_Everywhere/WebSockets.ino +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -342,7 +342,7 @@ bool webSocketsStart(void) // Start the httpd server if (settings.debugWebServer == true) - systemPrintf("Starting webSockets on port: %d\r\n", config.server_port); + systemPrintf("webSockets starting on port: %d\r\n", config.server_port); if (settings.debugWebServer == true) { @@ -354,13 +354,14 @@ bool webSocketsStart(void) { // Registering the ws handler if (settings.debugWebServer == true) - systemPrintln("Registering URI handlers"); + systemPrintln("webSockets registering URI handlers"); httpd_register_uri_handler(webSocketsHandle, &webSocketsPage); return true; } // Display the failure to start - systemPrintf("ERROR: webSockets failed to start, status: %s!\r\n", esp_err_to_name(status)); + if (settings.debugWebServer) + systemPrintf("ERROR: webSockets failed to start, status: %s!\r\n", esp_err_to_name(status)); return false; } @@ -375,7 +376,9 @@ void webSocketsStop() { // Stop the httpd server esp_err_t status = httpd_stop(webSocketsHandle); - if (status != ESP_OK) + 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; } From b40af673169917ba2542e24f338270e78fb373f5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 10:56:56 -1000 Subject: [PATCH 5/7] menuCommands: Protect against settingsCSV being nullptr --- Firmware/RTK_Everywhere/menuCommands.ino | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/Firmware/RTK_Everywhere/menuCommands.ino b/Firmware/RTK_Everywhere/menuCommands.ino index 029f4ae41..995d91148 100644 --- a/Firmware/RTK_Everywhere/menuCommands.ino +++ b/Firmware/RTK_Everywhere/menuCommands.ino @@ -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); + } - webSocketsSendString(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); + } - webSocketsSendString(settingsCSV); + webSocketsSendString(settingsCSV); + } knownSetting = true; } From 7ee6fe7e67e82df971e27e7ad9d2ea92ea2eb22d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 12:42:33 -1000 Subject: [PATCH 6/7] WebSockets: Add webSockets to the messages --- Firmware/RTK_Everywhere/WebSockets.ino | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Firmware/RTK_Everywhere/WebSockets.ino b/Firmware/RTK_Everywhere/WebSockets.ino index c762f7e94..a5409802a 100644 --- a/Firmware/RTK_Everywhere/WebSockets.ino +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -105,7 +105,7 @@ void webSocketsCreateFirmwareVersionString(char *settingsCSV) if (firmwareVersionIsReportedNewer(otaReportedVersion, currentVersion) == true) { if (settings.debugWebServer == true) - systemPrintln("New version detected"); + systemPrintln("WebSockets: New firmware version detected"); snprintf(newVersionCSV, sizeof(newVersionCSV), "%s,", otaReportedVersion); } else @@ -136,7 +136,8 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) 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); + systemPrintf("webSockets: Added client, _request: %p, _socketFD: %d\r\n", + client->_request, client->_socketFD); webSocketsConnected = true; lastDynamicDataUpdate = millis(); @@ -153,18 +154,18 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) 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); + systemPrintf("WebSockets: 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); + 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("Failed to malloc memory for buf"); + systemPrintln("WebSockets: Failed to malloc memory for buf"); return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; @@ -172,7 +173,7 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) 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); + systemPrintf("WebSockets: httpd_ws_recv_frame failed with %d\r\n", ret); rtkFree(buf, "Payload buffer (buf)"); return ret; } @@ -205,7 +206,7 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) 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, + 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; @@ -220,7 +221,7 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) { incomingSettings[incomingSettingsSpot++] = ws_pkt.payload[i]; if (incomingSettingsSpot == AP_CONFIG_SETTING_SIZE) - systemPrintln("incomingSettings wrap-around. Increase AP_CONFIG_SETTING_SIZE"); + systemPrintln("WebSockets: incomingSettings wrap-around. Increase AP_CONFIG_SETTING_SIZE"); incomingSettingsSpot %= AP_CONFIG_SETTING_SIZE; } timeSinceLastIncomingSetting = millis(); @@ -228,13 +229,13 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) else { if (settings.debugWebServer == true) - systemPrintln("Ignoring packet due to parsing block"); + systemPrintln("WebSockets: 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"); + systemPrintln("WebSockets: Client closed or refreshed the web page"); createSettingsString(settingsCSV); webSocketsConnected = false; @@ -260,7 +261,7 @@ void webSocketsSendFirmwareVersion(void) webSocketsCreateFirmwareVersionString(settingsCSV); if (settings.debugWebServer) - systemPrintf("WebServer: Firmware version requested. Sending: %s\r\n", settingsCSV); + systemPrintf("WebSockets: Firmware version requested. Sending: %s\r\n", settingsCSV); webSocketsSendString(settingsCSV); } @@ -304,7 +305,7 @@ void webSocketsSendString(const char *stringToSend) esp_err_t ret = httpd_ws_send_frame_async(webSocketsHandle, last_ws_fd, &ws_pkt); if (ret != ESP_OK) { - systemPrintf("httpd_ws_send_frame failed with %d\r\n", ret); + systemPrintf("WebSockets: httpd_ws_send_frame failed with %d\r\n", ret); } else { From dfc3e3514541ce507cf92c498443fbc56170ae23 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 8 Dec 2025 13:52:26 -1000 Subject: [PATCH 7/7] webSockets: Add list of active clients --- Firmware/RTK_Everywhere/WebSockets.ino | 160 ++++++++++++++++++++----- 1 file changed, 132 insertions(+), 28 deletions(-) diff --git a/Firmware/RTK_Everywhere/WebSockets.ino b/Firmware/RTK_Everywhere/WebSockets.ino index a5409802a..e186ea649 100644 --- a/Firmware/RTK_Everywhere/WebSockets.ino +++ b/Firmware/RTK_Everywhere/WebSockets.ino @@ -12,14 +12,26 @@ WebSockets.ino 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 int last_ws_fd; -// httpd_req_t *last_ws_req; -static bool webSocketsConnected; +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, @@ -125,21 +137,50 @@ void webSocketsCreateFirmwareVersionString(char *settingsCSV) //---------------------------------------- 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) { - // 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); + // 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); - webSocketsConnected = true; lastDynamicDataUpdate = millis(); webSocketsSendString(settingsCSV); @@ -238,7 +279,6 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) systemPrintln("WebSockets: Client closed or refreshed the web page"); createSettingsString(settingsCSV); - webSocketsConnected = false; } rtkFree(buf, "Payload buffer (buf)"); @@ -250,7 +290,7 @@ static esp_err_t webSocketsHandler(httpd_req_t *req) //---------------------------------------- bool webSocketsIsConnected() { - return webSocketsConnected; + return (webSocketsClientListHead != nullptr); } //---------------------------------------- @@ -280,38 +320,67 @@ void webSocketsSendSettings(void) //---------------------------------------- void webSocketsSendString(const char *stringToSend) { - if (!webSocketsConnected) + WEB_SOCKETS_CLIENT * client; + + if (!webSocketsIsConnected()) { systemPrintf("webSocketsSendString: 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 (webSocketsHandle)... - + // 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; - // 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); - //} + // Single thread access to the list of clients; + xSemaphoreTake(webSocketsMutex, portMAX_DELAY); - // If we use httpd_ws_send_frame_async, it requires a fd. - esp_err_t ret = httpd_ws_send_frame_async(webSocketsHandle, last_ws_fd, &ws_pkt); - if (ret != ESP_OK) + // Send this message to each of the clients + client = webSocketsClientListHead; + while (client) { - systemPrintf("WebSockets: httpd_ws_send_frame failed with %d\r\n", ret); - } - else - { - if (settings.debugWebServer == true) - systemPrintf("webSocketsSendString: %s\r\n", stringToSend); + // 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); } //---------------------------------------- @@ -350,6 +419,19 @@ bool webSocketsStart(void) 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) { @@ -371,10 +453,32 @@ bool webSocketsStart(void) //---------------------------------------- void webSocketsStop() { - webSocketsConnected = false; + 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)