esp32-claude-robbie/esp32_firmware/src/webserver.cpp

418 lines
12 KiB
C++

/**
* Claude's Eyes - Webserver Implementation
*
* REST API for controlling the robot
*/
#include "webserver.h"
#include "config.h"
#include "camera.h"
#include "motor_control.h"
#include "servo_control.h"
#include "ultrasonic.h"
#include "imu.h"
#include "display.h"
#include <WiFi.h>
// Global instance
WebServerModule WebServer;
WebServerModule::WebServerModule()
: _server(WEBSERVER_PORT)
, _claudeTextTimestamp(0)
, _claudeTextNew(false)
, _batteryPercent(100) // TODO: Implement battery reading
{
}
bool WebServerModule::begin() {
setupRoutes();
_server.begin();
Serial.printf("[WebServer] Started on port %d\n", WEBSERVER_PORT);
return true;
}
void WebServerModule::setupRoutes() {
// CORS preflight
_server.on("/*", HTTP_OPTIONS, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse(200);
response->addHeader("Access-Control-Allow-Origin", "*");
response->addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response->addHeader("Access-Control-Allow-Headers", "Content-Type");
request->send(response);
});
// GET /api/capture - Camera image
_server.on("/api/capture", HTTP_GET, [this](AsyncWebServerRequest* request) {
handleCapture(request);
});
// GET /api/status - Sensor data
_server.on("/api/status", HTTP_GET, [this](AsyncWebServerRequest* request) {
handleStatus(request);
});
// POST /api/command - Movement commands
_server.on("/api/command", HTTP_POST,
[](AsyncWebServerRequest* request) {},
NULL,
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
handleCommand(request, data, len);
}
);
// GET /api/claude_text - Get Claude's message
_server.on("/api/claude_text", HTTP_GET, [this](AsyncWebServerRequest* request) {
handleGetClaudeText(request);
});
// POST /api/claude_text - Set Claude's message
_server.on("/api/claude_text", HTTP_POST,
[](AsyncWebServerRequest* request) {},
NULL,
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
handlePostClaudeText(request, data, len);
}
);
// POST /api/display - Control display
_server.on("/api/display", HTTP_POST,
[](AsyncWebServerRequest* request) {},
NULL,
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
handleDisplay(request, data, len);
}
);
// Root - Simple status page
_server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
String html = "<!DOCTYPE html><html><head><title>Claude's Eyes</title>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:sans-serif;background:#1a1a2e;color:#eee;padding:20px;}";
html += "h1{color:#0ff;}a{color:#0ff;}</style></head><body>";
html += "<h1>Claude's Eyes</h1>";
html += "<p>Robot is online!</p>";
html += "<p>API Endpoints:</p><ul>";
html += "<li>GET /api/capture - Camera image</li>";
html += "<li>GET /api/status - Sensor data</li>";
html += "<li>POST /api/command - Movement</li>";
html += "<li>GET/POST /api/claude_text - Claude messages</li>";
html += "</ul>";
html += "<p><a href='/api/capture?key=" + String(API_KEY) + "'>View Camera</a></p>";
html += "</body></html>";
request->send(200, "text/html", html);
});
// 404 handler
_server.onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "application/json", "{\"error\":\"Not found\"}");
});
}
bool WebServerModule::checkAuth(AsyncWebServerRequest* request) {
if (!request->hasParam("key")) {
sendError(request, 401, "Missing API key");
return false;
}
String key = request->getParam("key")->value();
if (key != API_KEY) {
sendError(request, 403, "Invalid API key");
return false;
}
return true;
}
void WebServerModule::sendError(AsyncWebServerRequest* request, int code, const char* message) {
JsonDocument doc;
doc["error"] = message;
doc["code"] = code;
String response;
serializeJson(doc, response);
AsyncWebServerResponse* resp = request->beginResponse(code, "application/json", response);
resp->addHeader("Access-Control-Allow-Origin", "*");
request->send(resp);
}
void WebServerModule::sendJson(AsyncWebServerRequest* request, JsonDocument& doc) {
String response;
serializeJson(doc, response);
AsyncWebServerResponse* resp = request->beginResponse(200, "application/json", response);
resp->addHeader("Access-Control-Allow-Origin", "*");
request->send(resp);
}
// ============================================================================
// API Handlers
// ============================================================================
void WebServerModule::handleCapture(AsyncWebServerRequest* request) {
if (!checkAuth(request)) return;
// Check resolution parameter
if (request->hasParam("resolution")) {
String res = request->getParam("resolution")->value();
CameraResolution camRes = RES_VGA;
if (res == "QVGA") camRes = RES_QVGA;
else if (res == "VGA") camRes = RES_VGA;
else if (res == "SVGA") camRes = RES_SVGA;
else if (res == "XGA") camRes = RES_XGA;
else if (res == "SXGA") camRes = RES_SXGA;
else if (res == "UXGA") camRes = RES_UXGA;
Camera.setResolution(camRes);
}
// Check quality parameter
int quality = -1;
if (request->hasParam("quality")) {
quality = request->getParam("quality")->value().toInt();
}
// Capture frame
camera_fb_t* fb = Camera.capture(quality);
if (!fb) {
sendError(request, 500, Camera.getLastError());
return;
}
// Send image
AsyncWebServerResponse* response = request->beginResponse_P(
200, "image/jpeg", fb->buf, fb->len
);
response->addHeader("Access-Control-Allow-Origin", "*");
response->addHeader("Cache-Control", "no-cache");
request->send(response);
// Return frame buffer
Camera.returnFrame(fb);
}
void WebServerModule::handleStatus(AsyncWebServerRequest* request) {
if (!checkAuth(request)) return;
JsonDocument doc;
// Distance
doc["distance_cm"] = Ultrasonic.getLastDistance();
// Battery (placeholder)
doc["battery_percent"] = _batteryPercent;
// Current action
doc["current_action"] = Motors.getDirectionString();
// IMU data
const IMUData& imu = IMU.getData();
doc["imu"]["accel_x"] = imu.accel_x;
doc["imu"]["accel_y"] = imu.accel_y;
doc["imu"]["accel_z"] = imu.accel_z;
doc["imu"]["gyro_x"] = imu.gyro_x;
doc["imu"]["gyro_y"] = imu.gyro_y;
doc["imu"]["gyro_z"] = imu.gyro_z;
doc["imu"]["pitch"] = imu.pitch;
doc["imu"]["roll"] = imu.roll;
doc["imu"]["yaw"] = imu.yaw;
doc["imu"]["temperature"] = imu.temperature;
// WiFi
doc["wifi_rssi"] = WiFi.RSSI();
// Uptime
doc["uptime_seconds"] = getUptime();
// Servos
doc["servo_pan"] = Servos.getPan();
doc["servo_tilt"] = Servos.getTilt();
// Safety flags
doc["obstacle_warning"] = Ultrasonic.isWarning();
doc["obstacle_danger"] = Ultrasonic.isDanger();
doc["is_tilted"] = IMU.isTilted();
doc["is_moving"] = Motors.isMoving();
sendJson(request, doc);
}
void WebServerModule::handleCommand(AsyncWebServerRequest* request, uint8_t* data, size_t len) {
if (!checkAuth(request)) return;
// Parse JSON
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) {
sendError(request, 400, "Invalid JSON");
return;
}
String action = doc["action"] | "";
int speed = doc["speed"] | 50;
int duration = doc["duration_ms"] | 500;
JsonDocument response;
response["status"] = "ok";
response["executed"] = action;
// Handle movement commands
if (action == "forward") {
Motors.move(DIR_FORWARD, speed, duration);
response["message"] = "Moving forward";
}
else if (action == "backward") {
Motors.move(DIR_BACKWARD, speed, duration);
response["message"] = "Moving backward";
}
else if (action == "left") {
Motors.move(DIR_LEFT, speed, duration);
response["message"] = "Turning left";
}
else if (action == "right") {
Motors.move(DIR_RIGHT, speed, duration);
response["message"] = "Turning right";
}
else if (action == "stop") {
Motors.stop();
response["message"] = "Stopped";
}
// Servo commands
else if (action == "look_left") {
Servos.look(LOOK_LEFT);
response["message"] = "Looking left";
}
else if (action == "look_right") {
Servos.look(LOOK_RIGHT);
response["message"] = "Looking right";
}
else if (action == "look_up") {
Servos.look(LOOK_UP);
response["message"] = "Looking up";
}
else if (action == "look_down") {
Servos.look(LOOK_DOWN);
response["message"] = "Looking down";
}
else if (action == "look_center") {
Servos.look(LOOK_CENTER);
response["message"] = "Looking center";
}
else if (action == "look_custom") {
int pan = doc["pan"] | 90;
int tilt = doc["tilt"] | 90;
Servos.setPosition(pan, tilt);
response["message"] = "Custom look position";
}
else {
response["status"] = "error";
response["message"] = "Unknown action";
}
sendJson(request, response);
}
void WebServerModule::handleGetClaudeText(AsyncWebServerRequest* request) {
if (!checkAuth(request)) return;
JsonDocument doc;
doc["text"] = _claudeText;
doc["timestamp"] = _claudeTextTimestamp;
doc["new"] = _claudeTextNew;
// Mark as read
_claudeTextNew = false;
sendJson(request, doc);
}
void WebServerModule::handlePostClaudeText(AsyncWebServerRequest* request, uint8_t* data, size_t len) {
if (!checkAuth(request)) return;
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) {
sendError(request, 400, "Invalid JSON");
return;
}
String text = doc["text"] | "";
setClaudeText(text);
JsonDocument response;
response["status"] = "ok";
sendJson(request, response);
}
void WebServerModule::handleDisplay(AsyncWebServerRequest* request, uint8_t* data, size_t len) {
if (!checkAuth(request)) return;
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) {
sendError(request, 400, "Invalid JSON");
return;
}
String mode = doc["mode"] | "text";
String content = doc["content"] | "";
JsonDocument response;
response["status"] = "ok";
if (mode == "text") {
Display.showMessage(content.c_str());
response["message"] = "Text displayed";
}
else if (mode == "emoji") {
EmojiType emoji = EMOJI_HAPPY;
if (content == "happy") emoji = EMOJI_HAPPY;
else if (content == "thinking") emoji = EMOJI_THINKING;
else if (content == "surprised") emoji = EMOJI_SURPRISED;
else if (content == "sleepy") emoji = EMOJI_SLEEPY;
else if (content == "curious") emoji = EMOJI_CURIOUS;
else if (content == "confused") emoji = EMOJI_CONFUSED;
Display.showEmoji(emoji);
response["message"] = "Emoji displayed";
}
else if (mode == "status") {
Display.setMode(MODE_STATUS);
response["message"] = "Status mode";
}
else {
response["status"] = "error";
response["message"] = "Unknown mode";
}
sendJson(request, response);
}
void WebServerModule::setClaudeText(const String& text) {
_claudeText = text;
_claudeTextTimestamp = millis() / 1000;
_claudeTextNew = true;
// Also update display
Display.setClaudeText(text.c_str());
Serial.printf("[WebServer] Claude text set: %s\n", text.c_str());
}
bool WebServerModule::hasNewClaudeText() {
return _claudeTextNew;
}
const char* WebServerModule::getCurrentAction() {
return Motors.getDirectionString();
}
int WebServerModule::getWifiRssi() {
return WiFi.RSSI();
}