bridge mit ready und statemachine system
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user