/** * 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 // 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 = "Claude's Eyes"; html += ""; html += ""; html += "

Claude's Eyes

"; html += "

Robot is online!

"; html += "

API Endpoints:

"; html += "

View Camera

"; 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(); }