Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.2
0.2.4
2 changes: 1 addition & 1 deletion idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 0.2.2
version: 0.2.4
description: ESP32-S3 robotics control library
url: https://github.com/nfrproducts/probot-lib
dependencies:
Expand Down
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
"type": "git",
"url": "https://github.com/nfrproducts/probot-lib"
},
"version": "0.2.2"
"version": "0.2.4"
}
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=probot
version=0.2.2
version=0.2.4
author=Tuna Gül
maintainer=Tuna Gül <tunagul54@gmail.com>
sentence=ProBot Library for Robotics Competitions.
Expand Down
8 changes: 7 additions & 1 deletion src/driverstation/esp32s3/driver_station_esp32.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#pragma once
#ifdef ESP32
#include <WiFi.h>
#include <esp_wifi.h>
#include <WebServer.h>
#include <Arduino.h>
#include <probot/robot/state.hpp>
#include <probot/io/gamepad.hpp>
#include <probot/telemetry/telemetry.hpp>
#include "index_html.h"
#include "ws_joystick.hpp"

#ifndef PROBOT_WIFI_AP_PASSWORD
#error "DriverStation AP password not provided. Define PROBOT_WIFI_AP_PASSWORD (>=8 chars) before including probot.h."
Expand All @@ -31,7 +33,7 @@ namespace probot::driverstation::esp32 {
class DriverStation {
public:
DriverStation(robot::StateService& rs, io::GamepadService& gs)
: _rs(rs), _gs(gs), _server(80) {}
: _rs(rs), _gs(gs), _ws(gs), _server(80) {}

void begin(){
const char* pw = PROBOT_WIFI_AP_PASSWORD;
Expand All @@ -44,6 +46,8 @@ namespace probot::driverstation::esp32 {
ap_ssid_ = ssid;
WiFi.mode(WIFI_AP);
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);

Serial.println("[DS ] ========================================");
Serial.print("[DS ] WiFi SSID: ");
Expand All @@ -63,6 +67,7 @@ namespace probot::driverstation::esp32 {
_server.on("/getBattery", HTTP_GET, [this](){ handleGetBattery(); });
_server.on("/telemetry", HTTP_GET, [this](){ if (!enforceOwner()) return; handleTelemetry(); });
_server.begin();
_ws.begin(81);
}

void handleClient(){
Expand Down Expand Up @@ -203,6 +208,7 @@ namespace probot::driverstation::esp32 {

robot::StateService& _rs;
io::GamepadService& _gs;
WsJoystick _ws;
WebServer _server;
bool _owner_set=false;
IPAddress _owner;
Expand Down
52 changes: 51 additions & 1 deletion src/driverstation/esp32s3/index_html.h
Original file line number Diff line number Diff line change
Expand Up @@ -996,13 +996,62 @@ function stopAutoTimer(){
let gamepadSending=false;
let lastGamepadSend=0;
const GAMEPAD_SEND_INTERVAL=20; // 50Hz max

// WebSocket joystick channel
let wsJoystick=null;
let wsConnected=false;
let wsReconnectTimer=null;

function connectWebSocket(){
if(wsJoystick && wsJoystick.readyState<=1) return;
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();}
}
function scheduleReconnect(){
if(wsReconnectTimer) return;
wsReconnectTimer=setTimeout(()=>{wsReconnectTimer=null;connectWebSocket();},2000);
}
function packJoystickBinary(gp){
const nA=gp.axes.length;
const nB=gp.buttons.length;
const btnBytes=Math.ceil(nB/8);
const buf=new ArrayBuffer(4+nA*2+btnBytes);
const view=new DataView(buf);
view.setUint8(0,0x4A);
view.setUint8(1,nA);
view.setUint8(2,nB);
view.setUint8(3,0);
for(let i=0;i<nA;i++){
const v=Math.max(-1,Math.min(1,gp.axes[i]));
view.setInt16(4+i*2,Math.round(v*32767),false);
}
const btnOff=4+nA*2;
for(let i=0;i<nB;i++){
if(gp.buttons[i].pressed){
view.setUint8(btnOff+Math.floor(i/8),view.getUint8(btnOff+Math.floor(i/8))|(1<<(i%8)));
}
}
return buf;
}
async function sendGamepadData(gp){
const now=performance.now();
if(gamepadSending || (now-lastGamepadSend)<GAMEPAD_SEND_INTERVAL) return;
gamepadSending=true;
lastGamepadSend=now;
const data={axes:Array.from(gp.axes),buttons:gp.buttons.map(b=>b.pressed)};
try{
if(wsConnected && wsJoystick && wsJoystick.readyState===1){
try{
wsJoystick.send(packJoystickBinary(gp));
gamepadSending=false;
return;
}catch(e){wsConnected=false;scheduleReconnect();}
}
const data={axes:Array.from(gp.axes),buttons:gp.buttons.map(b=>b.pressed)};
await fetch("/updateController",{
method:"POST",
headers:{"Content-Type":"application/json"},
Expand Down Expand Up @@ -1060,6 +1109,7 @@ function stopAutoTimer(){
setPhaseDisplay('standby');
requestAnimationFrame(gamepadLoop);
syncState();
connectWebSocket();
});

document.getElementById('autoPeriod').addEventListener('input',e=>{
Expand Down
120 changes: 120 additions & 0 deletions src/driverstation/esp32s3/ws_joystick.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#pragma once
#ifdef ESP32
#include <esp_http_server.h>
#include <probot/io/gamepad.hpp>

namespace probot::driverstation::esp32 {

/**
* WebSocket joystick server (ESP-IDF httpd, port 81)
*
* Binary frame format:
* [0] uint8 0x4A ('J' magic)
* [1] uint8 axisCount (max 20)
* [2] uint8 buttonCount (max 20)
* [3] uint8 reserved
* [4..] int16[] axes (big-endian, value = float * 32767)
* [4+nA*2] uint8[] buttons (packed bits, LSB first)
*/
class WsJoystick {
public:
explicit WsJoystick(io::GamepadService& gs) : _gs(gs) {}

void begin(uint16_t port = 81) {
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.server_port = port;
cfg.ctrl_port = port + 1;
cfg.stack_size = 4096;

if (httpd_start(&_server, &cfg) != ESP_OK) {
Serial.println("[WS ] Failed to start WebSocket server");
return;
}

httpd_uri_t ws_uri = {
.uri = "/joystick",
.method = HTTP_GET,
.handler = wsHandler,
.user_ctx = this,
.is_websocket = true,
.handle_ws_control_frames = true,
};
httpd_register_uri_handler(_server, &ws_uri);

Serial.print("[WS ] WebSocket server on port ");
Serial.println(port);
}

private:
static constexpr uint8_t MAGIC = 0x4A;
static constexpr uint32_t MAX_AXES = 20;
static constexpr uint32_t MAX_BTNS = 20;
static constexpr size_t MAX_FRAME = 4 + MAX_AXES * 2 + (MAX_BTNS + 7) / 8;

static esp_err_t wsHandler(httpd_req_t* req) {
if (req->method == HTTP_GET) {
return ESP_OK; // WS handshake — just accept
}

auto* self = static_cast<WsJoystick*>(req->user_ctx);

// Step 1: 0-length receive to learn frame size & type
httpd_ws_frame_t frame = {};
frame.payload = nullptr;
esp_err_t ret = httpd_ws_recv_frame(req, &frame, 0);
if (ret != ESP_OK) return ret;

// Handle control frames
if (frame.type == HTTPD_WS_TYPE_CLOSE) return ESP_OK;
if (frame.type == HTTPD_WS_TYPE_PING) {
httpd_ws_frame_t pong = {};
pong.type = HTTPD_WS_TYPE_PONG;
return httpd_ws_send_frame(req, &pong);
}
if (frame.type != HTTPD_WS_TYPE_BINARY) return ESP_OK;

// Step 2: bounds check, then receive payload
if (frame.len == 0 || frame.len > MAX_FRAME) return ESP_OK;

uint8_t buf[MAX_FRAME];
frame.payload = buf;
ret = httpd_ws_recv_frame(req, &frame, frame.len);
if (ret != ESP_OK) return ret;

self->parseFrame(buf, frame.len);
return ESP_OK;
}

void parseFrame(const uint8_t* data, size_t len) {
if (len < 4 || data[0] != MAGIC) return;

uint32_t nA = data[1];
uint32_t nB = data[2];
if (nA > MAX_AXES) nA = MAX_AXES;
if (nB > MAX_BTNS) nB = MAX_BTNS;

size_t expected = 4 + nA * 2 + (nB + 7) / 8;
if (len < expected) return;

float axes[MAX_AXES];
const uint8_t* p = data + 4;
for (uint32_t i = 0; i < nA; i++) {
int16_t raw = static_cast<int16_t>((p[0] << 8) | p[1]);
axes[i] = static_cast<float>(raw) / 32767.0f;
p += 2;
}

bool buttons[MAX_BTNS];
for (uint32_t i = 0; i < nB; i++) {
buttons[i] = (p[i / 8] >> (i % 8)) & 1;
}

_gs.write(millis(), axes, nA, buttons, nB);
}

io::GamepadService& _gs;
httpd_handle_t _server = nullptr;
};

}
#endif // ESP32
2 changes: 1 addition & 1 deletion src/probot/io/gamepad.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace probot::io {
GamepadSnapshot z{};
_buf[0] = z;
_buf[1] = z;
_timeout_ms = 300;
_timeout_ms = 500;
}

void write(uint32_t now_ms, const float* axes, uint32_t nAxis, const bool* buttons, uint32_t nButton){
Expand Down