소스 검색

Beginning work on esp32 firmware

Fred Damstra 19 시간 전
부모
커밋
42f98335ad

+ 3 - 2
CLAUDE.md

@@ -51,7 +51,8 @@ freds_first_iphone_app/
 │   ├── wide_mouth_detection_guide.md # Detection tuning guide
 │   └── setup_mouth_detection.md  # Setup instructions
 ├── esp32_firmware/
-│   └── octv2_motor_controller.ino # ESP32 stepper control
+│   ├── octv2_motor_controller.ino # ESP32 stepper control firmware
+│   └── ESP32_COMMANDS.md          # Complete command reference
 └── CLAUDE.md                     # This file
 ```
 
@@ -126,7 +127,7 @@ avg_lip_thickness > 8                 # Lip separation pixels
 ```python
 # Mechanical compensation
 self.rotation_offset_degrees = 0.0    # Camera/launcher alignment
-self.elevation_offset_degrees = 0.0   # Gravity compensation
+self.pitch_offset_degrees = 0.0   # Gravity compensation
 ```
 
 ## Troubleshooting

+ 8 - 0
OreoLauncher/ContentView.swift

@@ -231,11 +231,19 @@ struct ContentView: View {
     }
 
     private func capturePhoto() {
+        guard networkService.isConnected else {
+            print("Cannot capture photo: Not connected to Pi")
+            return
+        }
+
         isStreaming = true
         let command = LauncherCommand.capturePhoto()
         networkService.sendCommand(command)
         networkService.requestPhoto { image in
             DispatchQueue.main.async {
+                if image == nil {
+                    print("Failed to capture photo")
+                }
                 // If we're not video streaming, show the captured photo
                 // If we are streaming, the photo was saved on Pi but video continues
                 if !self.isVideoStreaming {

+ 46 - 5
OreoLauncher/NetworkService.swift

@@ -72,20 +72,45 @@ class NetworkService: ObservableObject {
 
     func requestPhoto(completion: @escaping (UIImage?) -> Void) {
         guard let connection = connection, isConnected else {
-            completion(nil)
+            DispatchQueue.main.async {
+                completion(nil)
+            }
             return
         }
 
+        // Add timeout for photo requests
+        let timeoutTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { _ in
+            DispatchQueue.main.async {
+                completion(nil)
+            }
+        }
+
         connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in
+            timeoutTimer.invalidate()
+
+            if let error = error {
+                print("Photo receive error: \(error)")
+                DispatchQueue.main.async {
+                    completion(nil)
+                }
+                return
+            }
+
             if let data = data, !data.isEmpty {
                 let image = UIImage(data: data)
-                completion(image)
+                DispatchQueue.main.async {
+                    completion(image)
+                }
             } else {
-                completion(nil)
+                DispatchQueue.main.async {
+                    completion(nil)
+                }
             }
         }
     }
 
+    private var isStreamingActive = false
+
     func startVideoStream(onFrame: @escaping (UIImage?) -> Void) {
         guard let connection = connection, isConnected else {
             return
@@ -93,17 +118,32 @@ class NetworkService: ObservableObject {
 
         let command = LauncherCommand.startVideoStream()
         sendCommand(command)
+        isStreamingActive = true
 
         // Continuously receive video frames
         func receiveFrame() {
+            guard isStreamingActive else { return }
+
             connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in
+                guard self.isStreamingActive else { return }
+
+                if let error = error {
+                    print("Video stream error: \(error)")
+                    DispatchQueue.main.async {
+                        onFrame(nil)
+                    }
+                    return
+                }
+
                 if let data = data, !data.isEmpty {
                     let image = UIImage(data: data)
                     DispatchQueue.main.async {
                         onFrame(image)
                     }
-                    // Continue receiving frames
-                    receiveFrame()
+                    // Continue receiving frames only if still streaming
+                    if self.isStreamingActive {
+                        receiveFrame()
+                    }
                 }
             }
         }
@@ -111,6 +151,7 @@ class NetworkService: ObservableObject {
     }
 
     func stopVideoStream() {
+        isStreamingActive = false
         let command = LauncherCommand.stopVideoStream()
         sendCommand(command)
     }

+ 265 - 0
esp32_firmware/ESP32_COMMANDS.md

@@ -0,0 +1,265 @@
+# ESP32 Motor Controller - Command Reference
+
+This document describes all commands supported by the OCTv2 ESP32 motor controller firmware.
+
+## Overview
+
+The ESP32 controller handles precise stepper motor control for the OCTv2 system via serial communication at 115200 baud. All commands are text-based and must end with a newline character.
+
+## Command Categories
+
+### **Movement Commands**
+
+#### `HOME`
+- **Description**: Home both rotation and pitch motors to their limit switches
+- **Parameters**: None
+- **Response**: `OK` on success, `ERROR: <message>` on failure
+- **Example**:
+  ```
+  HOME
+  OK
+  ```
+- **Notes**: Must be run before any positioning commands. Sets current position to (0°, 0°)
+
+#### `MOVE <rotation> <pitch>`
+- **Description**: Set target position in degrees (non-blocking)
+- **Parameters**:
+  - `rotation`: -90.0 to +90.0 degrees (horizontal aim)
+  - `pitch`: 0.0 to 60.0 degrees (vertical aim)
+- **Response**: `OK` immediately (movement happens in background)
+- **Examples**:
+  ```
+  MOVE 45.0 30.0    # Set target to 45° right, 30° up
+  MOVE -30.0 15.5   # Update target to 30° left, 15.5° up
+  MOVE 0.0 0.0      # Set target to home position
+  ```
+- **Notes**:
+  - Values are automatically clamped to safe limits
+  - New targets override previous ones immediately
+  - Motors move smoothly to new position using acceleration profiles
+
+#### `REL <delta_rotation> <delta_pitch>`
+- **Description**: Adjust target relative to current target (non-blocking)
+- **Parameters**:
+  - `delta_rotation`: Degrees to adjust horizontally (+ = right, - = left)
+  - `delta_pitch`: Degrees to adjust vertically (+ = up, - = down)
+- **Response**: `OK` immediately (movement happens in background)
+- **Examples**:
+  ```
+  REL 5.0 0.0       # Adjust target 5° right
+  REL -2.5 1.0      # Adjust target 2.5° left, 1° up
+  REL 0.0 -5.0      # Adjust target 5° down
+  ```
+- **Notes**:
+  - Adjustments are relative to current target position
+  - Perfect for continuous tracking adjustments
+
+### **Action Commands**
+
+#### `FIRE`
+- **Description**: Trigger the Oreo firing mechanism
+- **Parameters**: None
+- **Response**: `OK` after firing sequence complete
+- **Example**:
+  ```
+  FIRE
+  🔥 FIRING OREO!
+  OK
+  ```
+- **Notes**: Uses either servo (90° pulse) or solenoid (100ms pulse) depending on configuration
+
+### **Status Commands**
+
+#### `POS`
+- **Description**: Get current motor positions
+- **Parameters**: None
+- **Response**: `<rotation> <pitch>` in degrees
+- **Example**:
+  ```
+  POS
+  45.0 30.0
+  ```
+
+#### `STATUS`
+- **Description**: Get detailed system status
+- **Parameters**: None
+- **Response**: Comprehensive status string
+- **Example**:
+  ```
+  STATUS
+  HOMED:1 ROT:43.2 ELEV:28.7 TARGET_ROT:45.0 TARGET_ELEV:30.0 MOVING:1 EMERGENCY:0 LIMITS:0,0
+  ```
+- **Status Fields**:
+  - `HOMED`: 1 if homed, 0 if not
+  - `ROT`: Current actual rotation in degrees
+  - `ELEV`: Current actual pitch in degrees
+  - `TARGET_ROT`: Target rotation in degrees
+  - `TARGET_ELEV`: Target pitch in degrees
+  - `MOVING`: 1 if motors moving, 0 if stopped
+  - `EMERGENCY`: 1 if in emergency stop, 0 if normal
+  - `LIMITS`: Limit switch states (rotation,pitch: 1=triggered, 0=clear)
+
+### **Safety Commands**
+
+#### `STOP`
+- **Description**: Emergency stop - immediately disable all motors
+- **Parameters**: None
+- **Response**: `EMERGENCY STOP ACTIVATED`
+- **Example**:
+  ```
+  STOP
+  EMERGENCY STOP ACTIVATED
+  ```
+- **Notes**: Disables stepper drivers, stops all motion. Use `RESET` to recover.
+
+#### `RESET`
+- **Description**: Reset from emergency stop state
+- **Parameters**: None
+- **Response**: `Emergency stop reset - system ready`
+- **Example**:
+  ```
+  RESET
+  Emergency stop reset - system ready
+  ```
+- **Notes**: Re-enables stepper drivers, resets watchdog timer
+
+## Communication Protocol
+
+### **Serial Settings**
+- **Baud Rate**: 115200
+- **Data Bits**: 8
+- **Parity**: None
+- **Stop Bits**: 1
+- **Line Ending**: Newline (`\n`) or Carriage Return + Newline (`\r\n`)
+
+### **Command Format**
+- Commands are **case-insensitive** (automatically converted to uppercase)
+- Maximum command length: **50 characters**
+- Parameters separated by **spaces**
+- Commands longer than 50 characters are rejected with `ERROR: Command too long`
+
+### **Response Types**
+- **`OK`**: Command executed successfully
+- **`ERROR: <description>`**: Command failed with error description
+- **Data responses**: Position data, status information
+- **Informational**: Status messages (e.g., "🔥 FIRING OREO!")
+
+## Safety Features
+
+### **Watchdog Timer**
+- **Timeout**: 30 seconds
+- **Behavior**: Automatic emergency stop if no commands received
+- **Reset**: Any valid command resets the timer
+- **Warning**: `WARNING: Communication timeout - emergency stop`
+
+### **Software Limits**
+- **Rotation**: Hard-limited to -90° to +90°
+- **Elevation**: Hard-limited to 0° to 60°
+- **Enforcement**: Values automatically clamped to safe ranges
+
+### **Emergency Protection**
+- **Emergency stop** disables stepper drivers immediately
+- **Buffer overflow protection** prevents command injection
+- **Invalid command** handling with clear error messages
+- **Limit switch** protection during homing
+
+### **Status LED Indicators**
+- **Solid ON**: System idle and ready
+- **Fast Blinking (100ms)**: Motors moving
+- **Very Fast Blinking (200ms)**: Emergency stop active
+
+## Error Conditions
+
+### **Common Errors**
+- `ERROR: Not homed` - Attempted movement before homing
+- `ERROR: Invalid MOVE syntax` - Incorrect command format
+- `ERROR: Invalid REL syntax` - Incorrect command format
+- `ERROR: Command too long` - Command exceeds 50 characters
+- `ERROR: Unknown command` - Unrecognized command
+
+### **Recovery Procedures**
+1. **Communication lost**: Check USB connection, restart ESP32
+2. **Emergency stop**: Send `RESET` command to restore operation
+3. **Not homed**: Send `HOME` command before movement
+4. **Invalid commands**: Check command syntax and try again
+
+## Hardware Configuration
+
+### **Default Pin Assignments**
+```cpp
+// Rotation Motor (A4988 Driver)
+#define ROTATION_STEP_PIN     2
+#define ROTATION_DIR_PIN      3
+#define ROTATION_ENABLE_PIN   4
+#define ROTATION_LIMIT_PIN    5
+
+// Elevation Motor (A4988 Driver)
+#define ELEVATION_STEP_PIN    6
+#define ELEVATION_DIR_PIN     7
+#define ELEVATION_ENABLE_PIN  8
+#define ELEVATION_LIMIT_PIN   9
+
+// Fire Mechanism
+#define FIRE_SERVO_PIN        10
+#define FIRE_SOLENOID_PIN     11
+
+// Status
+#define STATUS_LED_PIN        13
+```
+
+### **Motor Settings**
+- **Steps per revolution**: 200 (1.8° steppers)
+- **Microstepping**: 16x (A4988 drivers)
+- **Gear ratios**: 5:1 rotation, 3:1 pitch (configurable)
+- **Max speed**: 2000 steps/sec rotation, 1500 steps/sec pitch
+- **Acceleration**: 1000 steps/sec²
+
+## Usage Examples
+
+### **Basic Operation Sequence**
+```
+HOME                    # Home motors first
+OK
+MOVE 30.0 20.0         # Aim 30° right, 20° up
+OK
+FIRE                   # Fire Oreo
+🔥 FIRING OREO!
+OK
+POS                    # Check position
+30.0 20.0
+REL -30.0 -20.0        # Return to center
+OK
+```
+
+### **Status Monitoring**
+```
+STATUS
+HOMED:1 ROT:30.0 ELEV:20.0 RUNNING:0 EMERGENCY:0
+
+# During movement:
+MOVE 45.0 30.0
+STATUS
+HOMED:1 ROT:35.2 ELEV:25.1 RUNNING:1 EMERGENCY:0
+```
+
+### **Emergency Procedures**
+```
+STOP                   # Emergency stop
+EMERGENCY STOP ACTIVATED
+
+STATUS
+HOMED:1 ROT:35.2 ELEV:25.1 RUNNING:0 EMERGENCY:1
+
+RESET                  # Recover from emergency
+Emergency stop reset - system ready
+```
+
+## Integration Notes
+
+This ESP32 controller is designed to work with the OCTv2 Raspberry Pi server, which handles:
+- iOS app communication
+- Computer vision targeting
+- High-level control logic
+- Distance estimation and trajectory calculation
+
+The ESP32 focuses purely on precise, reliable motor control and safety.

+ 115 - 70
raspberry_pi_server/esp32_firmware/octv2_motor_controller.ino → esp32_firmware/octv2_motor_controller.ino

@@ -1,12 +1,12 @@
 /*
   OCTv2 (Oreo Cookie Thrower v2) - ESP32 Motor Controller
 
-  Controls two stepper motors for rotation and elevation
+  Controls two stepper motors for rotation and pitch
   Receives commands via Serial at 115200 baud
 
   Hardware:
   - Rotation Stepper: A4988 driver
-  - Elevation Stepper: A4988 driver
+  - Pitch Stepper: A4988 driver
   - Fire mechanism: Servo or solenoid
   - Limit switches for homing
 */
@@ -20,10 +20,10 @@
 #define ROTATION_ENABLE_PIN   4
 #define ROTATION_LIMIT_PIN    5   // Home limit switch
 
-#define ELEVATION_STEP_PIN    6
-#define ELEVATION_DIR_PIN     7
-#define ELEVATION_ENABLE_PIN  8
-#define ELEVATION_LIMIT_PIN   9   // Home limit switch
+#define PITCH_STEP_PIN    6
+#define PITCH_DIR_PIN     7
+#define PITCH_ENABLE_PIN  8
+#define PITCH_LIMIT_PIN   9   // Home limit switch
 
 #define FIRE_SERVO_PIN        10
 #define FIRE_SOLENOID_PIN     11
@@ -36,24 +36,24 @@
 
 // Mechanical configuration
 #define ROTATION_GEAR_RATIO   5.0   // 5:1 gear reduction
-#define ELEVATION_GEAR_RATIO  3.0   // 3:1 gear reduction
+#define PITCH_GEAR_RATIO  3.0   // 3:1 gear reduction
 #define ROTATION_STEPS_PER_DEGREE  (TOTAL_STEPS * ROTATION_GEAR_RATIO / 360.0)
-#define ELEVATION_STEPS_PER_DEGREE (TOTAL_STEPS * ELEVATION_GEAR_RATIO / 360.0)
+#define PITCH_STEPS_PER_DEGREE (TOTAL_STEPS * PITCH_GEAR_RATIO / 360.0)
 
 // Movement limits
 #define ROTATION_MIN_DEGREES  -90.0
 #define ROTATION_MAX_DEGREES   90.0
-#define ELEVATION_MIN_DEGREES   0.0
-#define ELEVATION_MAX_DEGREES  60.0
+#define PITCH_MIN_DEGREES   0.0
+#define PITCH_MAX_DEGREES  60.0
 
 // Speed settings
 #define MAX_SPEED_ROTATION    2000   // Steps per second
-#define MAX_SPEED_ELEVATION   1500   // Steps per second
+#define MAX_SPEED_PITCH   1500   // Steps per second
 #define ACCELERATION         1000    // Steps per second squared
 
 // Create stepper objects
 AccelStepper rotationStepper(AccelStepper::DRIVER, ROTATION_STEP_PIN, ROTATION_DIR_PIN);
-AccelStepper elevationStepper(AccelStepper::DRIVER, ELEVATION_STEP_PIN, ELEVATION_DIR_PIN);
+AccelStepper pitchStepper(AccelStepper::DRIVER, PITCH_STEP_PIN, PITCH_DIR_PIN);
 
 // Fire mechanism
 Servo fireServo;
@@ -61,9 +61,14 @@ bool useServoForFire = true;  // Set to false to use solenoid instead
 
 // Current positions in degrees
 float currentRotation = 0.0;
-float currentElevation = 0.0;
+float currentPitch = 0.0;
 bool isHomed = false;
 
+// Safety and error handling
+bool emergencyStop = false;
+unsigned long lastCommandTime = 0;
+const unsigned long WATCHDOG_TIMEOUT = 30000; // 30 seconds
+
 // Command parsing
 String inputCommand = "";
 bool commandReady = false;
@@ -74,21 +79,21 @@ void setup() {
 
   // Setup pins
   pinMode(ROTATION_ENABLE_PIN, OUTPUT);
-  pinMode(ELEVATION_ENABLE_PIN, OUTPUT);
+  pinMode(PITCH_ENABLE_PIN, OUTPUT);
   pinMode(ROTATION_LIMIT_PIN, INPUT_PULLUP);
-  pinMode(ELEVATION_LIMIT_PIN, INPUT_PULLUP);
+  pinMode(PITCH_LIMIT_PIN, INPUT_PULLUP);
   pinMode(FIRE_SOLENOID_PIN, OUTPUT);
   pinMode(STATUS_LED_PIN, OUTPUT);
 
   // Configure steppers
   rotationStepper.setMaxSpeed(MAX_SPEED_ROTATION);
   rotationStepper.setAcceleration(ACCELERATION);
-  elevationStepper.setMaxSpeed(MAX_SPEED_ELEVATION);
-  elevationStepper.setAcceleration(ACCELERATION);
+  pitchStepper.setMaxSpeed(MAX_SPEED_ELEVATION);
+  pitchStepper.setAcceleration(ACCELERATION);
 
   // Enable steppers
   digitalWrite(ROTATION_ENABLE_PIN, LOW);  // LOW = enabled for A4988
-  digitalWrite(ELEVATION_ENABLE_PIN, LOW);
+  digitalWrite(PITCH_ENABLE_PIN, LOW);
 
   // Setup fire mechanism
   if (useServoForFire) {
@@ -102,10 +107,30 @@ void setup() {
   digitalWrite(STATUS_LED_PIN, HIGH);
 
   Serial.println("✅ OCTv2 Motor Controller Ready");
-  Serial.println("Commands: HOME, MOVE <rot> <elev>, REL <rot> <elev>, FIRE, POS");
+  Serial.println("Commands: HOME, MOVE <rot> <pitch>, REL <rot> <pitch>, FIRE, POS");
 }
 
 void loop() {
+  // Check for emergency stop or communication timeout
+  if (emergencyStop || (millis() - lastCommandTime > WATCHDOG_TIMEOUT && isHomed)) {
+    if (!emergencyStop) {
+      Serial.println("WARNING: Communication timeout - emergency stop");
+      emergencyStop = true;
+    }
+    // Emergency stop: disable motors and fast blink LED
+    rotationStepper.stop();
+    pitchStepper.stop();
+    digitalWrite(ROTATION_ENABLE_PIN, HIGH);
+    digitalWrite(PITCH_ENABLE_PIN, HIGH);
+
+    static unsigned long emergencyBlink = 0;
+    if (millis() - emergencyBlink > 200) {
+      digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
+      emergencyBlink = millis();
+    }
+    return;
+  }
+
   // Handle serial commands
   handleSerialInput();
 
@@ -113,15 +138,18 @@ void loop() {
     processCommand();
     commandReady = false;
     inputCommand = "";
+    lastCommandTime = millis(); // Reset watchdog timer
   }
 
-  // Run steppers
-  rotationStepper.run();
-  elevationStepper.run();
+  // Run steppers (only if not in emergency stop)
+  if (!emergencyStop) {
+    rotationStepper.run();
+    pitchStepper.run();
+  }
 
-  // Blink status LED when moving
+  // Status LED: blink when moving, solid when idle
   static unsigned long lastBlink = 0;
-  if (rotationStepper.isRunning() || elevationStepper.isRunning()) {
+  if (rotationStepper.isRunning() || pitchStepper.isRunning()) {
     if (millis() - lastBlink > 100) {
       digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
       lastBlink = millis();
@@ -164,7 +192,15 @@ void processCommand() {
     reportPosition();
   }
   else if (inputCommand == "STOP") {
-    stopMotors();
+    emergencyStop = true;
+    Serial.println("EMERGENCY STOP ACTIVATED");
+  }
+  else if (inputCommand == "RESET") {
+    emergencyStop = false;
+    digitalWrite(ROTATION_ENABLE_PIN, LOW);
+    digitalWrite(PITCH_ENABLE_PIN, LOW);
+    lastCommandTime = millis();
+    Serial.println("Emergency stop reset - system ready");
   }
   else if (inputCommand == "STATUS") {
     reportStatus();
@@ -179,7 +215,7 @@ void homeMotors() {
 
   // Disable acceleration for homing
   rotationStepper.setAcceleration(500);
-  elevationStepper.setAcceleration(500);
+  pitchStepper.setAcceleration(500);
 
   // Home rotation motor
   Serial.println("Homing rotation...");
@@ -195,34 +231,34 @@ void homeMotors() {
   rotationStepper.move(100);  // Move away from limit
   while (rotationStepper.run()) { delay(1); }
 
-  // Home elevation motor
-  Serial.println("Homing elevation...");
-  elevationStepper.setSpeed(-300);  // Move slowly towards limit
-  while (digitalRead(ELEVATION_LIMIT_PIN) == HIGH) {
-    elevationStepper.runSpeed();
+  // Home pitch motor
+  Serial.println("Homing pitch...");
+  pitchStepper.setSpeed(-300);  // Move slowly towards limit
+  while (digitalRead(PITCH_LIMIT_PIN) == HIGH) {
+    pitchStepper.runSpeed();
     delay(1);
   }
-  elevationStepper.stop();
-  elevationStepper.setCurrentPosition(0);
+  pitchStepper.stop();
+  pitchStepper.setCurrentPosition(0);
 
   // Back off from limit
-  elevationStepper.move(50);
-  while (elevationStepper.run()) { delay(1); }
+  pitchStepper.move(50);
+  while (pitchStepper.run()) { delay(1); }
 
   // Restore normal acceleration
   rotationStepper.setAcceleration(ACCELERATION);
-  elevationStepper.setAcceleration(ACCELERATION);
+  pitchStepper.setAcceleration(ACCELERATION);
 
   // Set home position
   currentRotation = 0.0;
-  currentElevation = 0.0;
+  currentPitch = 0.0;
   isHomed = true;
 
   Serial.println("OK");
 }
 
 void handleMoveCommand() {
-  // Parse "MOVE <rotation> <elevation>"
+  // Parse "MOVE <rotation> <pitch>"
   int firstSpace = inputCommand.indexOf(' ', 5);
   if (firstSpace == -1) {
     Serial.println("ERROR: Invalid MOVE syntax");
@@ -230,13 +266,13 @@ void handleMoveCommand() {
   }
 
   float targetRotation = inputCommand.substring(5, firstSpace).toFloat();
-  float targetElevation = inputCommand.substring(firstSpace + 1).toFloat();
+  float targetPitch = inputCommand.substring(firstSpace + 1).toFloat();
 
-  moveToPosition(targetRotation, targetElevation);
+  moveToPosition(targetRotation, targetPitch);
 }
 
 void handleRelativeCommand() {
-  // Parse "REL <delta_rotation> <delta_elevation>"
+  // Parse "REL <delta_rotation> <delta_pitch>"
   int firstSpace = inputCommand.indexOf(' ', 4);
   if (firstSpace == -1) {
     Serial.println("ERROR: Invalid REL syntax");
@@ -244,15 +280,15 @@ void handleRelativeCommand() {
   }
 
   float deltaRotation = inputCommand.substring(4, firstSpace).toFloat();
-  float deltaElevation = inputCommand.substring(firstSpace + 1).toFloat();
+  float deltaPitch = inputCommand.substring(firstSpace + 1).toFloat();
 
   float targetRotation = currentRotation + deltaRotation;
-  float targetElevation = currentElevation + deltaElevation;
+  float targetPitch = currentPitch + deltaPitch;
 
-  moveToPosition(targetRotation, targetElevation);
+  moveToPosition(targetRotation, targetPitch);
 }
 
-void moveToPosition(float targetRotation, float targetElevation) {
+void moveToPosition(float targetRotation, float targetPitch) {
   if (!isHomed) {
     Serial.println("ERROR: Not homed");
     return;
@@ -260,27 +296,21 @@ void moveToPosition(float targetRotation, float targetElevation) {
 
   // Clamp to limits
   targetRotation = constrain(targetRotation, ROTATION_MIN_DEGREES, ROTATION_MAX_DEGREES);
-  targetElevation = constrain(targetElevation, ELEVATION_MIN_DEGREES, ELEVATION_MAX_DEGREES);
+  targetPitch = constrain(targetPitch, ELEVATION_MIN_DEGREES, ELEVATION_MAX_DEGREES);
 
   // Convert to steps
   long rotationSteps = (long)(targetRotation * ROTATION_STEPS_PER_DEGREE);
-  long elevationSteps = (long)(targetElevation * ELEVATION_STEPS_PER_DEGREE);
+  long elevationSteps = (long)(targetPitch * PITCH_STEPS_PER_DEGREE);
 
-  // Move steppers
+  // Set new targets (non-blocking)
   rotationStepper.moveTo(rotationSteps);
-  elevationStepper.moveTo(elevationSteps);
-
-  // Wait for completion
-  while (rotationStepper.isRunning() || elevationStepper.isRunning()) {
-    rotationStepper.run();
-    elevationStepper.run();
-    delay(1);
-  }
+  pitchStepper.moveTo(elevationSteps);
 
-  // Update current position
+  // Update target positions
   currentRotation = targetRotation;
-  currentElevation = targetElevation;
+  currentPitch = targetPitch;
 
+  // Immediate response - movement happens in background
   Serial.println("OK");
 }
 
@@ -304,30 +334,45 @@ void fireOreo() {
 
 void stopMotors() {
   rotationStepper.stop();
-  elevationStepper.stop();
+  pitchStepper.stop();
   Serial.println("OK");
 }
 
 void reportPosition() {
-  // Report actual position in degrees
-  Serial.print(currentRotation, 1);
+  // Report actual current position based on stepper positions
+  float actualRotation = rotationStepper.currentPosition() / ROTATION_STEPS_PER_DEGREE;
+  float actualPitch = pitchStepper.currentPosition() / PITCH_STEPS_PER_DEGREE;
+
+  Serial.print(actualRotation, 1);
   Serial.print(" ");
-  Serial.println(currentElevation, 1);
+  Serial.println(actualPitch, 1);
 }
 
 void reportStatus() {
+  // Report actual positions and targets
+  float actualRotation = rotationStepper.currentPosition() / ROTATION_STEPS_PER_DEGREE;
+  float actualPitch = pitchStepper.currentPosition() / PITCH_STEPS_PER_DEGREE;
+  float targetRotation = rotationStepper.targetPosition() / ROTATION_STEPS_PER_DEGREE;
+  float targetPitch = pitchStepper.targetPosition() / PITCH_STEPS_PER_DEGREE;
+
   Serial.print("HOMED:");
   Serial.print(isHomed ? "1" : "0");
   Serial.print(" ROT:");
-  Serial.print(currentRotation, 1);
-  Serial.print(" ELEV:");
-  Serial.print(currentElevation, 1);
+  Serial.print(actualRotation, 1);
+  Serial.print(" PITCH:");
+  Serial.print(actualPitch, 1);
+  Serial.print(" TARGET_ROT:");
+  Serial.print(targetRotation, 1);
+  Serial.print(" TARGET_PITCH:");
+  Serial.print(targetPitch, 1);
   Serial.print(" MOVING:");
-  Serial.print((rotationStepper.isRunning() || elevationStepper.isRunning()) ? "1" : "0");
+  Serial.print((rotationStepper.isRunning() || pitchStepper.isRunning()) ? "1" : "0");
+  Serial.print(" EMERGENCY:");
+  Serial.print(emergencyStop ? "1" : "0");
   Serial.print(" LIMITS:");
   Serial.print(digitalRead(ROTATION_LIMIT_PIN) ? "0" : "1");
   Serial.print(",");
-  Serial.println(digitalRead(ELEVATION_LIMIT_PIN) ? "0" : "1");
+  Serial.println(digitalRead(PITCH_LIMIT_PIN) ? "0" : "1");
 }
 
 // Helper function for debugging
@@ -336,8 +381,8 @@ void printDebugInfo() {
   Serial.print(rotationStepper.currentPosition());
   Serial.print(" target: ");
   Serial.print(rotationStepper.targetPosition());
-  Serial.print(" | Elev pos: ");
-  Serial.print(elevationStepper.currentPosition());
+  Serial.print(" | Pitch pos: ");
+  Serial.print(pitchStepper.currentPosition());
   Serial.print(" target: ");
-  Serial.println(elevationStepper.targetPosition());
+  Serial.println(pitchStepper.targetPosition());
 }

+ 4 - 3
raspberry_pi_server/README.md

@@ -26,7 +26,7 @@ Video Stream  AI Vision    Hardware Control
 
 ### Motors & Mechanics
 - **Stepper motors:** 200 steps/rev with 16x microstepping
-- **Gear ratios:** Configurable (5:1 rotation, 3:1 elevation)
+- **Gear ratios:** Configurable (5:1 rotation, 3:1 pitch)
 - **Precision:** ~0.1° accuracy with proper gearing
 
 ## 🚀 Quick Setup
@@ -76,8 +76,9 @@ ls /dev/tty*
 # Update port in Python code if needed
 # Edit octv2_server.py line: ESP32Controller(port='/dev/ttyUSB0')
 
-# Run the enhanced server
+# Run the enhanced server (will wait for ESP32 connection)
 python3 octv2_server.py
+# Note: Server will retry ESP32 connection indefinitely until successful
 ```
 
 ### 4. iOS App Connection
@@ -116,7 +117,7 @@ STOP                   → Emergency stop
 ```
 OK                     → Command successful
 ERROR: message         → Command failed
-45.0 30.0             → Position response (rotation elevation)
+45.0 30.0             → Position response (rotation pitch)
 HOMED:1 ROT:45.0 ...  → Status response
 ```
 

+ 15 - 15
raspberry_pi_server/camera_aim_calibration.md

@@ -21,7 +21,7 @@ self.max_adjustment_degrees = 10      # Smaller = more precise movements
 
 # In calculate_centering_adjustment():
 pixels_per_degree_rotation = 15       # Adjust for your setup
-pixels_per_degree_elevation = 12      # Adjust for your setup
+pixels_per_degree_pitch = 12      # Adjust for your setup
 ```
 
 ### **2. Distance Estimation**
@@ -36,8 +36,8 @@ self.average_face_width_cm = 16.0     # Average human face width
 ```python
 # Mechanical compensation
 self.rotation_offset_degrees = 0.0    # If camera/launcher not aligned
-self.elevation_offset_degrees = 0.0   # For gravity/drop compensation
-self.distance_elevation_factor = 0.5  # Higher elevation for closer targets
+self.pitch_offset_degrees = 0.0   # For gravity/drop compensation
+self.distance_pitch_factor = 0.5  # Higher pitch for closer targets
 ```
 
 ## 🎮 **Calibration Process**
@@ -75,11 +75,11 @@ If the system over/under-corrects, adjust these values:
 ```python
 # Too much movement (overshoots):
 pixels_per_degree_rotation = 20       # Increase value
-pixels_per_degree_elevation = 16      # Increase value
+pixels_per_degree_pitch = 16      # Increase value
 
 # Too little movement (doesn't reach target):
 pixels_per_degree_rotation = 10       # Decrease value
-pixels_per_degree_elevation = 8       # Decrease value
+pixels_per_degree_pitch = 8       # Decrease value
 ```
 
 ### **Step 4: Distance Accuracy Check**
@@ -104,10 +104,10 @@ For accurate trajectory at different distances:
 
 ```python
 # If shooting too low at close range:
-self.distance_elevation_factor = 0.7  # Increase compensation
+self.distance_pitch_factor = 0.7  # Increase compensation
 
 # If shooting too high at close range:
-self.distance_elevation_factor = 0.3  # Decrease compensation
+self.distance_pitch_factor = 0.3  # Decrease compensation
 ```
 
 ## 📊 **Understanding the Algorithm**
@@ -125,7 +125,7 @@ dy = mouth_y - center_y
 
 # 2. Convert to angle adjustments
 rotation_adj = dx / pixels_per_degree_rotation
-elevation_adj = -dy / pixels_per_degree_elevation
+pitch_adj = -dy / pixels_per_degree_pitch
 
 # 3. Apply distance scaling (closer = smaller adjustments)
 distance_factor = 100.0 / estimated_distance
@@ -133,7 +133,7 @@ adjusted_pixels_per_degree *= distance_factor
 
 # 4. Add compensation offsets
 rotation_adj += rotation_offset_degrees
-elevation_adj += elevation_offset_degrees + distance_compensation
+pitch_adj += pitch_offset_degrees + distance_compensation
 ```
 
 ## 🎯 **Visual Feedback Elements**
@@ -162,11 +162,11 @@ elevation_adj += elevation_offset_degrees + distance_compensation
 - Ensure mouth gets **centered in blue circle**
 
 ### **Overshooting targets:**
-- **Decrease** `pixels_per_degree_rotation/elevation` values
+- **Decrease** `pixels_per_degree_rotation/pitch` values
 - **Increase** `max_adjustment_degrees` for smaller steps
 
 ### **Undershooting targets:**
-- **Increase** `pixels_per_degree_rotation/elevation` values
+- **Increase** `pixels_per_degree_rotation/pitch` values
 - Check motor gear ratios match ESP32 firmware
 
 ### **Wrong distance estimates:**
@@ -178,7 +178,7 @@ elevation_adj += elevation_offset_degrees + distance_compensation
 - **Use offset parameters** to compensate:
 ```python
 self.rotation_offset_degrees = -2.0   # Aim 2° left
-self.elevation_offset_degrees = 1.5   # Aim 1.5° higher
+self.pitch_offset_degrees = 1.5   # Aim 1.5° higher
 ```
 
 ## 🎪 **Testing Tips**
@@ -192,9 +192,9 @@ self.elevation_offset_degrees = 1.5   # Aim 1.5° higher
 ## 🍪 **Advanced Features**
 
 ### **Distance-Based Trajectory:**
-The system automatically adjusts elevation based on distance:
-- **Closer targets** (50-100cm) = higher elevation
-- **Farther targets** (200cm+) = lower elevation
+The system automatically adjusts pitch based on distance:
+- **Closer targets** (50-100cm) = higher pitch
+- **Farther targets** (200cm+) = lower pitch
 
 ### **Iterative Centering:**
 If target not centered on first try:

+ 157 - 68
raspberry_pi_server/octv2_server.py

@@ -3,7 +3,7 @@
 OCTv2 (Oreo Cookie Thrower v2) - Raspberry Pi Server v2
 - ESP32 serial communication for motor control
 - Automatic mouth detection and targeting
-- Stepper motor control for rotation and elevation
+- Stepper motor control for rotation and pitch
 """
 
 import socket
@@ -45,15 +45,35 @@ class ESP32Controller:
         self.connect()
 
     def connect(self):
-        """Connect to ESP32 via serial"""
-        try:
-            self.serial_conn = serial.Serial(self.port, self.baudrate, timeout=1)
-            time.sleep(2)  # Wait for ESP32 to initialize
-            logger.info(f"Connected to ESP32 on {self.port}")
-            return True
-        except Exception as e:
-            logger.error(f"Failed to connect to ESP32: {e}")
-            return False
+        """Connect to ESP32 via serial with infinite retry"""
+        retry_delay = 2.0
+        attempt = 0
+
+        logger.info("ESP32 connection required - will retry indefinitely...")
+
+        while True:
+            attempt += 1
+            try:
+                self.serial_conn = serial.Serial(self.port, self.baudrate, timeout=1)
+                time.sleep(2)  # Wait for ESP32 to initialize
+
+                # Test connection with a simple command
+                test_response = self.send_command("STATUS")
+                if "ERROR" not in test_response:
+                    logger.info(f"✅ Connected to ESP32 on {self.port} (attempt {attempt})")
+                    return True
+                else:
+                    self.serial_conn.close()
+                    self.serial_conn = None
+
+            except Exception as e:
+                logger.warning(f"ESP32 connection attempt {attempt} failed: {e}")
+                if self.serial_conn:
+                    self.serial_conn.close()
+                    self.serial_conn = None
+
+            logger.info(f"Retrying ESP32 connection in {retry_delay} seconds...")
+            time.sleep(retry_delay)
 
     def send_command(self, command: str) -> str:
         """Send command to ESP32 and get response"""
@@ -71,19 +91,19 @@ class ESP32Controller:
             return "ERROR: Communication failed"
 
     def home_motors(self) -> bool:
-        """Home both rotation and elevation motors"""
+        """Home both rotation and pitch motors"""
         response = self.send_command("HOME")
         return response == "OK"
 
-    def move_to_position(self, rotation_degrees: float, elevation_degrees: float) -> bool:
-        """Move to absolute position (rotation: -90 to +90, elevation: 0 to 60)"""
-        cmd = f"MOVE {rotation_degrees:.1f} {elevation_degrees:.1f}"
+    def move_to_position(self, rotation_degrees: float, pitch_degrees: float) -> bool:
+        """Move to absolute position (rotation: -90 to +90, pitch: 0 to 60)"""
+        cmd = f"MOVE {rotation_degrees:.1f} {pitch_degrees:.1f}"
         response = self.send_command(cmd)
         return response == "OK"
 
-    def move_relative(self, delta_rotation: float, delta_elevation: float) -> bool:
+    def move_relative(self, delta_rotation: float, delta_pitch: float) -> bool:
         """Move relative to current position"""
-        cmd = f"REL {delta_rotation:.1f} {delta_elevation:.1f}"
+        cmd = f"REL {delta_rotation:.1f} {delta_pitch:.1f}"
         response = self.send_command(cmd)
         return response == "OK"
 
@@ -93,7 +113,7 @@ class ESP32Controller:
         return response == "OK"
 
     def get_position(self) -> Tuple[float, float]:
-        """Get current position (rotation, elevation)"""
+        """Get current position (rotation, pitch)"""
         response = self.send_command("POS")
         try:
             parts = response.split()
@@ -139,8 +159,8 @@ class MouthDetector:
 
         # Aiming offsets (configurable for mechanical compensation)
         self.rotation_offset_degrees = 0.0   # Adjust if camera/launcher not perfectly aligned
-        self.elevation_offset_degrees = 0.0  # Adjust for gravity compensation
-        self.distance_elevation_factor = 0.5  # Elevation adjustment based on distance
+        self.pitch_offset_degrees = 0.0  # Adjust for gravity compensation
+        self.distance_pitch_factor = 0.5  # Elevation adjustment based on distance
 
         logger.info("Mouth detector initialized")
 
@@ -208,6 +228,19 @@ class MouthDetector:
     def _analyze_mouth_state(self, landmarks) -> Tuple[str, float]:
         """
         Analyze mouth landmarks to determine state and confidence
+
+        Uses 68-point facial landmark model where mouth points are 48-67:
+        - Points 48-54: Outer lip contour (left to right)
+        - Points 55-59: Outer lip contour (right to left)
+        - Points 60-64: Inner lip contour (left to right)
+        - Points 65-67: Inner lip contour (right to left)
+
+        Algorithm:
+        1. Calculate mouth aspect ratios (height/width) for inner and outer lips
+        2. Measure lip separation (distance between inner lip points)
+        3. Classify based on thresholds tuned for WIDE_OPEN detection
+        4. Only WIDE_OPEN mouths are targeted for firing
+
         Returns: (state, confidence) where state is CLOSED, SPEAKING, SMILING, or WIDE_OPEN
         """
         # Key mouth landmark points
@@ -288,7 +321,23 @@ class MouthDetector:
     def calculate_centering_adjustment(self, mouth_x: int, mouth_y: int, face_width: int) -> Tuple[float, float, float]:
         """
         Calculate motor adjustments to center the mouth in camera view
-        Returns: (rotation_adjustment, elevation_adjustment, estimated_distance)
+
+        Camera-follows-aim targeting algorithm:
+        1. Calculate pixel offset from mouth center to camera center
+        2. Convert pixels to degrees using calibrated conversion factors
+        3. Apply distance scaling (closer targets need smaller adjustments)
+        4. Add mechanical offset compensation
+        5. Add distance-based pitch compensation for projectile trajectory
+
+        Args:
+            mouth_x, mouth_y: Center coordinates of detected mouth in pixels
+            face_width: Width of face in pixels (for distance estimation)
+
+        Returns:
+            (rotation_adjustment, pitch_adjustment, estimated_distance)
+            - rotation_adjustment: Horizontal angle change in degrees (-90 to +90)
+            - pitch_adjustment: Vertical angle change in degrees (0 to 60)
+            - estimated_distance: Distance to target in cm
         """
         # Calculate offset from center
         dx = mouth_x - self.center_x
@@ -309,41 +358,57 @@ class MouthDetector:
         # Convert pixel offset to approximate angle adjustment
         # This is empirical - you'll need to tune these values for your setup
         pixels_per_degree_rotation = 15 * distance_factor    # Adjust based on your camera/motor setup
-        pixels_per_degree_elevation = 12 * distance_factor   # Adjust based on your camera/motor setup
+        pixels_per_degree_pitch = 12 * distance_factor   # Adjust based on your camera/motor setup
 
         rotation_adjustment = dx / pixels_per_degree_rotation
-        elevation_adjustment = -dy / pixels_per_degree_elevation  # Negative because Y increases downward
+        pitch_adjustment = -dy / pixels_per_degree_pitch  # Negative because Y increases downward
 
         # Apply configured offsets
         rotation_adjustment += self.rotation_offset_degrees
-        elevation_adjustment += self.elevation_offset_degrees
+        pitch_adjustment += self.pitch_offset_degrees
 
-        # Add distance-based elevation compensation (closer targets need higher elevation)
-        distance_elevation_compensation = (200.0 - estimated_distance) * self.distance_elevation_factor / 100.0
-        elevation_adjustment += distance_elevation_compensation
+        # Add distance-based pitch compensation (closer targets need higher pitch)
+        distance_pitch_compensation = (200.0 - estimated_distance) * self.distance_pitch_factor / 100.0
+        pitch_adjustment += distance_pitch_compensation
 
         # Clamp to maximum adjustment per iteration
         rotation_adjustment = max(-self.max_adjustment_degrees,
                                 min(self.max_adjustment_degrees, rotation_adjustment))
-        elevation_adjustment = max(-self.max_adjustment_degrees,
-                                 min(self.max_adjustment_degrees, elevation_adjustment))
+        pitch_adjustment = max(-self.max_adjustment_degrees,
+                                 min(self.max_adjustment_degrees, pitch_adjustment))
 
-        return rotation_adjustment, elevation_adjustment, estimated_distance
+        return rotation_adjustment, pitch_adjustment, estimated_distance
 
 class OCTv2Server:
+    """
+    Main OCTv2 server handling iOS app communication and hardware control
+
+    Architecture:
+    - TCP server for iOS app communication (JSON protocol)
+    - ESP32 serial communication for motor control
+    - Computer vision for automatic mouth detection and targeting
+    - Camera streaming with detection overlays
+    """
+
     def __init__(self, host='0.0.0.0', port=8080):
         self.host = host
         self.port = port
         self.running = False
         self.clients = []
 
-        # Hardware components
+        # Initialize ESP32 controller (required - will block until connected)
+        logger.info("Initializing ESP32 controller...")
         self.esp32 = ESP32Controller()
-        self.mouth_detector = MouthDetector()
+
+        try:
+            self.mouth_detector = MouthDetector()
+        except Exception as e:
+            logger.error(f"Failed to initialize mouth detector: {e}")
+            self.mouth_detector = None
 
         # Hardware state
         self.current_rotation = 0.0  # -90 to +90 degrees
-        self.current_elevation = 30.0  # 0 to 60 degrees
+        self.current_pitch = 30.0  # 0 to 60 degrees
         self.is_auto_mode = False
         self.is_homed = False
         self.auto_fire_enabled = True
@@ -361,23 +426,47 @@ class OCTv2Server:
         self.setup_camera()
 
     def setup_camera(self):
-        """Initialize camera"""
+        """Initialize camera with error handling and fallback options"""
         if not CAMERA_AVAILABLE:
-            logger.info("Camera not available - simulating camera")
+            logger.warning("Picamera2 not available - running in simulation mode")
+            logger.info("Install with: sudo apt install python3-picamera2")
             return
 
-        try:
-            self.camera = Picamera2()
-            config = self.camera.create_preview_configuration(
-                main={"size": (640, 480)},
-                lores={"size": (320, 240)},
-                display="lores"
-            )
-            self.camera.configure(config)
-            self.camera.start()
-            logger.info("Camera initialized successfully")
-        except Exception as e:
-            logger.error(f"Camera setup failed: {e}")
+        max_retries = 3
+        for attempt in range(max_retries):
+            try:
+                self.camera = Picamera2()
+
+                # Configure camera with optimal settings for mouth detection
+                config = self.camera.create_preview_configuration(
+                    main={"size": (640, 480)},  # Good resolution for detection
+                    lores={"size": (320, 240)},  # Lower res for streaming
+                    display="lores"
+                )
+                self.camera.configure(config)
+                self.camera.start()
+
+                # Test camera by capturing a frame
+                test_frame = self.camera.capture_array()
+                if test_frame is not None and test_frame.size > 0:
+                    logger.info(f"Camera initialized successfully (attempt {attempt + 1})")
+                    return True
+
+            except Exception as e:
+                logger.warning(f"Camera initialization attempt {attempt + 1} failed: {e}")
+                if self.camera:
+                    try:
+                        self.camera.stop()
+                    except:
+                        pass
+                    self.camera = None
+
+                if attempt < max_retries - 1:
+                    time.sleep(1)
+
+        logger.error(f"Failed to initialize camera after {max_retries} attempts")
+        logger.info("System will run without camera functionality")
+        return False
 
     def start_server(self):
         """Start the TCP server"""
@@ -461,7 +550,7 @@ class OCTv2Server:
         elif action == 'aim_right':
             return self.aim_right()
         elif action == 'fire':
-            angle = command.get('angle', self.current_elevation)
+            angle = command.get('angle', self.current_pitch)
             return self.fire_oreo(angle)
         elif action == 'home':
             return self.home_device()
@@ -482,7 +571,7 @@ class OCTv2Server:
     def aim_left(self) -> Dict[str, Any]:
         """Move aim left by small increment"""
         new_rotation = max(-90, self.current_rotation - 5)
-        if self.esp32.move_to_position(new_rotation, self.current_elevation):
+        if self.esp32.move_to_position(new_rotation, self.current_pitch):
             self.current_rotation = new_rotation
             return {'status': 'success', 'rotation': self.current_rotation}
         return {'status': 'error', 'message': 'Failed to move left'}
@@ -490,28 +579,28 @@ class OCTv2Server:
     def aim_right(self) -> Dict[str, Any]:
         """Move aim right by small increment"""
         new_rotation = min(90, self.current_rotation + 5)
-        if self.esp32.move_to_position(new_rotation, self.current_elevation):
+        if self.esp32.move_to_position(new_rotation, self.current_pitch):
             self.current_rotation = new_rotation
             return {'status': 'success', 'rotation': self.current_rotation}
         return {'status': 'error', 'message': 'Failed to move right'}
 
-    def fire_oreo(self, elevation: float = None) -> Dict[str, Any]:
-        """Fire an Oreo at the specified elevation"""
-        if elevation is not None:
-            # Move to target elevation first
-            self.current_elevation = max(0, min(60, elevation))
-            if not self.esp32.move_to_position(self.current_rotation, self.current_elevation):
+    def fire_oreo(self, pitch: float = None) -> Dict[str, Any]:
+        """Fire an Oreo at the specified pitch"""
+        if pitch is not None:
+            # Move to target pitch first
+            self.current_pitch = max(0, min(60, pitch))
+            if not self.esp32.move_to_position(self.current_rotation, self.current_pitch):
                 return {'status': 'error', 'message': 'Failed to move to position'}
             time.sleep(0.5)  # Wait for positioning
 
-        logger.info(f"FIRING OREO at rotation={self.current_rotation}°, elevation={self.current_elevation}°!")
+        logger.info(f"FIRING OREO at rotation={self.current_rotation}°, pitch={self.current_pitch}°!")
 
         if self.esp32.fire_oreo():
             return {
                 'status': 'success',
-                'message': f'Oreo fired at R:{self.current_rotation}° E:{self.current_elevation}°',
+                'message': f'Oreo fired at R:{self.current_rotation}° E:{self.current_pitch}°',
                 'rotation': self.current_rotation,
-                'elevation': self.current_elevation
+                'pitch': self.current_pitch
             }
         else:
             return {'status': 'error', 'message': 'Fire mechanism failed'}
@@ -522,13 +611,13 @@ class OCTv2Server:
 
         if self.esp32.home_motors():
             self.current_rotation = 0.0
-            self.current_elevation = 0.0
+            self.current_pitch = 0.0
             self.is_homed = True
             return {
                 'status': 'success',
                 'message': 'Device homed successfully',
                 'rotation': 0,
-                'elevation': 0
+                'pitch': 0
             }
         else:
             return {'status': 'error', 'message': 'Homing failed'}
@@ -573,24 +662,24 @@ class OCTv2Server:
                             estimated_face_width = int(mw * 2.5)  # Face is roughly 2.5x wider than mouth
 
                             # Calculate centering adjustment
-                            rotation_adj, elevation_adj, distance = self.mouth_detector.calculate_centering_adjustment(
+                            rotation_adj, pitch_adj, distance = self.mouth_detector.calculate_centering_adjustment(
                                 mouth_center_x, mouth_center_y, estimated_face_width
                             )
 
                             logger.info(f"🎯 AUTO TARGET: Mouth detected (confidence {confidence:.2f}, distance ~{distance:.0f}cm)")
 
                             # Only adjust if mouth is not already centered
-                            if abs(rotation_adj) > 0.1 or abs(elevation_adj) > 0.1:
+                            if abs(rotation_adj) > 0.1 or abs(pitch_adj) > 0.1:
                                 # Calculate new position with adjustments
                                 new_rotation = max(-90, min(90, self.current_rotation + rotation_adj))
-                                new_elevation = max(0, min(60, self.current_elevation + elevation_adj))
+                                new_pitch = max(0, min(60, self.current_pitch + pitch_adj))
 
-                                logger.info(f"🎯 CENTERING: Adjusting R:{rotation_adj:+.1f}° E:{elevation_adj:+.1f}° -> R:{new_rotation:.1f}° E:{new_elevation:.1f}°")
+                                logger.info(f"🎯 CENTERING: Adjusting R:{rotation_adj:+.1f}° E:{pitch_adj:+.1f}° -> R:{new_rotation:.1f}° E:{new_pitch:.1f}°")
 
                                 # Move to center the target
-                                if self.esp32.move_to_position(new_rotation, new_elevation):
+                                if self.esp32.move_to_position(new_rotation, new_pitch):
                                     self.current_rotation = new_rotation
-                                    self.current_elevation = new_elevation
+                                    self.current_pitch = new_pitch
                                     time.sleep(0.5)  # Wait for positioning
                             else:
                                 logger.info("🎯 TARGET CENTERED: Mouth already in optimal position")
@@ -709,12 +798,12 @@ class OCTv2Server:
     def get_status(self) -> Dict[str, Any]:
         """Return current device status"""
         # Get actual position from ESP32
-        actual_rotation, actual_elevation = self.esp32.get_position()
+        actual_rotation, actual_pitch = self.esp32.get_position()
 
         return {
             'status': 'success',
             'rotation': actual_rotation,
-            'elevation': actual_elevation,
+            'pitch': actual_pitch,
             'auto_mode': self.is_auto_mode,
             'homed': self.is_homed,
             'streaming_clients': len(self.streaming_clients),