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/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: "); diff --git a/src/driverstation/esp32s3/index_html.h b/src/driverstation/esp32s3/index_html.h index 612234c..7c2151c 100644 --- a/src/driverstation/esp32s3/index_html.h +++ b/src/driverstation/esp32s3/index_html.h @@ -918,13 +918,23 @@ function stopAutoTimer(){ default: cmd="stop"; break; } + // WS lifecycle: open on init, close on stop + if(cmd==="stop"){ + wsStopped=true; + killWs(); + }else if(cmd==="init"){ + connectWebSocket(); + } + 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"); + 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(err); - return; + console.error("robotControl fetch error:",err); } const btn=document.getElementById('robotButton'); @@ -1001,21 +1011,41 @@ function stopAutoTimer(){ let wsJoystick=null; let wsConnected=false; 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 && wsJoystick.readyState<=1) return; + killWs(); + wsStopped=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;if(!wsStopped)scheduleReconnect();}; + ws.onerror=()=>{wsConnected=false;}; + ws.onmessage=()=>{wsLastActivity=performance.now();}; + wsJoystick=ws; + }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(wsStopped||!wsJoystick) return; + if(wsJoystick.readyState>1){wsConnected=false;wsJoystick=null;scheduleReconnect();return;} + if(wsConnected && performance.now()-wsLastActivity>3000){ + console.log('[WS] Stale, reconnecting'); + killWs(); + scheduleReconnect(); + } + } + setInterval(wsHealthCheck,1000); function packJoystickBinary(gp){ const nA=gp.axes.length; const nB=gp.buttons.length; @@ -1047,19 +1077,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..3198a7f 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,27 @@ 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; + httpd_ws_frame_t ping = {}; + ping.type = HTTPD_WS_TYPE_PING; + 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) { + 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]); + } + } + } + } + io::GamepadService& _gs; httpd_handle_t _server = nullptr; + TimerHandle_t _pingTimer = nullptr; }; }