182 lines
5.3 KiB
C++
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
|