Files
Transpotter-Stuff/esp32/src/webui.cpp
T
2026-09-20 18:55:16 +02:00

182 lines
5.3 KiB
C++

#include "webui.h"
#include "web_page.h"
#include "robot_config.h"
#include <WiFi.h>
#include <esp_http_server.h>
namespace webui {
namespace {
httpd_handle_t server = nullptr;
// Everything below is touched by both the HTTP task and the control loop.
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
Telemetry shared{};
DriveInput pendingDrive{};
bool haveDrive = false;
char pendingCmd[16] = {0};
bool haveCmd = false;
Settings pendingSettings{};
bool haveSettings = false;
float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); }
esp_err_t indexHandler(httpd_req_t* req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
return httpd_resp_send(req, PAGE_HTML, HTTPD_RESP_USE_STRLEN);
}
int buildTelemetry(char* out, size_t len) {
Telemetry t;
portENTER_CRITICAL(&mux);
t = shared;
portEXIT_CRITICAL(&mux);
return snprintf(out, len,
"{\"m\":\"%s\",\"a\":%d,\"k\":%d,\"cl\":%d,\"cr\":%d,"
"\"v\":%.3f,\"w\":%.3f,\"g\":%.2f,\"d\":%.3f,\"h\":%.1f,"
"\"b\":%.2f,\"t\":%.1f,\"fo\":%lu,\"fb\":%lu,"
"\"lim\":%.2f,\"rsp\":%.2f}",
t.mode, t.armed ? 1 : 0, t.link ? 1 : 0, t.cmdL, t.cmdR,
t.v, t.w, t.gain, t.dist, t.headDeg,
t.battery, t.temp,
(unsigned long)t.framesOk, (unsigned long)t.framesBad,
t.limit, t.response);
}
esp_err_t wsHandler(httpd_req_t* req) {
if (req->method == HTTP_GET) return ESP_OK; // handshake only
httpd_ws_frame_t in{};
in.type = HTTPD_WS_TYPE_TEXT;
// First call with len 0 reports the frame size.
esp_err_t err = httpd_ws_recv_frame(req, &in, 0);
if (err != ESP_OK) return err;
if (in.len == 0 || in.len > 64) return ESP_OK;
uint8_t buf[65] = {0};
in.payload = buf;
err = httpd_ws_recv_frame(req, &in, in.len);
if (err != ESP_OK) return err;
if (buf[0] == 'd') { // d,<throttle>,<steer>
float th = 0, st = 0;
if (sscanf((const char*)buf, "d,%f,%f", &th, &st) == 2) {
portENTER_CRITICAL(&mux);
pendingDrive.throttle = clampf(th, -1.0f, 1.0f);
pendingDrive.steer = clampf(st, -1.0f, 1.0f);
haveDrive = true;
portEXIT_CRITICAL(&mux);
}
} else if (buf[0] == 's') { // s,<limit>,<response>
float lim = 0, rsp = 0;
if (sscanf((const char*)buf, "s,%f,%f", &lim, &rsp) == 2) {
portENTER_CRITICAL(&mux);
pendingSettings.limit = clampf(lim, 0.05f, 1.0f);
pendingSettings.response = clampf(rsp, 0.0f, 1.0f);
haveSettings = true;
portEXIT_CRITICAL(&mux);
}
} else if (buf[0] == 'c') { // c,<word>
char word[16] = {0};
if (sscanf((const char*)buf, "c,%15s", word) == 1) {
portENTER_CRITICAL(&mux);
strncpy(pendingCmd, word, sizeof(pendingCmd) - 1);
pendingCmd[sizeof(pendingCmd) - 1] = '\0';
haveCmd = true;
portEXIT_CRITICAL(&mux);
}
}
// Answering every frame keeps the browser's 10 Hz cadence as the heartbeat
// and avoids the bookkeeping of async sends to tracked sockets.
char json[320];
const int n = buildTelemetry(json, sizeof(json));
if (n <= 0) return ESP_OK;
httpd_ws_frame_t out{};
out.type = HTTPD_WS_TYPE_TEXT;
out.payload = (uint8_t*)json;
out.len = (size_t)n;
return httpd_ws_send_frame(req, &out);
}
} // namespace
void begin() {
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CLIENTS);
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
// Below the Arduino loop task would starve the 100 Hz control loop under
// load; above it, a burst of HTTP work could delay a command frame. The
// mainboard closes its own loops at 16 kHz, so a late setpoint is harmless,
// but there is no reason to make it likely.
cfg.task_priority = 4;
cfg.stack_size = 4096;
cfg.max_open_sockets = 4;
cfg.lru_purge_enable = true;
if (httpd_start(&server, &cfg) != ESP_OK) {
Serial.println(F("! web UI: httpd_start failed"));
server = nullptr;
return;
}
httpd_uri_t page{};
page.uri = "/";
page.method = HTTP_GET;
page.handler = indexHandler;
page.user_ctx = nullptr;
httpd_register_uri_handler(server, &page);
httpd_uri_t ws{};
ws.uri = "/ws";
ws.method = HTTP_GET;
ws.handler = wsHandler;
ws.user_ctx = nullptr;
ws.is_websocket = true;
httpd_register_uri_handler(server, &ws);
Serial.printf("web UI: join WiFi \"%s\" (password %s), then open http://%s\r\n",
AP_SSID, AP_PASSWORD, WiFi.softAPIP().toString().c_str());
}
void publish(const Telemetry& t) {
portENTER_CRITICAL(&mux);
shared = t;
portEXIT_CRITICAL(&mux);
}
bool takeDrive(DriveInput& out) {
bool got = false;
portENTER_CRITICAL(&mux);
if (haveDrive) { out = pendingDrive; haveDrive = false; got = true; }
portEXIT_CRITICAL(&mux);
return got;
}
bool takeCommand(char* buf, size_t len) {
bool got = false;
portENTER_CRITICAL(&mux);
if (haveCmd) { strncpy(buf, pendingCmd, len - 1); buf[len - 1] = '\0'; haveCmd = false; got = true; }
portEXIT_CRITICAL(&mux);
return got;
}
bool takeSettings(Settings& out) {
bool got = false;
portENTER_CRITICAL(&mux);
if (haveSettings) { out = pendingSettings; haveSettings = false; got = true; }
portEXIT_CRITICAL(&mux);
return got;
}
uint8_t clients() { return WiFi.softAPgetStationNum(); }
} // namespace webui