From 18334c2a2b1dbe62a34ade405022790ff7d15e44 Mon Sep 17 00:00:00 2001 From: tunapro Date: Sat, 7 Feb 2026 15:02:48 +0300 Subject: [PATCH 1/4] caracal Co-Authored-By: Claude Opus 4.6 --- src/driverstation/esp32s3/index_html.h | 45 +++++++++++++++++------ src/driverstation/esp32s3/ws_joystick.hpp | 22 +++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/driverstation/esp32s3/index_html.h b/src/driverstation/esp32s3/index_html.h index 612234c..df52712 100644 --- a/src/driverstation/esp32s3/index_html.h +++ b/src/driverstation/esp32s3/index_html.h @@ -1001,21 +1001,39 @@ function stopAutoTimer(){ let wsJoystick=null; let wsConnected=false; let wsReconnectTimer=null; + let wsLastActivity=0; function connectWebSocket(){ - if(wsJoystick && wsJoystick.readyState<=1) return; + if(wsJoystick){ + try{wsJoystick.close();}catch(e){} + wsJoystick=null; + } + wsConnected=false; try{ - wsJoystick=new WebSocket(`ws://${location.hostname}:81/joystick`); - wsJoystick.binaryType='arraybuffer'; - wsJoystick.onopen=()=>{wsConnected=true;console.log('[WS] Connected');}; - wsJoystick.onclose=()=>{wsConnected=false;console.log('[WS] Closed');scheduleReconnect();}; - wsJoystick.onerror=()=>{wsConnected=false;}; - }catch(e){wsConnected=false;scheduleReconnect();} + const ws=new WebSocket(`ws://${location.hostname}:81/joystick`); + ws.binaryType='arraybuffer'; + ws.onopen=()=>{wsConnected=true;wsLastActivity=performance.now();console.log('[WS] Connected');}; + ws.onclose=()=>{wsConnected=false;wsJoystick=null;scheduleReconnect();}; + ws.onerror=()=>{wsConnected=false;}; + ws.onmessage=()=>{wsLastActivity=performance.now();}; + wsJoystick=ws; + }catch(e){scheduleReconnect();} } function scheduleReconnect(){ if(wsReconnectTimer) return; wsReconnectTimer=setTimeout(()=>{wsReconnectTimer=null;connectWebSocket();},2000); } + function wsHealthCheck(){ + if(!wsJoystick) return; + if(wsJoystick.readyState>1){wsConnected=false;wsJoystick=null;scheduleReconnect();return;} + if(wsConnected && performance.now()-wsLastActivity>3000){ + console.log('[WS] Stale, reconnecting'); + try{wsJoystick.close();}catch(e){} + wsConnected=false;wsJoystick=null; + scheduleReconnect(); + } + } + setInterval(wsHealthCheck,1000); function packJoystickBinary(gp){ const nA=gp.axes.length; const nB=gp.buttons.length; @@ -1047,19 +1065,22 @@ function stopAutoTimer(){ if(wsConnected && wsJoystick && wsJoystick.readyState===1){ try{ wsJoystick.send(packJoystickBinary(gp)); + wsLastActivity=now; gamepadSending=false; return; - }catch(e){wsConnected=false;scheduleReconnect();} + }catch(e){wsConnected=false;wsJoystick=null;scheduleReconnect();} } + const ac=new AbortController(); + const tid=setTimeout(()=>ac.abort(),2000); const data={axes:Array.from(gp.axes),buttons:gp.buttons.map(b=>b.pressed)}; await fetch("/updateController",{ method:"POST", headers:{"Content-Type":"application/json"}, - body:JSON.stringify(data) + body:JSON.stringify(data), + signal:ac.signal }); - }catch(err){ - console.error(err); - }finally{ + clearTimeout(tid); + }catch(err){}finally{ gamepadSending=false; } } diff --git a/src/driverstation/esp32s3/ws_joystick.hpp b/src/driverstation/esp32s3/ws_joystick.hpp index bcddb78..7d5f84a 100644 --- a/src/driverstation/esp32s3/ws_joystick.hpp +++ b/src/driverstation/esp32s3/ws_joystick.hpp @@ -41,6 +41,10 @@ namespace probot::driverstation::esp32 { }; httpd_register_uri_handler(_server, &ws_uri); + // Periodic ping to detect dead connections + _pingTimer = xTimerCreate("ws_ping", pdMS_TO_TICKS(2000), pdTRUE, this, pingTimerCb); + if (_pingTimer) xTimerStart(_pingTimer, 0); + Serial.print("[WS ] WebSocket server on port "); Serial.println(port); } @@ -112,8 +116,26 @@ namespace probot::driverstation::esp32 { _gs.write(millis(), axes, nA, buttons, nB); } + static void pingTimerCb(TimerHandle_t t) { + auto* self = static_cast(pvTimerGetTimerID(t)); + if (!self->_server) return; + // Send PING to all connected WS clients + httpd_ws_frame_t ping = {}; + ping.type = HTTPD_WS_TYPE_PING; + // Get connected clients and send ping + size_t fds = 4; + int clients[4]; + httpd_get_client_list(self->_server, &fds, clients); + for (size_t i = 0; i < fds; i++) { + if (httpd_ws_get_fd_info(self->_server, clients[i]) == HTTPD_WS_CLIENT_WEBSOCKET) { + httpd_ws_send_frame_async(self->_server, clients[i], &ping); + } + } + } + io::GamepadService& _gs; httpd_handle_t _server = nullptr; + TimerHandle_t _pingTimer = nullptr; }; } From 2a875bfbd8bab09725003c5094f32aed6bbe1ea4 Mon Sep 17 00:00:00 2001 From: tunapro Date: Sat, 7 Feb 2026 15:04:33 +0300 Subject: [PATCH 2/4] caracal power Co-Authored-By: Claude Opus 4.6 --- src/driverstation/esp32s3/driver_station_esp32.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/driverstation/esp32s3/driver_station_esp32.hpp b/src/driverstation/esp32s3/driver_station_esp32.hpp index 2de5fad..bfc1d65 100644 --- a/src/driverstation/esp32s3/driver_station_esp32.hpp +++ b/src/driverstation/esp32s3/driver_station_esp32.hpp @@ -50,6 +50,7 @@ namespace probot::driverstation::esp32 { WiFi.softAP(ssid.c_str(), pw, PROBOT_WIFI_AP_CHANNEL); esp_wifi_set_bandwidth(WIFI_IF_AP, WIFI_BW_HT20); esp_wifi_set_ps(WIFI_PS_NONE); + WiFi.setTxPower(WIFI_POWER_19_5dBm); Serial.println("[DS ] ========================================"); Serial.print("[DS ] WiFi SSID: "); From 86bef37bc83d37e4c37e536bae35c9adf4be4e25 Mon Sep 17 00:00:00 2001 From: tunapro Date: Sat, 7 Feb 2026 20:04:00 +0300 Subject: [PATCH 3/4] caracal: harden WebSocket lifecycle and dead connection recovery Co-Authored-By: Claude Opus 4.6 --- probot-lib.code-workspace | 9 +++++++ src/driverstation/esp32s3/index_html.h | 33 ++++++++++++++--------- src/driverstation/esp32s3/ws_joystick.hpp | 13 ++++----- 3 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 probot-lib.code-workspace diff --git a/probot-lib.code-workspace b/probot-lib.code-workspace new file mode 100644 index 0000000..0416d95 --- /dev/null +++ b/probot-lib.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "probot-lib", + "path": "../../Arduino/libraries/probot-lib" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/driverstation/esp32s3/index_html.h b/src/driverstation/esp32s3/index_html.h index df52712..6158c37 100644 --- a/src/driverstation/esp32s3/index_html.h +++ b/src/driverstation/esp32s3/index_html.h @@ -926,6 +926,13 @@ function stopAutoTimer(){ console.error(err); return; } + // WS lifecycle: open on init, close on stop + if(cmd==="stop"){ + wsStopped=true; + killWs(); + }else if(cmd==="init"){ + connectWebSocket(); + } const btn=document.getElementById('robotButton'); if(controlState==="idle"){ @@ -1003,33 +1010,35 @@ function stopAutoTimer(){ let wsReconnectTimer=null; let wsLastActivity=0; + let wsStopped=false; + function killWs(){ + if(wsReconnectTimer){clearTimeout(wsReconnectTimer);wsReconnectTimer=null;} + if(wsJoystick){wsJoystick.onopen=null;wsJoystick.onclose=null;wsJoystick.onerror=null;wsJoystick.onmessage=null;try{wsJoystick.close();}catch(e){}} + wsJoystick=null;wsConnected=false; + } function connectWebSocket(){ - if(wsJoystick){ - try{wsJoystick.close();}catch(e){} - wsJoystick=null; - } - wsConnected=false; + killWs(); + wsStopped=false; try{ const ws=new WebSocket(`ws://${location.hostname}:81/joystick`); ws.binaryType='arraybuffer'; ws.onopen=()=>{wsConnected=true;wsLastActivity=performance.now();console.log('[WS] Connected');}; - ws.onclose=()=>{wsConnected=false;wsJoystick=null;scheduleReconnect();}; + ws.onclose=()=>{wsConnected=false;wsJoystick=null;if(!wsStopped)scheduleReconnect();}; ws.onerror=()=>{wsConnected=false;}; ws.onmessage=()=>{wsLastActivity=performance.now();}; wsJoystick=ws; - }catch(e){scheduleReconnect();} + }catch(e){if(!wsStopped)scheduleReconnect();} } function scheduleReconnect(){ - if(wsReconnectTimer) return; - wsReconnectTimer=setTimeout(()=>{wsReconnectTimer=null;connectWebSocket();},2000); + if(wsReconnectTimer||wsStopped) return; + wsReconnectTimer=setTimeout(()=>{wsReconnectTimer=null;if(!wsStopped)connectWebSocket();},2000); } function wsHealthCheck(){ - if(!wsJoystick) return; + if(wsStopped||!wsJoystick) return; if(wsJoystick.readyState>1){wsConnected=false;wsJoystick=null;scheduleReconnect();return;} if(wsConnected && performance.now()-wsLastActivity>3000){ console.log('[WS] Stale, reconnecting'); - try{wsJoystick.close();}catch(e){} - wsConnected=false;wsJoystick=null; + killWs(); scheduleReconnect(); } } diff --git a/src/driverstation/esp32s3/ws_joystick.hpp b/src/driverstation/esp32s3/ws_joystick.hpp index 7d5f84a..3198a7f 100644 --- a/src/driverstation/esp32s3/ws_joystick.hpp +++ b/src/driverstation/esp32s3/ws_joystick.hpp @@ -119,16 +119,17 @@ namespace probot::driverstation::esp32 { static void pingTimerCb(TimerHandle_t t) { auto* self = static_cast(pvTimerGetTimerID(t)); if (!self->_server) return; - // Send PING to all connected WS clients httpd_ws_frame_t ping = {}; ping.type = HTTPD_WS_TYPE_PING; - // Get connected clients and send ping - size_t fds = 4; - int clients[4]; - httpd_get_client_list(self->_server, &fds, clients); + size_t fds = 8; + int clients[8]; + if (httpd_get_client_list(self->_server, &fds, clients) != ESP_OK) return; for (size_t i = 0; i < fds; i++) { if (httpd_ws_get_fd_info(self->_server, clients[i]) == HTTPD_WS_CLIENT_WEBSOCKET) { - httpd_ws_send_frame_async(self->_server, clients[i], &ping); + esp_err_t err = httpd_ws_send_frame_async(self->_server, clients[i], &ping); + if (err != ESP_OK) { + httpd_sess_trigger_close(self->_server, clients[i]); + } } } } From 2ba953a9afeb83ea0b01c77a2750b65f640571f3 Mon Sep 17 00:00:00 2001 From: tunapro Date: Sat, 7 Feb 2026 20:17:26 +0300 Subject: [PATCH 4/4] caracal Co-Authored-By: Claude Opus 4.6 --- src/driverstation/esp32s3/index_html.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/driverstation/esp32s3/index_html.h b/src/driverstation/esp32s3/index_html.h index 6158c37..7c2151c 100644 --- a/src/driverstation/esp32s3/index_html.h +++ b/src/driverstation/esp32s3/index_html.h @@ -918,14 +918,6 @@ function stopAutoTimer(){ default: cmd="stop"; break; } - const url=`/robotControl?cmd=${cmd}&auto=${enableAuto?1:0}&autoLen=${autoLen}`; - try{ - const r=await fetch(url); - if(!r.ok) throw new Error("Robot command failed"); - }catch(err){ - console.error(err); - return; - } // WS lifecycle: open on init, close on stop if(cmd==="stop"){ wsStopped=true; @@ -934,6 +926,17 @@ function stopAutoTimer(){ connectWebSocket(); } + const url=`/robotControl?cmd=${cmd}&auto=${enableAuto?1:0}&autoLen=${autoLen}`; + try{ + const ac=new AbortController(); + const tid=setTimeout(()=>ac.abort(),3000); + const r=await fetch(url,{signal:ac.signal}); + clearTimeout(tid); + if(!r.ok) console.error("Robot command failed:",r.status); + }catch(err){ + console.error("robotControl fetch error:",err); + } + const btn=document.getElementById('robotButton'); if(controlState==="idle"){ controlState="armed";