bridge mit ready und statemachine system

This commit is contained in:
2025-12-27 01:31:45 +01:00
parent ed2964bbbf
commit 90707055ce
36 changed files with 2791 additions and 355 deletions
+223
View File
@@ -0,0 +1,223 @@
/**
* Claude's Eyes - Camera Module Implementation
*
* OV5640 with 120° wide-angle lens on Waveshare ESP32-S3-Touch-LCD-2
*/
#include "camera.h"
#include "config.h"
// Global instance
CameraModule Camera;
CameraModule::CameraModule()
: _initialized(false)
, _lastError(nullptr)
, _currentResolution(RES_VGA)
, _currentQuality(DEFAULT_JPEG_QUALITY)
{
}
bool CameraModule::begin() {
#if !ENABLE_CAMERA
_lastError = "Camera disabled in config";
return false;
#endif
// Camera configuration
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = CAMERA_PIN_SIOD;
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = CAMERA_XCLK_FREQ;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = DEFAULT_FRAME_SIZE;
config.jpeg_quality = _currentQuality;
config.fb_count = 2; // Double buffering for better performance
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_LATEST;
// Check for PSRAM
if (psramFound()) {
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
Serial.println("[Camera] PSRAM found, using double buffering");
} else {
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_DRAM;
Serial.println("[Camera] No PSRAM, using single buffer");
}
// Initialize camera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
_lastError = "Camera init failed";
Serial.printf("[Camera] Init failed with error 0x%x\n", err);
return false;
}
// Get sensor and apply settings
sensor_t* sensor = esp_camera_sensor_get();
if (sensor) {
// OV5640 specific settings
sensor->set_brightness(sensor, 0); // -2 to 2
sensor->set_contrast(sensor, 0); // -2 to 2
sensor->set_saturation(sensor, 0); // -2 to 2
sensor->set_special_effect(sensor, 0); // 0 = No Effect
sensor->set_whitebal(sensor, 1); // Auto white balance
sensor->set_awb_gain(sensor, 1); // Auto WB gain
sensor->set_wb_mode(sensor, 0); // Auto WB mode
sensor->set_exposure_ctrl(sensor, 1); // Auto exposure
sensor->set_aec2(sensor, 0); // AEC DSP
sensor->set_ae_level(sensor, 0); // AE level
sensor->set_aec_value(sensor, 300); // AEC value
sensor->set_gain_ctrl(sensor, 1); // Auto gain
sensor->set_agc_gain(sensor, 0); // AGC gain
sensor->set_gainceiling(sensor, (gainceiling_t)0); // Gain ceiling
sensor->set_bpc(sensor, 0); // Black pixel correction
sensor->set_wpc(sensor, 1); // White pixel correction
sensor->set_raw_gma(sensor, 1); // Raw gamma
sensor->set_lenc(sensor, 1); // Lens correction
sensor->set_hmirror(sensor, 0); // Horizontal mirror
sensor->set_vflip(sensor, 0); // Vertical flip
sensor->set_dcw(sensor, 1); // Downsize EN
sensor->set_colorbar(sensor, 0); // Color bar test
Serial.printf("[Camera] Sensor PID: 0x%x\n", sensor->id.PID);
}
_initialized = true;
Serial.println("[Camera] Initialized successfully");
return true;
}
camera_fb_t* CameraModule::capture(int quality) {
if (!_initialized) {
_lastError = "Camera not initialized";
return nullptr;
}
// Temporarily change quality if specified
if (quality > 0 && quality != _currentQuality) {
sensor_t* sensor = esp_camera_sensor_get();
if (sensor) {
sensor->set_quality(sensor, quality);
}
}
// Capture frame
camera_fb_t* fb = esp_camera_fb_get();
// Restore quality
if (quality > 0 && quality != _currentQuality) {
sensor_t* sensor = esp_camera_sensor_get();
if (sensor) {
sensor->set_quality(sensor, _currentQuality);
}
}
if (!fb) {
_lastError = "Frame capture failed";
Serial.println("[Camera] Frame capture failed");
return nullptr;
}
Serial.printf("[Camera] Captured frame: %dx%d, %d bytes\n",
fb->width, fb->height, fb->len);
return fb;
}
void CameraModule::returnFrame(camera_fb_t* fb) {
if (fb) {
esp_camera_fb_return(fb);
}
}
bool CameraModule::setResolution(CameraResolution resolution) {
if (!_initialized) {
_lastError = "Camera not initialized";
return false;
}
sensor_t* sensor = esp_camera_sensor_get();
if (!sensor) {
_lastError = "Could not get sensor";
return false;
}
framesize_t framesize = resolutionToFramesize(resolution);
if (sensor->set_framesize(sensor, framesize) != 0) {
_lastError = "Failed to set resolution";
return false;
}
_currentResolution = resolution;
Serial.printf("[Camera] Resolution set to %s\n", getResolutionString());
return true;
}
bool CameraModule::setQuality(int quality) {
if (quality < 10 || quality > 63) {
_lastError = "Quality must be 10-63";
return false;
}
if (!_initialized) {
_currentQuality = quality;
return true;
}
sensor_t* sensor = esp_camera_sensor_get();
if (!sensor) {
_lastError = "Could not get sensor";
return false;
}
if (sensor->set_quality(sensor, quality) != 0) {
_lastError = "Failed to set quality";
return false;
}
_currentQuality = quality;
Serial.printf("[Camera] Quality set to %d\n", quality);
return true;
}
const char* CameraModule::getResolutionString() {
switch (_currentResolution) {
case RES_QVGA: return "QVGA";
case RES_VGA: return "VGA";
case RES_SVGA: return "SVGA";
case RES_XGA: return "XGA";
case RES_SXGA: return "SXGA";
case RES_UXGA: return "UXGA";
default: return "Unknown";
}
}
framesize_t CameraModule::resolutionToFramesize(CameraResolution res) {
switch (res) {
case RES_QVGA: return FRAMESIZE_QVGA;
case RES_VGA: return FRAMESIZE_VGA;
case RES_SVGA: return FRAMESIZE_SVGA;
case RES_XGA: return FRAMESIZE_XGA;
case RES_SXGA: return FRAMESIZE_SXGA;
case RES_UXGA: return FRAMESIZE_UXGA;
default: return FRAMESIZE_VGA;
}
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Claude's Eyes - Camera Module Header
*
* Handles OV5640 camera initialization and image capture
*/
#ifndef CAMERA_H
#define CAMERA_H
#include <Arduino.h>
#include "esp_camera.h"
// Resolution enum for easy selection
enum CameraResolution {
RES_QVGA, // 320x240
RES_VGA, // 640x480
RES_SVGA, // 800x600
RES_XGA, // 1024x768
RES_SXGA, // 1280x1024
RES_UXGA // 1600x1200
};
class CameraModule {
public:
CameraModule();
/**
* Initialize the camera
* @return true if successful
*/
bool begin();
/**
* Capture a JPEG image
* @param quality JPEG quality (10-63, lower = better)
* @return Camera frame buffer (must be returned with returnFrame())
*/
camera_fb_t* capture(int quality = -1);
/**
* Return frame buffer after use
* @param fb Frame buffer to return
*/
void returnFrame(camera_fb_t* fb);
/**
* Set camera resolution
* @param resolution Resolution enum value
* @return true if successful
*/
bool setResolution(CameraResolution resolution);
/**
* Set JPEG quality
* @param quality Quality value (10-63, lower = better)
* @return true if successful
*/
bool setQuality(int quality);
/**
* Get current resolution as string
*/
const char* getResolutionString();
/**
* Check if camera is initialized
*/
bool isInitialized() { return _initialized; }
/**
* Get last error message
*/
const char* getLastError() { return _lastError; }
private:
bool _initialized;
const char* _lastError;
CameraResolution _currentResolution;
int _currentQuality;
framesize_t resolutionToFramesize(CameraResolution res);
};
// Global instance
extern CameraModule Camera;
#endif // CAMERA_H
+189
View File
@@ -0,0 +1,189 @@
/**
* Claude's Eyes - Configuration Header
*
* Hardware: Waveshare ESP32-S3-Touch-LCD-2 + Freenove 4WD Car Kit
*
* WICHTIG: Die GPIO-Pins für Motoren/Servos/Sensoren müssen noch
* verifiziert werden, sobald die Hardware da ist!
*/
#ifndef CONFIG_H
#define CONFIG_H
// ============================================================================
// WiFi Configuration
// ============================================================================
#define WIFI_SSID "DEIN_WLAN_NAME"
#define WIFI_PASSWORD "DEIN_WLAN_PASSWORT"
// API Security
#define API_KEY "claudes_eyes_secret_2025"
// Webserver Port
#define WEBSERVER_PORT 80
// ============================================================================
// Camera Configuration (OV5640 on Waveshare ESP32-S3-Touch-LCD-2)
// ============================================================================
// 24-Pin Camera Interface on Waveshare board
#define CAMERA_PIN_PWDN -1 // Power down not used
#define CAMERA_PIN_RESET -1 // Reset not used
#define CAMERA_PIN_XCLK 15 // External clock
#define CAMERA_PIN_SIOD 4 // I2C SDA (SCCB)
#define CAMERA_PIN_SIOC 5 // I2C SCL (SCCB)
#define CAMERA_PIN_D7 16 // Data pins
#define CAMERA_PIN_D6 17
#define CAMERA_PIN_D5 18
#define CAMERA_PIN_D4 12 // Note: Might conflict with TFT_RST
#define CAMERA_PIN_D3 10 // Note: Might conflict with TFT_CS
#define CAMERA_PIN_D2 8
#define CAMERA_PIN_D1 9
#define CAMERA_PIN_D0 11 // Note: Might conflict with TFT_DC
#define CAMERA_PIN_VSYNC 6 // Vertical sync
#define CAMERA_PIN_HREF 7 // Horizontal reference
#define CAMERA_PIN_PCLK 13 // Pixel clock
// Camera settings
#define CAMERA_XCLK_FREQ 20000000 // 20MHz external clock
#define DEFAULT_JPEG_QUALITY 12 // 10-63, lower = better quality
#define DEFAULT_FRAME_SIZE FRAMESIZE_VGA // 640x480
// ============================================================================
// Motor Configuration (Freenove 4WD Car Kit)
// ============================================================================
// ACHTUNG: Diese Pins müssen noch mit dem Freenove Shield abgeglichen werden!
// Der Freenove Shield verwendet wahrscheinlich TB6612 oder L298N Motortreiber
// Motor A (linke Seite - 2 Motoren parallel)
#define MOTOR_A_IN1 39 // Direction pin 1
#define MOTOR_A_IN2 40 // Direction pin 2
#define MOTOR_A_PWM 41 // Speed control (PWM)
// Motor B (rechte Seite - 2 Motoren parallel)
#define MOTOR_B_IN1 42 // Direction pin 1
#define MOTOR_B_IN2 2 // Direction pin 2
#define MOTOR_B_PWM 1 // Speed control (PWM)
// PWM Configuration
#define MOTOR_PWM_FREQ 5000 // 5kHz PWM frequency
#define MOTOR_PWM_RESOLUTION 8 // 8-bit resolution (0-255)
#define MOTOR_PWM_CHANNEL_A 0 // LEDC channel for motor A
#define MOTOR_PWM_CHANNEL_B 1 // LEDC channel for motor B
// Speed limits
#define MOTOR_MAX_SPEED 255 // Maximum PWM value
#define MOTOR_MIN_SPEED 50 // Minimum PWM to overcome friction
#define DEFAULT_SPEED 150 // Default driving speed
// ============================================================================
// Servo Configuration (Pan/Tilt for Camera)
// ============================================================================
// Servos vom Freenove Kit
#define SERVO_PAN_PIN 38 // Horizontal rotation
#define SERVO_TILT_PIN 37 // Vertical rotation
// Servo limits (in degrees)
#define SERVO_PAN_MIN 0
#define SERVO_PAN_MAX 180
#define SERVO_PAN_CENTER 90
#define SERVO_TILT_MIN 30 // Don't go too far down
#define SERVO_TILT_MAX 150 // Don't go too far up
#define SERVO_TILT_CENTER 90
// Servo movement speed (degrees per step for smooth movement)
#define SERVO_STEP_DELAY_MS 15 // Delay between steps
#define SERVO_STEP_SIZE 2 // Degrees per step
// ============================================================================
// Ultrasonic Sensor (HC-SR04)
// ============================================================================
#define ULTRASONIC_TRIGGER_PIN 21
#define ULTRASONIC_ECHO_PIN 47
// Distance thresholds (in cm)
#define OBSTACLE_DANGER_DISTANCE 15 // Stop immediately
#define OBSTACLE_WARNING_DISTANCE 30 // Slow down
#define ULTRASONIC_MAX_DISTANCE 400 // Maximum measurable distance
// ============================================================================
// Display Configuration (Built-in on Waveshare)
// ============================================================================
// ST7789 240x320 display - pins defined in platformio.ini via TFT_eSPI
#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320
#define DISPLAY_ROTATION 0 // 0, 1, 2, or 3
// Backlight
#define DISPLAY_BL_PIN 45
#define DISPLAY_BL_PWM_CHANNEL 2
// Colors (RGB565)
#define COLOR_BACKGROUND 0x0000 // Black
#define COLOR_TEXT 0xFFFF // White
#define COLOR_ACCENT 0x07FF // Cyan
#define COLOR_WARNING 0xFD20 // Orange
#define COLOR_DANGER 0xF800 // Red
#define COLOR_SUCCESS 0x07E0 // Green
// ============================================================================
// Touch Configuration (CST816S on Waveshare)
// ============================================================================
#define TOUCH_SDA_PIN 48
#define TOUCH_SCL_PIN 8
#define TOUCH_INT_PIN 3
#define TOUCH_RST_PIN -1 // Not connected
// ============================================================================
// IMU Configuration (QMI8658 on Waveshare)
// ============================================================================
#define IMU_SDA_PIN 48 // Shared with touch
#define IMU_SCL_PIN 8 // Shared with touch
#define IMU_INT1_PIN -1 // Not used
#define IMU_INT2_PIN -1 // Not used
// ============================================================================
// LED Matrix (Optional - from Freenove Kit)
// ============================================================================
#define LED_MATRIX_DIN_PIN 35
#define LED_MATRIX_CLK_PIN 36
#define LED_MATRIX_CS_PIN 0
// ============================================================================
// RGB LEDs (Optional - from Freenove Kit)
// ============================================================================
#define RGB_LED_PIN 48 // WS2812 data pin
#define RGB_LED_COUNT 4 // Number of LEDs
// ============================================================================
// System Configuration
// ============================================================================
// Task intervals (in milliseconds)
#define SENSOR_UPDATE_INTERVAL 100 // Read sensors every 100ms
#define DISPLAY_UPDATE_INTERVAL 250 // Update display 4x per second
#define STATUS_BROADCAST_INTERVAL 1000 // Broadcast status every second
// Safety
#define COMMAND_TIMEOUT_MS 5000 // Stop if no command for 5 seconds
#define MAX_CONTINUOUS_DRIVE_MS 30000 // Max 30 seconds continuous driving
// Debug
#define DEBUG_SERIAL true // Enable serial debug output
#define DEBUG_BAUD_RATE 115200
// ============================================================================
// Feature Toggles
// ============================================================================
#define ENABLE_CAMERA true
#define ENABLE_MOTORS true
#define ENABLE_SERVOS true
#define ENABLE_ULTRASONIC true
#define ENABLE_IMU true
#define ENABLE_DISPLAY true
#define ENABLE_TOUCH true
#define ENABLE_LED_MATRIX false // Disabled by default
#define ENABLE_RGB_LEDS false // Disabled by default
#endif // CONFIG_H
+414
View File
@@ -0,0 +1,414 @@
/**
* Claude's Eyes - Display Module Implementation
*
* ST7789 TFT with CST816S capacitive touch
*/
#include "display.h"
#include "config.h"
// Global instance
DisplayModule Display;
DisplayModule::DisplayModule()
: _touch(TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_RST_PIN, TOUCH_INT_PIN)
, _initialized(false)
, _currentMode(MODE_STATUS)
, _lastUpdateTime(0)
, _scrollOffset(0)
, _distance(0)
, _battery(100)
, _wifiRssi(0)
{
}
bool DisplayModule::begin() {
#if !ENABLE_DISPLAY
Serial.println("[Display] Disabled in config");
return false;
#endif
// Initialize TFT
_tft.init();
_tft.setRotation(DISPLAY_ROTATION);
_tft.fillScreen(COLOR_BACKGROUND);
// Configure backlight PWM
ledcSetup(DISPLAY_BL_PWM_CHANNEL, 5000, 8);
ledcAttachPin(DISPLAY_BL_PIN, DISPLAY_BL_PWM_CHANNEL);
setBrightness(200); // Start at ~80%
#if ENABLE_TOUCH
// Initialize touch
_touch.begin();
Serial.println("[Display] Touch initialized");
#endif
// Show startup screen
_tft.setTextColor(COLOR_ACCENT, COLOR_BACKGROUND);
_tft.setTextSize(2);
_tft.setTextDatum(MC_DATUM);
_tft.drawString("Claude's Eyes", DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2 - 20);
_tft.setTextSize(1);
_tft.setTextColor(TFT_WHITE, COLOR_BACKGROUND);
_tft.drawString("Initializing...", DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2 + 20);
_initialized = true;
Serial.println("[Display] Initialized successfully");
return true;
}
void DisplayModule::update() {
if (!_initialized) return;
unsigned long now = millis();
if (now - _lastUpdateTime < DISPLAY_UPDATE_INTERVAL) {
return;
}
_lastUpdateTime = now;
switch (_currentMode) {
case MODE_STATUS:
drawStatusScreen();
break;
case MODE_CONTROLS:
drawControlsScreen();
break;
default:
break;
}
}
void DisplayModule::setMode(DisplayMode mode) {
if (mode == _currentMode) return;
_currentMode = mode;
_tft.fillScreen(COLOR_BACKGROUND);
Serial.printf("[Display] Mode changed to %d\n", mode);
}
void DisplayModule::showEmoji(EmojiType emoji) {
setMode(MODE_EMOJI);
drawEmojiScreen(emoji);
}
void DisplayModule::showMessage(const char* message, uint16_t color) {
setMode(MODE_MESSAGE);
drawMessageScreen(message, color);
}
void DisplayModule::updateStatus(const char* action, float distance,
int battery, int wifiRssi) {
_currentAction = action;
_distance = distance;
_battery = battery;
_wifiRssi = wifiRssi;
if (_currentMode == MODE_STATUS) {
// Will be drawn on next update()
}
}
void DisplayModule::setClaudeText(const char* text) {
_claudeText = text;
_scrollOffset = 0;
}
TouchButton DisplayModule::checkTouch() {
#if !ENABLE_TOUCH
return BTN_NONE;
#endif
if (!_touch.available()) {
return BTN_NONE;
}
int x = _touch.data.x;
int y = _touch.data.y;
// Adjust for rotation if needed
// x and y are in display coordinates
if (_currentMode == MODE_CONTROLS) {
// Control layout:
// [ STOP ] (top, full width)
// [ ^ ] (forward)
// [< HOME >] (left, home, right)
// [ v ] (backward)
// Stop button (top 60 pixels)
if (y < 60) {
return BTN_STOP;
}
// Forward (y 60-120, center)
if (y >= 60 && y < 120 && x >= 80 && x < 160) {
return BTN_FORWARD;
}
// Middle row (y 120-200)
if (y >= 120 && y < 200) {
if (x < 80) return BTN_LEFT;
if (x >= 160) return BTN_RIGHT;
return BTN_HOME;
}
// Backward (y 200-260, center)
if (y >= 200 && y < 260 && x >= 80 && x < 160) {
return BTN_BACKWARD;
}
}
// In status mode, any touch switches to controls
if (_currentMode == MODE_STATUS) {
setMode(MODE_CONTROLS);
}
return BTN_NONE;
}
void DisplayModule::setBrightness(uint8_t brightness) {
ledcWrite(DISPLAY_BL_PWM_CHANNEL, brightness);
}
// ============================================================================
// Drawing Functions
// ============================================================================
void DisplayModule::drawStatusScreen() {
// Header bar with icons
_tft.fillRect(0, 0, DISPLAY_WIDTH, 30, TFT_DARKGREY);
drawWifiIcon(5, 5, _wifiRssi);
drawBatteryIcon(DISPLAY_WIDTH - 35, 5, _battery);
// Current action (large, centered)
_tft.setTextColor(COLOR_ACCENT, COLOR_BACKGROUND);
_tft.setTextSize(3);
_tft.setTextDatum(TC_DATUM);
// Action icons
const char* actionIcon = "?";
if (_currentAction == "idle" || _currentAction == "stop") actionIcon = "STOP";
else if (_currentAction == "forward") actionIcon = "^";
else if (_currentAction == "backward") actionIcon = "v";
else if (_currentAction == "left") actionIcon = "<";
else if (_currentAction == "right") actionIcon = ">";
_tft.fillRect(0, 50, DISPLAY_WIDTH, 60, COLOR_BACKGROUND);
_tft.drawString(actionIcon, DISPLAY_WIDTH / 2, 70);
// Distance indicator
_tft.setTextSize(1);
_tft.setTextColor(TFT_WHITE, COLOR_BACKGROUND);
_tft.setTextDatum(TL_DATUM);
_tft.fillRect(0, 130, DISPLAY_WIDTH, 20, COLOR_BACKGROUND);
char distStr[32];
snprintf(distStr, sizeof(distStr), "Distance: %.0f cm", _distance);
_tft.drawString(distStr, 10, 130);
// Distance bar
int barWidth = map(constrain(_distance, 0, 100), 0, 100, DISPLAY_WIDTH - 20, 0);
uint16_t barColor = _distance > OBSTACLE_WARNING_DISTANCE ? COLOR_SUCCESS :
_distance > OBSTACLE_DANGER_DISTANCE ? COLOR_WARNING : COLOR_DANGER;
_tft.fillRect(10, 150, DISPLAY_WIDTH - 20, 10, TFT_DARKGREY);
_tft.fillRect(10, 150, barWidth, 10, barColor);
// Claude's text (scrolling)
_tft.fillRect(0, 180, DISPLAY_WIDTH, DISPLAY_HEIGHT - 180, TFT_DARKGREY);
_tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
_tft.setTextSize(1);
_tft.setTextDatum(TL_DATUM);
if (_claudeText.length() > 0) {
// Word wrap and display
String displayText = _claudeText;
int y = 185;
int lineHeight = 15;
int maxCharsPerLine = 38;
while (displayText.length() > 0 && y < DISPLAY_HEIGHT - 10) {
String line = displayText.substring(0, min((int)displayText.length(), maxCharsPerLine));
_tft.drawString(line.c_str(), 5, y);
displayText = displayText.substring(line.length());
y += lineHeight;
}
}
// Touch hint
_tft.setTextDatum(BC_DATUM);
_tft.setTextColor(TFT_DARKGREY, COLOR_BACKGROUND);
_tft.drawString("Touch for controls", DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 5);
}
void DisplayModule::drawControlsScreen() {
// Stop button (red, top)
_tft.fillRect(10, 10, DISPLAY_WIDTH - 20, 50, COLOR_DANGER);
_tft.setTextColor(TFT_WHITE, COLOR_DANGER);
_tft.setTextSize(2);
_tft.setTextDatum(MC_DATUM);
_tft.drawString("STOP", DISPLAY_WIDTH / 2, 35);
// Forward button
_tft.fillRect(80, 70, 80, 50, TFT_DARKGREY);
_tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
_tft.drawString("^", 120, 95);
// Left button
_tft.fillRect(10, 130, 60, 60, TFT_DARKGREY);
_tft.drawString("<", 40, 160);
// Home button
_tft.fillRect(80, 130, 80, 60, COLOR_ACCENT);
_tft.setTextColor(TFT_BLACK, COLOR_ACCENT);
_tft.drawString("HOME", 120, 160);
// Right button
_tft.fillRect(170, 130, 60, 60, TFT_DARKGREY);
_tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
_tft.drawString(">", 200, 160);
// Backward button
_tft.fillRect(80, 200, 80, 50, TFT_DARKGREY);
_tft.drawString("v", 120, 225);
// Back to status hint
_tft.setTextSize(1);
_tft.setTextDatum(BC_DATUM);
_tft.setTextColor(TFT_DARKGREY, COLOR_BACKGROUND);
_tft.drawString("Auto-return to status", DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 5);
}
void DisplayModule::drawEmojiScreen(EmojiType emoji) {
int cx = DISPLAY_WIDTH / 2;
int cy = DISPLAY_HEIGHT / 2;
int radius = 60;
switch (emoji) {
case EMOJI_HAPPY:
drawHappyFace(cx, cy, radius);
break;
case EMOJI_THINKING:
drawThinkingFace(cx, cy, radius);
break;
case EMOJI_SURPRISED:
drawSurprisedFace(cx, cy, radius);
break;
case EMOJI_SLEEPY:
drawSleepyFace(cx, cy, radius);
break;
case EMOJI_CURIOUS:
drawCuriousFace(cx, cy, radius);
break;
case EMOJI_CONFUSED:
drawConfusedFace(cx, cy, radius);
break;
}
}
void DisplayModule::drawMessageScreen(const char* message, uint16_t color) {
_tft.setTextColor(color, COLOR_BACKGROUND);
_tft.setTextSize(2);
_tft.setTextDatum(MC_DATUM);
_tft.drawString(message, DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2);
}
void DisplayModule::drawWifiIcon(int x, int y, int rssi) {
uint16_t color = rssi > -50 ? COLOR_SUCCESS :
rssi > -70 ? COLOR_WARNING : COLOR_DANGER;
// Simple 3-bar WiFi icon
int bars = rssi > -50 ? 3 : rssi > -70 ? 2 : rssi > -90 ? 1 : 0;
for (int i = 0; i < 3; i++) {
uint16_t barColor = (i < bars) ? color : TFT_DARKGREY;
_tft.fillRect(x + i * 8, y + (2 - i) * 6, 6, 6 + i * 6, barColor);
}
}
void DisplayModule::drawBatteryIcon(int x, int y, int percent) {
uint16_t color = percent > 50 ? COLOR_SUCCESS :
percent > 20 ? COLOR_WARNING : COLOR_DANGER;
// Battery outline
_tft.drawRect(x, y, 28, 16, TFT_WHITE);
_tft.fillRect(x + 28, y + 4, 3, 8, TFT_WHITE);
// Fill level
int fillWidth = map(percent, 0, 100, 0, 24);
_tft.fillRect(x + 2, y + 2, fillWidth, 12, color);
}
// Emoji faces
void DisplayModule::drawHappyFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
_tft.fillCircle(cx - 20, cy - 15, 8, TFT_BLACK); // Left eye
_tft.fillCircle(cx + 20, cy - 15, 8, TFT_BLACK); // Right eye
// Smile arc
for (int i = -30; i <= 30; i++) {
int px = cx + i;
int py = cy + 20 + (i * i) / 50;
_tft.fillCircle(px, py, 3, TFT_BLACK);
}
}
void DisplayModule::drawThinkingFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
_tft.fillCircle(cx - 20, cy - 15, 8, TFT_BLACK); // Left eye
_tft.fillCircle(cx + 20, cy - 15, 8, TFT_BLACK); // Right eye
// Raised eyebrow (right)
_tft.drawLine(cx + 10, cy - 30, cx + 35, cy - 35, TFT_BLACK);
// Thinking mouth (small line)
_tft.drawLine(cx - 15, cy + 25, cx + 15, cy + 25, TFT_BLACK);
}
void DisplayModule::drawSurprisedFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
_tft.fillCircle(cx - 20, cy - 15, 12, TFT_BLACK); // Left eye (big)
_tft.fillCircle(cx + 20, cy - 15, 12, TFT_BLACK); // Right eye (big)
// O mouth
_tft.fillCircle(cx, cy + 25, 15, TFT_BLACK);
}
void DisplayModule::drawSleepyFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
// Closed eyes (lines)
_tft.drawLine(cx - 30, cy - 15, cx - 10, cy - 15, TFT_BLACK);
_tft.drawLine(cx + 10, cy - 15, cx + 30, cy - 15, TFT_BLACK);
// Small mouth
_tft.drawLine(cx - 10, cy + 25, cx + 10, cy + 25, TFT_BLACK);
// Z's
_tft.setTextColor(TFT_BLACK, TFT_YELLOW);
_tft.setTextSize(2);
_tft.drawString("z", cx + 45, cy - 40);
_tft.setTextSize(1);
_tft.drawString("z", cx + 55, cy - 50);
}
void DisplayModule::drawCuriousFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
_tft.fillCircle(cx - 20, cy - 15, 8, TFT_BLACK); // Left eye
_tft.fillCircle(cx + 20, cy - 15, 10, TFT_BLACK); // Right eye (bigger)
// Raised eyebrow
_tft.drawLine(cx + 10, cy - 30, cx + 35, cy - 25, TFT_BLACK);
// Small smile
_tft.drawLine(cx - 10, cy + 25, cx + 15, cy + 20, TFT_BLACK);
}
void DisplayModule::drawConfusedFace(int cx, int cy, int r) {
_tft.fillCircle(cx, cy, r, TFT_YELLOW);
_tft.fillCircle(cx - 20, cy - 15, 8, TFT_BLACK); // Left eye
_tft.fillCircle(cx + 20, cy - 15, 8, TFT_BLACK); // Right eye
// Squiggly eyebrows
for (int i = 0; i < 20; i++) {
int py = cy - 30 + sin(i * 0.5) * 3;
_tft.drawPixel(cx - 30 + i, py, TFT_BLACK);
_tft.drawPixel(cx + 10 + i, py, TFT_BLACK);
}
// Wavy mouth
for (int i = -15; i <= 15; i++) {
int py = cy + 25 + sin(i * 0.3) * 5;
_tft.fillCircle(cx + i, py, 2, TFT_BLACK);
}
}
+143
View File
@@ -0,0 +1,143 @@
/**
* Claude's Eyes - Display Module Header
*
* ST7789 240x320 TFT with CST816S touch on Waveshare ESP32-S3
*/
#ifndef DISPLAY_H
#define DISPLAY_H
#include <Arduino.h>
#include <TFT_eSPI.h>
#include <CST816S.h>
// Display modes
enum DisplayMode {
MODE_STATUS, // Show status information
MODE_EMOJI, // Show emoji/expression
MODE_MESSAGE, // Show text message
MODE_CONTROLS // Show touch controls
};
// Emoji types for expressions
enum EmojiType {
EMOJI_HAPPY,
EMOJI_THINKING,
EMOJI_SURPRISED,
EMOJI_SLEEPY,
EMOJI_CURIOUS,
EMOJI_CONFUSED
};
// Touch button IDs
enum TouchButton {
BTN_NONE = 0,
BTN_STOP, // Emergency stop
BTN_FORWARD,
BTN_BACKWARD,
BTN_LEFT,
BTN_RIGHT,
BTN_HOME // Return to base
};
class DisplayModule {
public:
DisplayModule();
/**
* Initialize display and touch
* @return true if successful
*/
bool begin();
/**
* Update display (call from loop)
*/
void update();
/**
* Set display mode
*/
void setMode(DisplayMode mode);
/**
* Show emoji expression
*/
void showEmoji(EmojiType emoji);
/**
* Show text message
* @param message Text to display
* @param color Text color (default white)
*/
void showMessage(const char* message, uint16_t color = TFT_WHITE);
/**
* Update status display
* @param action Current action string
* @param distance Distance to obstacle (cm)
* @param battery Battery percentage
* @param wifiRssi WiFi signal strength
*/
void updateStatus(const char* action, float distance,
int battery, int wifiRssi);
/**
* Set Claude's last message (scrolling text)
*/
void setClaudeText(const char* text);
/**
* Check for touch input
* @return Button ID or BTN_NONE
*/
TouchButton checkTouch();
/**
* Set backlight brightness
* @param brightness 0-255
*/
void setBrightness(uint8_t brightness);
/**
* Get display object for direct access
*/
TFT_eSPI& getTFT() { return _tft; }
private:
TFT_eSPI _tft;
CST816S _touch;
bool _initialized;
DisplayMode _currentMode;
unsigned long _lastUpdateTime;
int _scrollOffset;
String _claudeText;
// Status values
String _currentAction;
float _distance;
int _battery;
int _wifiRssi;
// Drawing functions
void drawStatusScreen();
void drawEmojiScreen(EmojiType emoji);
void drawMessageScreen(const char* message, uint16_t color);
void drawControlsScreen();
void drawWifiIcon(int x, int y, int rssi);
void drawBatteryIcon(int x, int y, int percent);
// Emoji drawing
void drawHappyFace(int centerX, int centerY, int radius);
void drawThinkingFace(int centerX, int centerY, int radius);
void drawSurprisedFace(int centerX, int centerY, int radius);
void drawSleepyFace(int centerX, int centerY, int radius);
void drawCuriousFace(int centerX, int centerY, int radius);
void drawConfusedFace(int centerX, int centerY, int radius);
};
// Global instance
extern DisplayModule Display;
#endif // DISPLAY_H
+150
View File
@@ -0,0 +1,150 @@
/**
* Claude's Eyes - IMU Module Implementation
*
* QMI8658 6-axis IMU using SensorLib
*/
#include "imu.h"
#include "config.h"
#include <Wire.h>
#include <SensorQMI8658.hpp>
// Global instance
IMUModule IMU;
// Sensor instance
static SensorQMI8658 qmi;
IMUModule::IMUModule()
: _initialized(false)
, _lastError(nullptr)
, _lastUpdateTime(0)
{
memset(&_data, 0, sizeof(_data));
}
bool IMUModule::begin() {
#if !ENABLE_IMU
Serial.println("[IMU] Disabled in config");
return false;
#endif
// Initialize I2C (shared with touch on Waveshare board)
Wire.begin(IMU_SDA_PIN, IMU_SCL_PIN);
// Initialize QMI8658
if (!qmi.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IMU_SDA_PIN, IMU_SCL_PIN)) {
// Try alternate address
if (!qmi.begin(Wire, QMI8658_H_SLAVE_ADDRESS, IMU_SDA_PIN, IMU_SCL_PIN)) {
_lastError = "QMI8658 not found";
Serial.println("[IMU] QMI8658 not found on I2C");
return false;
}
}
Serial.printf("[IMU] QMI8658 found, Chip ID: 0x%02X\n", qmi.getChipID());
// Configure accelerometer
// Range: ±8g, ODR: 896.8Hz
qmi.configAccelerometer(
SensorQMI8658::ACC_RANGE_8G,
SensorQMI8658::ACC_ODR_896_8Hz,
SensorQMI8658::LPF_MODE_0
);
// Configure gyroscope
// Range: ±512dps, ODR: 896.8Hz
qmi.configGyroscope(
SensorQMI8658::GYR_RANGE_512DPS,
SensorQMI8658::GYR_ODR_896_8Hz,
SensorQMI8658::LPF_MODE_0
);
// Enable sensors
qmi.enableAccelerometer();
qmi.enableGyroscope();
_initialized = true;
Serial.println("[IMU] Initialized successfully");
// Take initial reading
update();
return true;
}
void IMUModule::update() {
if (!_initialized) return;
unsigned long now = millis();
if (now - _lastUpdateTime < UPDATE_INTERVAL) {
return;
}
float dt = (now - _lastUpdateTime) / 1000.0f;
_lastUpdateTime = now;
// Read sensor data
if (qmi.getDataReady()) {
float ax, ay, az;
float gx, gy, gz;
if (qmi.getAccelerometer(ax, ay, az)) {
_data.accel_x = ax;
_data.accel_y = ay;
_data.accel_z = az;
}
if (qmi.getGyroscope(gx, gy, gz)) {
_data.gyro_x = gx;
_data.gyro_y = gy;
_data.gyro_z = gz;
}
_data.temperature = qmi.getTemperature_C();
// Compute orientation angles
computeAngles();
// Integrate yaw from gyroscope
if (dt > 0 && dt < 1.0f) { // Sanity check
_data.yaw += _data.gyro_z * dt;
// Keep yaw in -180 to 180 range
while (_data.yaw > 180.0f) _data.yaw -= 360.0f;
while (_data.yaw < -180.0f) _data.yaw += 360.0f;
}
}
}
void IMUModule::computeAngles() {
// Calculate pitch and roll from accelerometer
// Assumes sensor is mounted level when robot is flat
// Pitch: rotation around X axis
// tan(pitch) = -ax / sqrt(ay² + az²)
_data.pitch = atan2(-_data.accel_x,
sqrt(_data.accel_y * _data.accel_y +
_data.accel_z * _data.accel_z)) * 180.0f / PI;
// Roll: rotation around Y axis
// tan(roll) = ay / az
_data.roll = atan2(_data.accel_y, _data.accel_z) * 180.0f / PI;
}
bool IMUModule::isTilted() {
// Check if tilted more than 30 degrees in any direction
const float TILT_THRESHOLD = 30.0f;
return (abs(_data.pitch) > TILT_THRESHOLD ||
abs(_data.roll) > TILT_THRESHOLD);
}
bool IMUModule::isUpsideDown() {
// Z-axis should be positive (pointing up) when right-side up
// If Z is negative, robot is flipped
return _data.accel_z < -5.0f; // Threshold accounts for noise
}
void IMUModule::resetYaw() {
_data.yaw = 0;
}
+92
View File
@@ -0,0 +1,92 @@
/**
* Claude's Eyes - IMU Module Header
*
* QMI8658 6-axis IMU (accelerometer + gyroscope) on Waveshare board
*/
#ifndef IMU_H
#define IMU_H
#include <Arduino.h>
// IMU data structure
struct IMUData {
// Accelerometer (m/s²)
float accel_x;
float accel_y;
float accel_z;
// Gyroscope (degrees/s)
float gyro_x;
float gyro_y;
float gyro_z;
// Computed values
float pitch; // Rotation around X axis (degrees)
float roll; // Rotation around Y axis (degrees)
float yaw; // Rotation around Z axis (degrees, integrated from gyro)
// Temperature
float temperature;
};
class IMUModule {
public:
IMUModule();
/**
* Initialize the IMU
* @return true if successful
*/
bool begin();
/**
* Update IMU readings
*/
void update();
/**
* Get current IMU data
*/
const IMUData& getData() { return _data; }
/**
* Check if robot is tilted beyond safe angle
*/
bool isTilted();
/**
* Check if robot is upside down
*/
bool isUpsideDown();
/**
* Reset yaw integration
*/
void resetYaw();
/**
* Check if initialized
*/
bool isInitialized() { return _initialized; }
/**
* Get last error
*/
const char* getLastError() { return _lastError; }
private:
bool _initialized;
const char* _lastError;
IMUData _data;
unsigned long _lastUpdateTime;
static const unsigned long UPDATE_INTERVAL = 20; // 50Hz update rate
void computeAngles();
};
// Global instance
extern IMUModule IMU;
#endif // IMU_H
+302
View File
@@ -0,0 +1,302 @@
/**
* Claude's Eyes - Main Program
*
* Autonomous exploration robot controlled by Claude AI
*
* Hardware:
* - Waveshare ESP32-S3-Touch-LCD-2
* - OV5640 Camera (120° wide-angle)
* - Freenove 4WD Car Kit
*
* Created: December 2025
* By: Stefan (HackerSoft) & Claude (Anthropic)
*/
#include <Arduino.h>
#include <WiFi.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 "webserver.h"
// State machine
enum RobotState {
STATE_INITIALIZING,
STATE_CONNECTING,
STATE_READY,
STATE_EXPLORING,
STATE_OBSTACLE,
STATE_ERROR
};
RobotState currentState = STATE_INITIALIZING;
unsigned long lastCommandTime = 0;
unsigned long lastStatusUpdate = 0;
// Forward declarations
void setupWiFi();
void handleSafety();
void handleTouchInput();
void updateStatusDisplay();
void setup() {
// Initialize serial
Serial.begin(DEBUG_BAUD_RATE);
while (!Serial && millis() < 3000) {
delay(10);
}
Serial.println("\n\n");
Serial.println("╔═══════════════════════════════════════╗");
Serial.println("║ Claude's Eyes v1.0 ║");
Serial.println("║ Autonomous Exploration Robot ║");
Serial.println("╚═══════════════════════════════════════╝");
Serial.println();
// Initialize display first (for visual feedback)
Serial.println("[Main] Initializing display...");
if (Display.begin()) {
Display.showMessage("Starting...", TFT_CYAN);
}
delay(500);
// Initialize camera
Serial.println("[Main] Initializing camera...");
if (!Camera.begin()) {
Serial.println("[Main] WARNING: Camera init failed!");
Display.showMessage("Camera Error!", TFT_RED);
delay(2000);
} else {
Serial.println("[Main] Camera ready");
}
// Initialize motors
Serial.println("[Main] Initializing motors...");
if (!Motors.begin()) {
Serial.println("[Main] WARNING: Motors init failed!");
} else {
Serial.println("[Main] Motors ready");
}
// Initialize servos
Serial.println("[Main] Initializing servos...");
if (!Servos.begin()) {
Serial.println("[Main] WARNING: Servos init failed!");
} else {
Serial.println("[Main] Servos ready");
// Center the camera
Servos.center();
}
// Initialize ultrasonic sensor
Serial.println("[Main] Initializing ultrasonic sensor...");
if (!Ultrasonic.begin()) {
Serial.println("[Main] WARNING: Ultrasonic init failed!");
} else {
Serial.println("[Main] Ultrasonic ready");
}
// Initialize IMU
Serial.println("[Main] Initializing IMU...");
if (!IMU.begin()) {
Serial.println("[Main] WARNING: IMU init failed!");
} else {
Serial.println("[Main] IMU ready");
}
// Connect to WiFi
currentState = STATE_CONNECTING;
Display.showMessage("Connecting WiFi...", TFT_YELLOW);
setupWiFi();
// Start webserver
Serial.println("[Main] Starting webserver...");
if (!WebServer.begin()) {
Serial.println("[Main] ERROR: WebServer failed!");
currentState = STATE_ERROR;
Display.showMessage("Server Error!", TFT_RED);
return;
}
// All systems go!
currentState = STATE_READY;
Display.setMode(MODE_STATUS);
Display.showEmoji(EMOJI_HAPPY);
delay(1000);
Display.setMode(MODE_STATUS);
Serial.println();
Serial.println("╔═══════════════════════════════════════╗");
Serial.println("║ SYSTEM READY ║");
Serial.printf( "║ IP: %-30s ║\n", WiFi.localIP().toString().c_str());
Serial.println("║ Waiting for Claude... ║");
Serial.println("╚═══════════════════════════════════════╝");
Serial.println();
lastCommandTime = millis();
lastStatusUpdate = millis();
}
void loop() {
unsigned long now = millis();
// Update sensors
Ultrasonic.update();
IMU.update();
Servos.update();
Motors.update();
// Handle safety checks
handleSafety();
// Handle touch input
handleTouchInput();
// Update display periodically
if (now - lastStatusUpdate >= DISPLAY_UPDATE_INTERVAL) {
lastStatusUpdate = now;
updateStatusDisplay();
}
// Command timeout safety
if (Motors.isMoving() && (now - lastCommandTime > COMMAND_TIMEOUT_MS)) {
Serial.println("[Main] Command timeout - stopping");
Motors.stop();
}
// Small delay to prevent watchdog issues
delay(1);
}
void setupWiFi() {
Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
// Update display
if (attempts % 2 == 0) {
char msg[32];
snprintf(msg, sizeof(msg), "WiFi... %d", attempts);
Display.showMessage(msg, TFT_YELLOW);
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" Connected!");
Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str());
Serial.printf("[WiFi] Signal strength: %d dBm\n", WiFi.RSSI());
char msg[32];
snprintf(msg, sizeof(msg), "IP: %s", WiFi.localIP().toString().c_str());
Display.showMessage(msg, TFT_GREEN);
delay(2000);
} else {
Serial.println(" FAILED!");
Serial.println("[WiFi] Could not connect to WiFi");
// Start AP mode as fallback
Serial.println("[WiFi] Starting AP mode...");
WiFi.mode(WIFI_AP);
WiFi.softAP("ClaudesEyes", "claude2025");
Serial.printf("[WiFi] AP IP: %s\n", WiFi.softAPIP().toString().c_str());
Display.showMessage("AP: ClaudesEyes", TFT_ORANGE);
delay(2000);
}
}
void handleSafety() {
// Check for obstacles
if (Ultrasonic.isDanger() && Motors.isMoving()) {
MotorDirection dir = Motors.getCurrentDirection();
if (dir == DIR_FORWARD) {
Serial.println("[Safety] Obstacle too close - stopping!");
Motors.stop();
Display.showEmoji(EMOJI_SURPRISED);
// Set Claude text about obstacle
WebServer.setClaudeText("Hindernis erkannt! Ich halte an.");
}
}
// Check if robot is tilted
if (IMU.isTilted() && Motors.isMoving()) {
Serial.println("[Safety] Robot tilted - stopping!");
Motors.stop();
Display.showEmoji(EMOJI_CONFUSED);
}
// Check if upside down
if (IMU.isUpsideDown()) {
Serial.println("[Safety] Robot upside down!");
Motors.stop();
Motors.setEnabled(false); // Disable motors completely
Display.showMessage("HILFE!", TFT_RED);
}
}
void handleTouchInput() {
TouchButton btn = Display.checkTouch();
if (btn == BTN_NONE) return;
lastCommandTime = millis();
switch (btn) {
case BTN_STOP:
Serial.println("[Touch] STOP pressed");
Motors.stop();
Display.showEmoji(EMOJI_SLEEPY);
break;
case BTN_FORWARD:
Serial.println("[Touch] Forward pressed");
Motors.move(DIR_FORWARD, 50, 1000);
break;
case BTN_BACKWARD:
Serial.println("[Touch] Backward pressed");
Motors.move(DIR_BACKWARD, 50, 1000);
break;
case BTN_LEFT:
Serial.println("[Touch] Left pressed");
Motors.move(DIR_LEFT, 50, 500);
break;
case BTN_RIGHT:
Serial.println("[Touch] Right pressed");
Motors.move(DIR_RIGHT, 50, 500);
break;
case BTN_HOME:
Serial.println("[Touch] Home pressed");
// TODO: Implement return to base
Display.showMessage("Coming home!", TFT_CYAN);
break;
default:
break;
}
}
void updateStatusDisplay() {
if (Display.isInitialized()) {
Display.updateStatus(
Motors.getDirectionString(),
Ultrasonic.getLastDistance(),
WebServer.getBatteryPercent(),
WiFi.RSSI()
);
}
}
+211
View File
@@ -0,0 +1,211 @@
/**
* Claude's Eyes - Motor Control Implementation
*
* 4WD control using TB6612/L298N motor driver on Freenove shield
*/
#include "motor_control.h"
#include "config.h"
// Global instance
MotorControl Motors;
MotorControl::MotorControl()
: _initialized(false)
, _enabled(true)
, _isMoving(false)
, _currentDirection(DIR_STOP)
, _currentSpeed(0)
, _moveStartTime(0)
, _moveDuration(0)
{
}
bool MotorControl::begin() {
#if !ENABLE_MOTORS
Serial.println("[Motors] Disabled in config");
return false;
#endif
// Configure motor A pins
pinMode(MOTOR_A_IN1, OUTPUT);
pinMode(MOTOR_A_IN2, OUTPUT);
// Configure motor B pins
pinMode(MOTOR_B_IN1, OUTPUT);
pinMode(MOTOR_B_IN2, OUTPUT);
// Configure PWM for motor A
ledcSetup(MOTOR_PWM_CHANNEL_A, MOTOR_PWM_FREQ, MOTOR_PWM_RESOLUTION);
ledcAttachPin(MOTOR_A_PWM, MOTOR_PWM_CHANNEL_A);
// Configure PWM for motor B
ledcSetup(MOTOR_PWM_CHANNEL_B, MOTOR_PWM_FREQ, MOTOR_PWM_RESOLUTION);
ledcAttachPin(MOTOR_B_PWM, MOTOR_PWM_CHANNEL_B);
// Start with motors stopped
stop();
_initialized = true;
Serial.println("[Motors] Initialized successfully");
return true;
}
void MotorControl::move(MotorDirection direction, int speed, unsigned long duration_ms) {
if (!_initialized || !_enabled) {
Serial.println("[Motors] Not initialized or disabled");
return;
}
// Clamp speed to valid range
speed = constrain(speed, 0, 100);
// Convert percent to actual speed value
int actualSpeed = percentToSpeed(speed);
_currentDirection = direction;
_currentSpeed = speed;
_moveStartTime = millis();
_moveDuration = duration_ms;
_isMoving = (direction != DIR_STOP);
Serial.printf("[Motors] Move: %s, speed: %d%%, duration: %lums\n",
getDirectionString(), speed, duration_ms);
switch (direction) {
case DIR_FORWARD:
setMotorA(actualSpeed);
setMotorB(actualSpeed);
break;
case DIR_BACKWARD:
setMotorA(-actualSpeed);
setMotorB(-actualSpeed);
break;
case DIR_LEFT:
// Pivot turn: left motors backward, right motors forward
setMotorA(-actualSpeed);
setMotorB(actualSpeed);
break;
case DIR_RIGHT:
// Pivot turn: left motors forward, right motors backward
setMotorA(actualSpeed);
setMotorB(-actualSpeed);
break;
case DIR_SOFT_LEFT:
// Soft turn: reduce left side speed
setMotorA(actualSpeed / 3);
setMotorB(actualSpeed);
break;
case DIR_SOFT_RIGHT:
// Soft turn: reduce right side speed
setMotorA(actualSpeed);
setMotorB(actualSpeed / 3);
break;
case DIR_STOP:
default:
stop();
break;
}
}
void MotorControl::stop() {
// Set direction pins to brake mode
digitalWrite(MOTOR_A_IN1, LOW);
digitalWrite(MOTOR_A_IN2, LOW);
digitalWrite(MOTOR_B_IN1, LOW);
digitalWrite(MOTOR_B_IN2, LOW);
// Set PWM to 0
ledcWrite(MOTOR_PWM_CHANNEL_A, 0);
ledcWrite(MOTOR_PWM_CHANNEL_B, 0);
_isMoving = false;
_currentDirection = DIR_STOP;
_currentSpeed = 0;
_moveDuration = 0;
Serial.println("[Motors] Stopped");
}
void MotorControl::update() {
if (!_isMoving || _moveDuration == 0) {
return;
}
// Check if movement duration has elapsed
if (millis() - _moveStartTime >= _moveDuration) {
Serial.println("[Motors] Movement duration elapsed");
stop();
}
}
void MotorControl::setMotorA(int speed) {
// speed: -255 to 255
// Negative = backward, Positive = forward
if (speed > 0) {
// Forward
digitalWrite(MOTOR_A_IN1, HIGH);
digitalWrite(MOTOR_A_IN2, LOW);
ledcWrite(MOTOR_PWM_CHANNEL_A, speed);
} else if (speed < 0) {
// Backward
digitalWrite(MOTOR_A_IN1, LOW);
digitalWrite(MOTOR_A_IN2, HIGH);
ledcWrite(MOTOR_PWM_CHANNEL_A, -speed);
} else {
// Stop
digitalWrite(MOTOR_A_IN1, LOW);
digitalWrite(MOTOR_A_IN2, LOW);
ledcWrite(MOTOR_PWM_CHANNEL_A, 0);
}
}
void MotorControl::setMotorB(int speed) {
// speed: -255 to 255
// Negative = backward, Positive = forward
if (speed > 0) {
// Forward
digitalWrite(MOTOR_B_IN1, HIGH);
digitalWrite(MOTOR_B_IN2, LOW);
ledcWrite(MOTOR_PWM_CHANNEL_B, speed);
} else if (speed < 0) {
// Backward
digitalWrite(MOTOR_B_IN1, LOW);
digitalWrite(MOTOR_B_IN2, HIGH);
ledcWrite(MOTOR_PWM_CHANNEL_B, -speed);
} else {
// Stop
digitalWrite(MOTOR_B_IN1, LOW);
digitalWrite(MOTOR_B_IN2, LOW);
ledcWrite(MOTOR_PWM_CHANNEL_B, 0);
}
}
int MotorControl::percentToSpeed(int percent) {
if (percent <= 0) return 0;
if (percent >= 100) return MOTOR_MAX_SPEED;
// Map percent to speed range, accounting for minimum speed
return map(percent, 0, 100, MOTOR_MIN_SPEED, MOTOR_MAX_SPEED);
}
const char* MotorControl::getDirectionString() {
switch (_currentDirection) {
case DIR_STOP: return "stop";
case DIR_FORWARD: return "forward";
case DIR_BACKWARD: return "backward";
case DIR_LEFT: return "left";
case DIR_RIGHT: return "right";
case DIR_SOFT_LEFT: return "soft_left";
case DIR_SOFT_RIGHT: return "soft_right";
default: return "unknown";
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Claude's Eyes - Motor Control Header
*
* Controls the 4WD drive system (2 motors per side)
*/
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H
#include <Arduino.h>
// Movement directions
enum MotorDirection {
DIR_STOP,
DIR_FORWARD,
DIR_BACKWARD,
DIR_LEFT, // Turn left (pivot)
DIR_RIGHT, // Turn right (pivot)
DIR_SOFT_LEFT, // Soft turn left (one side slower)
DIR_SOFT_RIGHT // Soft turn right (one side slower)
};
class MotorControl {
public:
MotorControl();
/**
* Initialize motor pins and PWM
* @return true if successful
*/
bool begin();
/**
* Move in a direction for a specified duration
* @param direction Movement direction
* @param speed Speed (0-100 percent)
* @param duration_ms Duration in milliseconds (0 = continuous)
*/
void move(MotorDirection direction, int speed = 50, unsigned long duration_ms = 0);
/**
* Stop all motors immediately
*/
void stop();
/**
* Check if motors are currently active
*/
bool isMoving() { return _isMoving; }
/**
* Get current direction
*/
MotorDirection getCurrentDirection() { return _currentDirection; }
/**
* Get current speed (0-100)
*/
int getCurrentSpeed() { return _currentSpeed; }
/**
* Get direction as string
*/
const char* getDirectionString();
/**
* Update motor state (call from loop for timed movements)
*/
void update();
/**
* Enable/disable motors (safety feature)
*/
void setEnabled(bool enabled) { _enabled = enabled; if (!enabled) stop(); }
bool isEnabled() { return _enabled; }
private:
bool _initialized;
bool _enabled;
bool _isMoving;
MotorDirection _currentDirection;
int _currentSpeed;
unsigned long _moveStartTime;
unsigned long _moveDuration;
// Internal motor control
void setMotorA(int speed); // -255 to 255
void setMotorB(int speed); // -255 to 255
int percentToSpeed(int percent);
};
// Global instance
extern MotorControl Motors;
#endif // MOTOR_CONTROL_H
+187
View File
@@ -0,0 +1,187 @@
/**
* Claude's Eyes - Servo Control Implementation
*
* Pan/Tilt camera mount using standard hobby servos
*/
#include "servo_control.h"
#include "config.h"
// Global instance
ServoControl Servos;
ServoControl::ServoControl()
: _initialized(false)
, _attached(false)
, _isMoving(false)
, _currentPan(SERVO_PAN_CENTER)
, _currentTilt(SERVO_TILT_CENTER)
, _targetPan(SERVO_PAN_CENTER)
, _targetTilt(SERVO_TILT_CENTER)
, _lastStepTime(0)
{
}
bool ServoControl::begin() {
#if !ENABLE_SERVOS
Serial.println("[Servos] Disabled in config");
return false;
#endif
// Allow allocation of all timers for servo library
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
// Attach servos
_panServo.setPeriodHertz(50); // Standard 50Hz servo
_tiltServo.setPeriodHertz(50);
attach();
// Move to center position
center();
_initialized = true;
Serial.println("[Servos] Initialized successfully");
return true;
}
void ServoControl::attach() {
if (_attached) return;
_panServo.attach(SERVO_PAN_PIN, 500, 2400); // Min/max pulse width
_tiltServo.attach(SERVO_TILT_PIN, 500, 2400);
_attached = true;
Serial.println("[Servos] Attached");
}
void ServoControl::detach() {
if (!_attached) return;
_panServo.detach();
_tiltServo.detach();
_attached = false;
Serial.println("[Servos] Detached");
}
void ServoControl::look(LookDirection direction, bool smooth) {
int targetPan = _currentPan;
int targetTilt = _currentTilt;
switch (direction) {
case LOOK_CENTER:
targetPan = SERVO_PAN_CENTER;
targetTilt = SERVO_TILT_CENTER;
break;
case LOOK_LEFT:
targetPan = SERVO_PAN_MAX; // Look left (servo rotates right)
break;
case LOOK_RIGHT:
targetPan = SERVO_PAN_MIN; // Look right (servo rotates left)
break;
case LOOK_UP:
targetTilt = SERVO_TILT_MAX;
break;
case LOOK_DOWN:
targetTilt = SERVO_TILT_MIN;
break;
default:
break;
}
setPosition(targetPan, targetTilt, smooth);
}
void ServoControl::setPosition(int pan, int tilt, bool smooth) {
if (!_initialized) {
Serial.println("[Servos] Not initialized");
return;
}
// Ensure servos are attached
if (!_attached) {
attach();
}
// Clamp to valid ranges
_targetPan = clampPan(pan);
_targetTilt = clampTilt(tilt);
Serial.printf("[Servos] Target position: pan=%d, tilt=%d\n", _targetPan, _targetTilt);
if (smooth) {
// Smooth movement will be handled in update()
_isMoving = true;
_lastStepTime = millis();
} else {
// Immediate movement
_currentPan = _targetPan;
_currentTilt = _targetTilt;
_panServo.write(_currentPan);
_tiltServo.write(_currentTilt);
_isMoving = false;
}
}
void ServoControl::center() {
setPosition(SERVO_PAN_CENTER, SERVO_TILT_CENTER, false);
}
void ServoControl::update() {
if (!_isMoving) return;
unsigned long now = millis();
if (now - _lastStepTime < SERVO_STEP_DELAY_MS) {
return;
}
_lastStepTime = now;
bool panDone = false;
bool tiltDone = false;
// Move pan servo
if (_currentPan < _targetPan) {
_currentPan = min(_currentPan + SERVO_STEP_SIZE, _targetPan);
_panServo.write(_currentPan);
} else if (_currentPan > _targetPan) {
_currentPan = max(_currentPan - SERVO_STEP_SIZE, _targetPan);
_panServo.write(_currentPan);
} else {
panDone = true;
}
// Move tilt servo
if (_currentTilt < _targetTilt) {
_currentTilt = min(_currentTilt + SERVO_STEP_SIZE, _targetTilt);
_tiltServo.write(_currentTilt);
} else if (_currentTilt > _targetTilt) {
_currentTilt = max(_currentTilt - SERVO_STEP_SIZE, _targetTilt);
_tiltServo.write(_currentTilt);
} else {
tiltDone = true;
}
// Check if movement complete
if (panDone && tiltDone) {
_isMoving = false;
Serial.printf("[Servos] Movement complete: pan=%d, tilt=%d\n",
_currentPan, _currentTilt);
}
}
int ServoControl::clampPan(int angle) {
return constrain(angle, SERVO_PAN_MIN, SERVO_PAN_MAX);
}
int ServoControl::clampTilt(int angle) {
return constrain(angle, SERVO_TILT_MIN, SERVO_TILT_MAX);
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Claude's Eyes - Servo Control Header
*
* Controls pan/tilt servos for camera positioning
*/
#ifndef SERVO_CONTROL_H
#define SERVO_CONTROL_H
#include <Arduino.h>
#include <ESP32Servo.h>
// Look commands
enum LookDirection {
LOOK_CENTER,
LOOK_LEFT,
LOOK_RIGHT,
LOOK_UP,
LOOK_DOWN,
LOOK_CUSTOM // Use with setPosition()
};
class ServoControl {
public:
ServoControl();
/**
* Initialize servos
* @return true if successful
*/
bool begin();
/**
* Move camera to look in a direction
* @param direction Direction to look
* @param smooth Use smooth movement (default true)
*/
void look(LookDirection direction, bool smooth = true);
/**
* Set specific pan/tilt position
* @param pan Pan angle (0-180, 90 = center)
* @param tilt Tilt angle (0-180, 90 = center)
* @param smooth Use smooth movement
*/
void setPosition(int pan, int tilt, bool smooth = true);
/**
* Get current pan angle
*/
int getPan() { return _currentPan; }
/**
* Get current tilt angle
*/
int getTilt() { return _currentTilt; }
/**
* Center both servos
*/
void center();
/**
* Update servos (call from loop for smooth movement)
*/
void update();
/**
* Check if servos are currently moving
*/
bool isMoving() { return _isMoving; }
/**
* Detach servos to save power/prevent jitter
*/
void detach();
/**
* Reattach servos
*/
void attach();
private:
Servo _panServo;
Servo _tiltServo;
bool _initialized;
bool _attached;
bool _isMoving;
int _currentPan;
int _currentTilt;
int _targetPan;
int _targetTilt;
unsigned long _lastStepTime;
void moveToTarget();
int clampPan(int angle);
int clampTilt(int angle);
};
// Global instance
extern ServoControl Servos;
#endif // SERVO_CONTROL_H
+90
View File
@@ -0,0 +1,90 @@
/**
* Claude's Eyes - Ultrasonic Sensor Implementation
*
* HC-SR04 ultrasonic distance sensor
*/
#include "ultrasonic.h"
#include "config.h"
// Global instance
UltrasonicSensor Ultrasonic;
UltrasonicSensor::UltrasonicSensor()
: _initialized(false)
, _lastDistance(0)
, _lastMeasurementTime(0)
{
}
bool UltrasonicSensor::begin() {
#if !ENABLE_ULTRASONIC
Serial.println("[Ultrasonic] Disabled in config");
return false;
#endif
pinMode(ULTRASONIC_TRIGGER_PIN, OUTPUT);
pinMode(ULTRASONIC_ECHO_PIN, INPUT);
// Initial trigger to low
digitalWrite(ULTRASONIC_TRIGGER_PIN, LOW);
// Take initial measurement
_lastDistance = getDistance();
_initialized = true;
Serial.println("[Ultrasonic] Initialized successfully");
return true;
}
float UltrasonicSensor::getDistance() {
if (!_initialized && ENABLE_ULTRASONIC) {
// Not initialized yet, try anyway
}
// Send trigger pulse
digitalWrite(ULTRASONIC_TRIGGER_PIN, LOW);
delayMicroseconds(2);
digitalWrite(ULTRASONIC_TRIGGER_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(ULTRASONIC_TRIGGER_PIN, LOW);
// Measure echo pulse duration
// Timeout after ~25ms (corresponds to ~4m distance)
unsigned long duration = pulseIn(ULTRASONIC_ECHO_PIN, HIGH, 25000);
if (duration == 0) {
// No echo received (object too far or sensor error)
return ULTRASONIC_MAX_DISTANCE;
}
// Calculate distance: sound travels at ~343 m/s = 0.0343 cm/µs
// Distance = (duration * 0.0343) / 2 (divide by 2 for round trip)
float distance = (duration * 0.0343f) / 2.0f;
// Clamp to valid range
if (distance > ULTRASONIC_MAX_DISTANCE) {
distance = ULTRASONIC_MAX_DISTANCE;
}
_lastDistance = distance;
return distance;
}
bool UltrasonicSensor::isDanger() {
return _lastDistance > 0 && _lastDistance < OBSTACLE_DANGER_DISTANCE;
}
bool UltrasonicSensor::isWarning() {
return _lastDistance > 0 && _lastDistance < OBSTACLE_WARNING_DISTANCE;
}
void UltrasonicSensor::update() {
if (!_initialized) return;
unsigned long now = millis();
if (now - _lastMeasurementTime >= MEASUREMENT_INTERVAL) {
_lastMeasurementTime = now;
getDistance();
}
}
+64
View File
@@ -0,0 +1,64 @@
/**
* Claude's Eyes - Ultrasonic Sensor Header
*
* HC-SR04 ultrasonic distance sensor for obstacle detection
*/
#ifndef ULTRASONIC_H
#define ULTRASONIC_H
#include <Arduino.h>
class UltrasonicSensor {
public:
UltrasonicSensor();
/**
* Initialize the sensor
* @return true if successful
*/
bool begin();
/**
* Get distance measurement
* @return Distance in centimeters (0 if no echo)
*/
float getDistance();
/**
* Check if there's an obstacle within danger distance
*/
bool isDanger();
/**
* Check if there's an obstacle within warning distance
*/
bool isWarning();
/**
* Get last measurement (without triggering new one)
*/
float getLastDistance() { return _lastDistance; }
/**
* Update sensor (call periodically)
*/
void update();
/**
* Check if sensor is initialized
*/
bool isInitialized() { return _initialized; }
private:
bool _initialized;
float _lastDistance;
unsigned long _lastMeasurementTime;
static const unsigned long MEASUREMENT_INTERVAL = 50; // 50ms between measurements
};
// Global instance
extern UltrasonicSensor Ultrasonic;
#endif // ULTRASONIC_H
+417
View File
@@ -0,0 +1,417 @@
/**
* 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();
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Claude's Eyes - Webserver Header
*
* AsyncWebServer with REST API for camera, motors, sensors
*/
#ifndef WEBSERVER_H
#define WEBSERVER_H
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
class WebServerModule {
public:
WebServerModule();
/**
* Initialize webserver
* @return true if successful
*/
bool begin();
/**
* Get server uptime in seconds
*/
unsigned long getUptime() { return millis() / 1000; }
/**
* Set Claude's text (from API)
*/
void setClaudeText(const String& text);
/**
* Get Claude's text
*/
String getClaudeText() { return _claudeText; }
/**
* Check if there's new Claude text
*/
bool hasNewClaudeText();
/**
* Get current action string
*/
const char* getCurrentAction();
/**
* Get WiFi RSSI
*/
int getWifiRssi();
/**
* Get battery percentage (placeholder)
*/
int getBatteryPercent() { return _batteryPercent; }
private:
AsyncWebServer _server;
String _claudeText;
unsigned long _claudeTextTimestamp;
bool _claudeTextNew;
int _batteryPercent;
// Request handlers
void setupRoutes();
// API handlers
void handleCapture(AsyncWebServerRequest* request);
void handleStatus(AsyncWebServerRequest* request);
void handleCommand(AsyncWebServerRequest* request, uint8_t* data, size_t len);
void handleGetClaudeText(AsyncWebServerRequest* request);
void handlePostClaudeText(AsyncWebServerRequest* request, uint8_t* data, size_t len);
void handleDisplay(AsyncWebServerRequest* request, uint8_t* data, size_t len);
// Auth check
bool checkAuth(AsyncWebServerRequest* request);
void sendError(AsyncWebServerRequest* request, int code, const char* message);
void sendJson(AsyncWebServerRequest* request, JsonDocument& doc);
};
// Global instance
extern WebServerModule WebServer;
#endif // WEBSERVER_H