Implementing Position Hold using Optical flow sensor and ToF on Litewing ESP32 Drone

Published  February 6, 2026   0
User Avatar Dharagesh
Author
LiteWing Flight Positioning Module - Optical Position Hold Guide

The LiteWing Drone Flight Positioning Module enables autonomous position hold capabilities using optical flow sensors combined with dead reckoning algorithms. This guide explores how to implement a fully autonomous position hold system where the drone takes off, hovers at a target height, and actively maintains its position in space without manual intervention. This creates a hands-free flying experience where the drone stabilizes itself against environmental disturbances and holds its position with minimal drift. This tutorial is demonstrated using the Litewing ESP32 Drone and Litewing Drone Positioning Module

System Overview

The optical position hold system represents the foundation of autonomous flight control. Unlike manual control where you constantly adjust the drone's position, or joystick-assisted control where you command movements, this system operates completely autonomously. Once you initiate the flight sequence, the drone takes off to the target height and immediately begins working to maintain that exact position. The system continuously measures velocity using the optical flow sensor, integrates these measurements to estimate position through dead reckoning, and applies PID control corrections to counteract any detected drift or motion. This approach is ideal for applications requiring stable hover, aerial photography, or as a learning platform for understanding autonomous flight control algorithms.

Hardware Setup

LiteWing Flight Positioning Module - Drone Connection

Before working with the optical position hold system, ensure your LiteWing drone is properly equipped with the Flight Positioning Module and running compatible firmware. If you have not yet installed the Module or updated the firmware, refer to the Flight Positioning Module User Guide. That document provides complete hardware installation instructions and firmware flashing procedures.

Software Setup

Setting up your development environment is critical for working with the position hold system. You need Python installed with the cflib library properly configured, and you must be able to establish CRTP communication with the LiteWing drone over UDP. The cflib library has specific prerequisites that must be met. We recommend you follow the LiteWing Python SDK Programming Guide for detailed setup instructions. That guide covers installing Python dependencies, setting up cflib, connecting to the LiteWing Wi-Fi interface, and validating your communication setup with test scripts. Completing that setup is a prerequisite for this guide.

How Dead Reckoning Position Hold Works

The position hold system relies on dead reckoning, a technique that estimates position by integrating velocity measurements over time rather than receiving absolute position information from an external source like GPS. Dead reckoning builds up a relative position estimate starting from an arbitrary origin point that we define at a particular moment in the flight.

The core principle is straightforward. If you know how fast you're moving and in which direction, you can estimate how far you've traveled by multiplying velocity by time. The optical flow sensor provides velocity measurements in the X and Y directions (relative to the drone's body frame). By integrating these velocities at each sensor update, we estimate how far the drone has moved from its starting position

 

.The mathematical expression for this integration is:

Dead Reckoning FormulaDead-Reckoning Formula y

In these equations, Δt is the time interval between measurements, vx and vy represent velocities in meters per second To compute these values accurately, a height reference relative to the ground is required. This is obtained by combining data from an optical flow sensor (PMW3901MB) and a Time-of-Flight (ToF) sensor (VL53L1X). The detailed calculations for this process are described in our Optical Flow Tracking Guide. During the position-hold phase, this integration runs continuously, gradually building an estimate of the system’s position relative to the point at which integration started.

Once we have this position estimate, implementing position hold becomes conceptually simple. The system captures the current estimated position and treats it as the target position. From that point on, calculations run at approximately 50 Hz. First, we compute the position error, which is the difference between the target position and the current estimated position. Second, we compute the velocity error, which is how fast the drone is currently moving (we want zero velocity when holding position). These two error signals feed into PID controllers that compute velocity correction commands. The flight controller receives these corrections and adjusts thrust and tilt to counteract the detected errors.

The control law combines both position error and velocity error. Position error indicates how far we've drifted from the target:

Position Error Indicates Formula

Velocity error indicates how fast we're moving when we should be stationary:

Position Error Indicates Formula

The correction command combines contributions from both error signals:

Correctio Command

This dual-loop approach, called cascaded control, provides both position restoration and velocity damping. The position controller generates a restoring force that pulls the drone back toward the target position. The velocity controller provides damping that prevents oscillation and overshoot. Without velocity damping, the position controller might cause the drone to overshoot repeatedly, creating a bouncing behavior. The velocity term smooths this response.

One fundamental challenge of all dead reckoning systems is drift accumulation. Small inaccuracies in velocity measurements, when integrated over time, result in position errors that grow continuously. A velocity measurement error of just 1 cm/s causes a position error of 60 cm after one minute. To mitigate this without relying on absolute position references, we implement complementary mechanisms.

MechanismDescriptionPurpose / EffectKey Parameters
Velocity smoothing and thresholdingRaw optical flow velocity is filtered using an exponential moving average to reduce sample-to-sample noise. Very small velocity values are clamped to zero.Suppresses random noise while remaining responsive to real motion; prevents integrating noise when the drone is stationary.EMA smoothing factor; velocity threshold ≈ 0.005 m/s
Integral decayWhen velocity magnitude is near zero, a small decay term pulls the estimated position back toward the origin, acting like a weak spring.Gradually corrects drift during stationary periods without affecting motion tracking.Decay rate (tunable); applied only under low-velocity conditions
Periodic position resetThe integrated position estimate is reset to zero at fixed time intervals. The drone does not physically move; the origin is redefined.Prevents unbounded drift accumulation over long durations.Reset interval: 90 seconds

PID Control System

The position hold implementation uses cascaded PID controllers for both position and velocity. Although the default configuration uses proportional-only control (P-control), the full PID structure is available for tuning to optimize performance in your specific environment.

Understanding how these controllers work helps with effective tuning. A PID controller computes its output as the sum of three terms: proportional, integral, and derivative. The proportional term responds to current error magnitude. The integral term accumulates error over time and responds to persistent offsets. The derivative term responds to the rate of change of error and provides predictive damping.

Our position PID controller attempts to drive position error to zero. Its output is:

PID Control System Formula

The default gains for the position controller are:

GainDefault ValueEffect
POSITION_KP1.1Main restoring force toward target position
POSITION_KI0.03Eliminates steady-state offset 
POSITION_KD0.0Provides additional damping (disabled by default)

The proportional term provides the main restoring force. Higher proportional gain creates stronger position correction, pulling the drone back toward the target more aggressively. However, excessive gain causes instability and oscillation. Insufficient gain allows drift without adequate correction. A good starting range is 1.0 to 1.5, with the default of 1.2 typically working well.

Our velocity PID controller provides active damping by opposing residual velocity. Its output is:

PID Control System Formula

The default gains for the velocity controller are:

GainDefault ValueEffect
VELOCITY_KP1.2Active damping to prevent overshoot
VELOCITY_KI0.01Typically not needed
VELOCITY_KD0.0Typically not needed

The velocity controller acts as active damping. When the position controller commands a correction, it creates motion. The velocity controller opposes that motion, preventing overshoot and oscillation. This is why we can use relatively high position gains without instability, the velocity loop provides necessary damping.

The final velocity command sent to the drone combines both PID outputs:

total_correction_vx = u_pos_x + u_vel_x
total_correction_vy = u_pos_y + u_vel_y

These corrections are clamped to safe limits to prevent aggressive maneuvers that could destabilize the drone. The default limit is ±0.1 m/s, providing smooth, controlled corrections without sudden motions.

CRTP Communication for Position Hold

The optical position hold script communicates with the drone using CRTP log variables that stream sensor data at high frequency. We request three primary variables at 100 Hz (every 10 milliseconds) to provide sufficient update frequency for the control loop.

VariableTypePurpose
motion.deltaXint16_tPixel shift in X direction from optical flow sensor
motion.deltaYint16_tPixel shift in Y direction from optical flow sensor
stateEstimate.zfloatFused altitude estimate in meters

The motion deltas come directly from the PMW3901MB optical flow sensor and represent how many pixels the ground pattern has moved between consecutive sensor frames. The height value comes from the drone's state estimator, which fuses data from the VL53L1X Time-of-Flight sensor with IMU measurements to provide stable height readings.

To convert raw pixel deltas into velocities expressed in meters per second, we apply a geometry-based scaling calculation that accounts for the sensor's field of view, optical resolution, and sample rate. The conversion follows this formula:

velocity_constant = (4.4 * DEG_TO_RAD) / (30.0 * DT)
velocity = delta_value * altitude * velocity_constant

The constant 4.4 represents the effective field of view in degrees. The value 30 corresponds to the optical resolution (how many pixels span that field of view). DT is the sample period in seconds, typically 0.01 for our 10ms logging interval. This scaling produces velocities in meters per second for position integration and control calculations.

Code Walkthrough

The complete script implementation is available on the LiteWing GitHub repository under the Python-Scripts/Flight_Positioning_Shield/ directory. You can access it at: 

GitHub Link

LiteWing Flight Positioning Module FileLiteWing Flight Positioning Module Zip File

LiteWing Flight Positioning Module - GitHub Python Scripts

The file we're examining is named dead-reckoning-optical-position-hold.py. In the following sections, we'll walk through the key components to understand how everything fits together.

Configuration Parameters

The script begins with configuration parameters controlling the drone's connection, flight behavior, control timing, and PID tuning. These parameters are defined as constants at the top of the file, making them easy to locate and modify.

# Connection Configuration
DRONE_URI = "udp://192.168.43.42"
TARGET_HEIGHT = 0.3  # Target hover height in meters
TAKEOFF_TIME = 0.5   # Time allocated for takeoff sequence
HOVER_DURATION = 60.0  # How long to hover with position hold
LANDING_TIME = 0.5   # Time allocated for landing sequence
DEBUG_MODE = False   # Set to True to disable motors for testing
# Control Loop Timing
VELOCITY_SMOOTHING_ALPHA = 0.9  # Filtering strength for velocity
VELOCITY_THRESHOLD = 0.005       # Consider drone "stationary" below this
CONTROL_UPDATE_RATE = 0.02       # 50Hz control loop (20ms period)
SENSOR_PERIOD_MS = 10            # 100Hz sensor data streaming
DT = SENSOR_PERIOD_MS / 1000.0   # Convert to seconds
# Trim Corrections (manual bias adjustments)
TRIM_VX = 0.0
TRIM_VY = 0.0
# Position PID Controller Gains
POSITION_KP = 1.2
POSITION_KI = 0.0
POSITION_KD = 0.0
# Velocity PID Controller Gains
VELOCITY_KP = 1.2
VELOCITY_KI = 0.0
VELOCITY_KD = 0.0
# Control Limits and Safety
MAX_CORRECTION = 0.1              # Maximum velocity correction (m/s)
DRIFT_COMPENSATION_RATE = 0.004   # Position decay rate when stationary
MAX_POSITION_ERROR = 2.0          # Position estimate clamp limit
PERIODIC_RESET_INTERVAL = 90.0    # Reset integrated position periodically

Several parameters deserve special attention. 

ParameterPurposeNotes
CONTROL_UPDATE_RATESets how often position-hold calculations runAt 50 Hz (20 ms), provides responsive corrections without overloading the drone’s command interface
TARGET_HEIGHTDefines the desired hover height after takeoffUsed as the steady-state height for position hold
TRIM_VXCompensates for drift along the X axis 
TRIM_VYCompensates for drift along the Y axisApply a small bias if the drone drifts even when commanded to hold position
PID GainsControl aggressiveness of the position-hold controllerDefault values suit typical indoor environments with reliable optical flow
MAX_CORRECTIONLimits the maximum velocity commandActs as a safety clamp to prevent overly aggressive maneuvers during large position errors
PERIODIC_RESET_INTERVALResets the position estimate periodicallyPrevents unbounded drift accumulation over time

Global State Variables

The script maintains several categories of state variables tracking the complete control system status from raw sensor readings through computed position to PID controller internal state. Understanding what these variables represent helps you follow the control flow.

# Raw sensor data from the drone
current_height = 0.0
motion_delta_x = 0
motion_delta_y = 0
sensor_data_ready = False
current_battery_voltage = 0.0
battery_data_ready = False
# Computed velocities (m/s)
current_vx = 0.
current_vy = 0.0
velocity_x_history = [0.0, 0.0]  # For smoothing filter
velocity_y_history = [0.0, 0.0]
# Integrated position estimate (dead reckoning)
integrated_position_x = 0.0
integrated_position_y = 0.0
last_integration_time = time.time()
last_reset_time = time.time()
position_integration_enabled = False
# PID controller internal state
position_integral_x = 0.0
position_integral_y = 0.0
last_position_error_x = 0.0
last_position_error_y = 0.0
velocity_integral_x = 0.0
velocity_integral_y = 0.0
last_velocity_error_x = 0.0
last_velocity_error_y = 0.0
# Target position for hold mode
target_position_x = 0.0
target_position_y = 0.0
# Control outputs sent to drone
current_correction_vx = 0.0
current_correction_vy = 0.0
# Flight state
flight_phase = "IDLE"
flight_active = False

The sensor data variables hold the most recent values received from the drone. Velocity variables contain both raw and smoothed velocity estimates computed from optical flow deltas. Integrated position variables represent our dead reckoning estimate of where the drone is relative to the origin. PID state variables store integral accumulation and previous error values needed for I and D terms. Target position variables define where we want to hold. Finally, flight state variables track the current phase of operation.

Velocity Calculation and Smoothing

Raw optical flow deltas must be converted to velocities and smoothed before use. Two functions handle this processing.

def calculate_velocity(delta_value, altitude):
   """Convert optical flow delta to linear velocity"""
   if altitude <= 0:
       return 0.0
   if USE_HEIGHT_SCALING:
       velocity_constant = (4.4 * DEG_TO_RAD) / (30.0 * DT)
       velocity = delta_value * altitude * velocity_constant
   else:
       velocity = delta_value * OPTICAL_FLOW_SCALE * DT
   return velocity

The calculate_velocity function takes a raw pixel delta from the optical flow sensor and current altitude, then returns the corresponding linear velocity in meters per second. The calculation accounts for the sensor's 4.4-degree field of view, 30-pixel resolution, and logging sample period. If altitude is zero or negative, the function returns zero velocity because we cannot reliably compute velocity without valid altitude information.

def smooth_velocity(new_velocity, history):
   """Simple 2-point smoothing filter with adjustable alpha"""
   history[1] = history[0]
   history[0] = new_velocity
   alpha = VELOCITY_SMOOTHING_ALPHA
   smoothed = (history[0] * alpha) + (history[1] * (1 - alpha))
   if abs(smoothed) < VELOCITY_THRESHOLD:
       smoothed = 0.0
   return smoothed

The smooth_velocity function implements a simple exponential moving average filter. We maintain a two-point history for each axis, and the smoothed output is a weighted combination of current and previous values. The alpha parameter (typically 0.9) controls the balance between new and old data. Higher alpha produces smoother output with slightly more lag. After smoothing, we apply a threshold test: velocities smaller than 0.005 m/s are clamped to exactly zero. This prevents integrating noise when the drone is actually stationary.

Position Integration

Velocities are integrated over time to estimate position through dead reckoning. The integrate_position function handles this calculation along with drift compensation and bounds checking.

def integrate_position(vx, vy, dt):
   """Dead reckoning: integrate velocity to position"""
   global integrated_position_x, integrated_position_y
 
   # Sanity check on time step
   if dt <= 0 or dt > 0.1:
       return
 
   # Simple integration
   integrated_position_x += vx * dt
   integrated_position_y += vy * dt
 
   # Apply drift compensation when moving slowly
   velocity_magnitude = (vx * vx + vy * vy) ** 0.5
   if velocity_magnitude < VELOCITY_THRESHOLD * 2:
       integrated_position_x -= integrated_position_x * DRIFT_COMPENSATION_RATE * dt
       integrated_position_y -= integrated_position_y * DRIFT_COMPENSATION_RATE * dt
 
   # Clamp position error
   integrated_position_x = max(-MAX_POSITION_ERROR,
                               min(MAX_POSITION_ERROR, integrated_position_x))
   integrated_position_y = max(-MAX_POSITION_ERROR,
                               min(MAX_POSITION_ERROR, integrated_position_y))

The function begins by validating the time step. If dt is zero, negative, or unreasonably large (which could happen if logging temporarily paused), we skip integration to avoid creating a large jump in the position estimate. The core integration is straightforward: we add the product of velocity and time to the current position.

The drift compensation mechanism activates when velocity magnitude is very small, indicating the drone is nearly stationary. In this condition, we apply a small decay that gradually pulls the position estimate back toward zero. The decay rate is proportional to current position error, so larger errors decay faster. This acts like a weak centering spring that helps counteract long-term drift without interfering with tracking real motion.

Finally, we clamp the position estimate to reasonable bounds. If sensor errors or other problems cause integration to run away, the clamp prevents absurdly large position values. The default limit of ±2 meters is sufficient for typical indoor flight spaces.

Periodic Position Reset

To prevent unbounded drift accumulation in the dead reckoning system, we implement periodic position resets that completely zero the integrated position estimate at regular intervals.

def periodic_position_reset():
   """Reset integrated position periodically to prevent drift accumulation"""
   global integrated_position_x, integrated_position_y, last_reset_time
   current_time = time.time()
   if current_time - last_reset_time >= PERIODIC_RESET_INTERVAL:
       integrated_position_x = 0.0
       integrated_position_y = 0.0
       last_reset_time = current_time
       return True
   return False

This function checks if the configured reset interval has elapsed since the last reset. By default, this interval is 90 seconds. When the interval expires, we reset both position estimates to zero and update the last reset timestamp. The function returns True when a reset occurs, allowing the calling code to log this event. The reset is transparent to the physical drone, it doesn't move, we simply redefine the current location as the new origin and continue position hold from there.

Position Hold Reset

When starting position hold or transitioning between flight phases, we need to reset the control system state to ensure clean initialization.

def reset_position_tracking():
   """Reset integrated position tracking"""
   global integrated_position_x, integrated_position_y
   global last_integration_time, last_reset_time
   global position_integration_enabled
   global position_integral_x, position_integral_y
   global velocity_integral_x, velocity_integral_y
   global last_position_error_x, last_position_error_y
   global last_velocity_error_x, last_velocity_error_y
   global target_position_x, target_position_y
 
   integrated_position_x = 0.0
   integrated_position_y = 0.0
   target_position_x = 0.0
   target_position_y = 0.0
   last_integration_time = time.time()
   last_reset_time = time.time()
   position_integration_enabled = True
 
   # Reset PID state
   position_integral_x = 0.0
   position_integral_y = 0.0
   velocity_integral_x = 0.0
   velocity_integral_y = 0.0
   last_position_error_x = 0.0
   last_position_error_y = 0.0
   last_velocity_error_x = 0.0
   last_velocity_error_y = 0.0

This function performs a complete state reset. We set both current position estimate and target position to zero, effectively defining the origin at the drone's current location. All PID controller internal state gets cleared, including integral accumulators and stored previous errors. If we didn't clear these, the controller would "remember" errors from previous flights and incorrectly apply them to the new position hold attempt. We also enable position integration so the system begins tracking position immediately.

PID Control Calculation

The calculate_position_hold_corrections function implements the core position hold logic by combining position and velocity PID controllers to compute correction commands.

def calculate_position_hold_corrections():
   """Calculate control corrections using PID controllers"""
   global current_correction_vx, current_correction_vy
   global position_integral_x, position_integral_y
   global last_position_error_x, last_position_error_y
   global velocity_integral_x, velocity_integral_y
   global last_velocity_error_x, last_velocity_error_y
 
   if not sensor_data_ready or current_height <= 0:
       current_correction_vx = 0.0
       current_correction_vy = 0.0
       return 0.0, 0.0
 
   # Calculate position errors (negative because we want to correct toward target)
   position_error_x = -(integrated_position_x - target_position_x)
   position_error_y = -(integrated_position_y - target_position_y)
 
   # Calculate velocity errors (negative because we want to dampen velocity)
   velocity_error_x = -current_vx
   velocity_error_y = -current_vy
 
   # Position PID Controller
   # Proportional
   position_p_x = position_error_x * POSITION_KP
   position_p_y = position_error_y * POSITION_KP
   # Integral (with anti-windup)
   position_integral_x += position_error_x * CONTROL_UPDATE_RATE
   position_integral_y += position_error_y * CONTROL_UPDATE_RATE
   position_integral_x = max(-0.1, min(0.1, position_integral_x))
   position_integral_y = max(-0.1, min(0.1, position_integral_y))
   position_i_x = position_integral_x * POSITION_KI
   position_i_y = position_integral_y * POSITION_KI
   # Derivative
   position_derivative_x = (position_error_x - last_position_error_x) / CONTROL_UPDATE_RATE
   position_derivative_y = (position_error_y - last_position_error_y) / CONTROL_UPDATE_RATE
   position_d_x = position_derivative_x * POSITION_KD
   position_d_y = position_derivative_y * POSITION_KD
   last_position_error_x = position_error_x
   last_position_error_y = position_error_y
 
   # Velocity PID Controller
   # Proportional
   velocity_p_x = velocity_error_x * VELOCITY_KP
   velocity_p_y = velocity_error_y * VELOCITY_KP
   # Integral (with anti-windup)
   velocity_integral_x += velocity_error_x * CONTROL_UPDATE_RATE
   velocity_integral_y += velocity_error_y * CONTROL_UPDATE_RATE
   velocity_integral_x = max(-0.05, min(0.05, velocity_integral_x))
   velocity_integral_y = max(-0.05, min(0.05, velocity_integral_y))
   velocity_i_x = velocity_integral_x * VELOCITY_KI
   velocity_i_y = velocity_integral_y * VELOCITY_KI
   # Derivative
   velocity_derivative_x = (velocity_error_x - last_velocity_error_x) / CONTROL_UPDATE_RATE
   velocity_derivative_y = (velocity_error_y - last_velocity_error_y) / CONTROL_UPDATE_RATE
   velocity_d_x = velocity_derivative_x * VELOCITY_KD
   velocity_d_y = velocity_derivative_y * VELOCITY_KD
   last_velocity_error_x = velocity_error_x
   last_velocity_error_y = velocity_error_y
 
   # Combine PID outputs
   position_correction_vx = position_p_x + position_i_x + position_d_x
   position_correction_vy = position_p_y + position_i_y + position_d_y
   velocity_correction_vx = velocity_p_x + velocity_i_x + velocity_d_x
   velocity_correction_vy = velocity_p_y + velocity_i_y + velocity_d_y
 
   # Total corrections
   total_vx = position_correction_vx + velocity_correction_vx
   total_vy = position_correction_vy + velocity_correction_vy
 
   # Apply limits
   total_vx = max(-MAX_CORRECTION, min(MAX_CORRECTION, total_vx))
   total_vy = max(-MAX_CORRECTION, min(MAX_CORRECTION, total_vy))
 
   # Store for GUI display
   current_correction_vx = total_vx
   current_correction_vy = total_vy
   return total_vx, total_vy

This function implements the full cascaded PID architecture. We compute position errors by comparing where we are to where we want to be, and velocity errors by comparing current velocity to the desired velocity of zero. For each axis, we calculate the P, I, and D terms of both position and velocity controllers. Integral terms are accumulated over time and clamped to prevent windup. Derivative terms are computed as the rate of change of error. Finally, we sum all contributions from both controllers and apply a safety clamp to ensure commanded corrections stay within reasonable bounds.

Motion Sensor Callback

The motion_callback function receives optical flow data from the drone and processes it into velocity and position estimates.

def motion_callback(timestamp, data, logconf):
   """Motion sensor data callback"""
   global current_height, motion_delta_x, motion_delta_y, sensor_data_ready
   global current_vx, current_vy, last_integration_time
 
   # Get sensor data
   current_height = data.get("stateEstimate.z", 0)
   motion_delta_x = data.get("motion.deltaX", 0)
   motion_delta_y = data.get("motion.deltaY", 0)
   sensor_data_ready = True
 
   # Calculate velocities
   raw_velocity_x = calculate_velocity(motion_delta_x, current_height)
   raw_velocity_y = calculate_velocity(motion_delta_y, current_height)
 
   # Apply smoothing
   current_vx = smooth_velocity(raw_velocity_x, velocity_x_history)
   current_vy = smooth_velocity(raw_velocity_y, velocity_y_history)
 
   # Dead reckoning position integration
   current_time = time.time()
   dt = current_time - last_integration_time
   if 0.001 <= dt <= 0.1 and position_integration_enabled:
       integrate_position(current_vx, current_vy, dt)
   last_integration_time = current_time
 
   # Update history for GUI
   update_history()

This callback runs every time we receive new sensor data from the drone, which happens at 100 Hz. We extract altitude and optical flow deltas, convert deltas to velocities, apply smoothing, and integrate position. The time step validation ensures we only integrate when dt is reasonable, neither too small (which could indicate a duplicate packet) nor too large (which could indicate a communication gap). Position integration only occurs when position_integration_enabled is True, allowing us to disable integration during certain flight phases like takeoff and landing.

Flight Control Loop

The main flight control loop executes the complete position hold sequence from takeoff through landing.

def flight_thread_func(self):
   """Position hold flight thread"""
   global flight_active, flight_phase
   global position_integration_enabled
 
   # Initialize
   cflib.crtp.init_drivers()
   cf = Crazyflie(rw_cache="./cache")
 
   try:
       with SyncCrazyflie(DRONE_URI, cf=cf) as scf:
           flight_active = True
         
           # Setup logging
           log_motion, log_battery = setup_logging(cf, logger=self.log_to_output)
           time.sleep(1.0)
         
           # Reset position tracking
           reset_position_tracking()
         
           # Enable high-level commander
           if not DEBUG_MODE:
               cf.commander.send_setpoint(0, 0, 0, 0)
               time.sleep(0.1)
               cf.param.set_value("commander.enHighLevel", "1")
               time.sleep(0.5)
         
           # Takeoff
           flight_phase = "TAKEOFF"
           init_csv_logging(logger=self.log_to_output)
           start_time_local = time.time()
         
           while time.time() - start_time_local < TAKEOFF_TIME and flight_active:
               if not DEBUG_MODE:
                   cf.commander.send_hover_setpoint(TRIM_VX, TRIM_VY, 0, TARGET_HEIGHT)
               log_to_csv()
               time.sleep(0.01)
         
           # Stabilization
           flight_phase = "STABILIZING"
           stabilization_start = time.time()
           while time.time() - stabilization_start < 3.0 and flight_active:
               motion_vx, motion_vy = calculate_position_hold_corrections()
               log_to_csv()
               total_vx = TRIM_VX + motion_vy
               total_vy = TRIM_VY + motion_vx
               if not DEBUG_MODE:
                   cf.commander.send_hover_setpoint(total_vx, total_vy, 0, TARGET_HEIGHT)
               time.sleep(CONTROL_UPDATE_RATE)
         
           # Position Hold
           flight_phase = "POSITION_HOLD"
           hover_start = time.time()
           self.log_to_output(f"Position Hold active for {HOVER_DURATION:.0f}s")
         
           while time.time() - hover_start < HOVER_DURATION and flight_active:
               motion_vx, motion_vy = calculate_position_hold_corrections()
               if periodic_position_reset():
                   flight_phase = "POSITION_HOLD (RESET)"
                   self.log_to_output("Position reset to origin")
               else:
                   flight_phase = "POSITION_HOLD"
             
               log_to_csv()
               total_vx = TRIM_VX + motion_vy
               total_vy = TRIM_VY + motion_vx
               if not DEBUG_MODE:
                   cf.commander.send_hover_setpoint(total_vx, total_vy, 0, TARGET_HEIGHT)
               time.sleep(CONTROL_UPDATE_RATE)
         
           # Landing
           flight_phase = "LANDING"
           landing_start = time.time()
           while time.time() - landing_start < LANDING_TIME and flight_active:
               if not DEBUG_MODE:
                   cf.commander.send_hover_setpoint(TRIM_VX, TRIM_VY, 0, 0)
               log_to_csv()
               time.sleep(0.01)
         
           if not DEBUG_MODE:
               cf.commander.send_setpoint(0, 0, 0, 0)
           flight_phase = "COMPLETE"
         
   except Exception as e:
       flight_phase = "ERROR"
       self.log_to_output(f"Error: {str(e)}")
   finally:
       close_csv_logging(logger=self.log_to_output)
       flight_active = False

This function executes the complete flight sequence. It starts by initializing CRTP drivers and establishing a connection to the drone. After setting up sensor logging and enabling the high-level commander, it executes four distinct flight phases. During takeoff, the drone ascends to the target height using simple hover commands without position hold corrections. During stabilization, position hold calculations begin but the drone is still settling from the ascent. During the main position hold phase, the full control loop runs continuously, calculating corrections at 50 Hz and periodically resetting the position estimate. During landing, position hold is disabled and the drone descends using simple hover commands. Finally, motors are stopped and the flight completes.

GUI and Visualization

The script includes a comprehensive Tkinter-based graphical user interface that provides both control and monitoring capabilities. The interface is divided into several functional areas for parameter adjustment, system state observation, and flight path visualization.

LiteWing Flight Positioning Module - GUI Flight Parameters

The parameter adjustment panel allows modifying critical system parameters without editing code and restarting. You can change the target height, which determines how high the drone hovers after takeoff. The hover duration controls how long the position hold phase lasts. TRIM VX and TRIM VY parameters let you add small bias corrections if the drone consistently drifts in a particular direction. The PID gain controls allow adjusting both position and velocity controller gains. The "Apply All Values" button commits your parameter changes to the running system.

LiteWing Flight Positioning Module - GUI Real-Time Values

The real-time status display section shows current values of important system variables. You can see current height as measured by sensors, battery voltage (helps you know when to land), estimated velocities in X and Y, estimated position from dead reckoning, computed correction values being sent to the drone, and current flight phase. The flight phase indicator tells you whether the system is idle, in sensor test, taking off, stabilizing, holding position, or landing.

The trajectory visualization uses four matplotlib plots embedded in the Tkinter window. The first plot shows estimated velocities in X and Y over time, giving you a sense of how the drone is moving.

LiteWing Flight Positioning Module - Velocities Plot

The second plot displays the integrated 2D position estimate, this is your dead reckoning trajectory with current position marked by a red dot and the target position (origin) marked in yellow.

LiteWing Flight Positioning Module - Position Dead Reckoning Plot

The third plot shows control corrections being applied by the PID controller, helping you understand how hard the system is working to maintain position.

LiteWing Flight Positioning Module - Correction Value Plot

The fourth plot tracks height over time with a reference line indicating your target height.

LiteWing Flight Positioning Module - ToF Height Plot

The sensor test functionality provides a way to verify all sensors are working correctly before flight. When you click "Sensor Test", the system connects to the drone, sets up sensor logging, and begins streaming data without starting the motors. 

LiteWing Flight Positioning Module - GUI Sensor Test

This allows you to confirm the optical flow sensor is detecting motion (move the drone manually and watch velocity values change), the ToF sensor provides valid readings, and battery voltage is above the safe threshold. Once sensors are verified, you can proceed with the flight.

Tuning PID Parameters

The default P-only control configuration works well for basic position hold in most environments, but you may want to tune PID gains to optimize performance for your specific conditions. Understanding what each gain does and how to adjust it helps you achieve better position hold quality.

GainWhat It DoesIf Too LowIf Too HighTypical / Default Values
POSITION_KPDetermines how aggressively the drone moves back toward the target positionDrone drifts away with weak correctionOscillation and instability around the targetStart 1.0–1.5, default 1.2
POSITION_KIAccumulates error over time to remove steady-state offsetPersistent small offset from targetIntegral windup, instabilityStart 0.0, typical 0.01–0.05
POSITION_KDResponds to rate of position error change (predictive damping)Usually unnecessary in cascaded controlCan add noise and instabilityTypically 0.0
VELOCITY_KPProvides damping; resists motion to prevent overshootOvershoot, oscillationSluggish, overdamped responseDefault 1.2
VELOCITY_KICorrects steady-state velocity errorRarely neededCan destabilize system0.0
VELOCITY_KDReacts to velocity error rateNot required in most setupsNoise amplification0.0

Recommended PID Tuning Procedure:

StepActionWhat to ObserveAdjustment
1Start with P-only controlStable hover baselinePOSITION_KP = 1.2, VELOCITY_KP = 1.2, all others 0.0
2Check for oscillationDrone bounces back and forthDecrease POSITION_KP in 0.1 steps
3Check for driftDrone slowly wanders awayIncrease POSITION_KP in 0.1 steps
4Look for steady-state offsetDrone sits consistently off-targetAdd POSITION_KI (0.01-0.05)
5Evaluate overshootDrone passes target before settlingIncrease VELOCITY_KP
6Validate changesStable hover without oscillation or driftTest thoroughly before further tuning

 

Testing the System

Testing the optical position hold system should follow a structured progression, starting with sensor validation, moving to basic flight testing, and finally evaluating position hold quality. This staged approach helps isolate problems early and avoids unnecessary risk during flight.

Sensor Validation

Begin by launching the script and clicking the Sensor Test (ARM) button. This connects to the drone and starts streaming sensor data without spinning the motors. The GUI will report connection status and sensor availability while displaying live values that should respond to manual movement.

ParameterExpected Behavior (On Ground)
HeightClose to 0 m
Battery VoltageAbove LOW_BATTERY_THRESHOLD (≈ 2.9 V)
Velocity (X/Y)Changes when sliding the drone on a textured surface
Sensor StatusAll sensors report valid data

If the optical flow sensor is working correctly, velocity values will change when the drone is moved across a patterned surface. Once all readings look reasonable, click Stop Sensor Test to prepare for flight.

LiteWing Flight Positioning Module - GUI Overview

Basic Flight Testing

Before flight, adjust the target height if needed. For initial tests, keep this conservative, around 0.2 to 0.3 meters, until you’re confident in the system’s behavior. Ensure the flight area is clear, with at least 2 meters of open space and a textured surface below for reliable optical flow tracking.

Test StepWhat to Observe
Start Position HoldSmooth autonomous takeoff
Climb PhaseDrone reaches target height without oscillation
Hold PhaseDrone stabilizes before entering position hold
Trajectory PlotRed position marker stays near the yellow origin marker

When you click Start Position Hold, the drone will take off, stabilize at the target height, and enter the main hold loop. The position estimate should remain clustered near the origin in the GUI.

 

Evaluating Position Hold Quality

Evaluating performance means knowing what “good” looks like and recognizing signs of poor tuning or environmental issues. A properly tuned system will hold position with minimal correction and no consistent drift.

MetricGood Performance Indicator
Position ErrorWithin ±0.15 m of target
Velocity CorrectionsTypically below 0.05 m/s
Trajectory DriftNo consistent bias in one direction
Motion PatternMinor wandering, not continuous movement

Some slow wandering is normal due to dead-reckoning error accumulation, but it should not steadily pull the drone in a single direction. Consistent drift usually points to tuning issues, poor surface texture, or uneven lighting.

Troubleshooting Common Issues

Several common issues can affect position hold performance. Understanding how to diagnose and resolve these problems will help you achieve reliable operation.

IssueObserved BehaviorLikely CauseRecommended Action
Position Hold DriftThe drone drifts steadily in one direction even though position hold mode is enabled.

This behavior is usually caused by an unsuitable flight surface that lacks visible texture, operation outside the optical flow sensor’s effective height range, external air disturbances such as fans, or incorrect trim values that introduce directional bias(TRIM_VX, TRIM_VY).

Use a textured and non-reflective surface, keep the flight height between 0.08 and 3 meters, remove sources of airflow, and verify that TRIM_VX and TRIM_VY are correctly adjusted.
OscillationThe drone repeatedly overshoots and oscillates around the target position.This typically occurs when the position controller proportional gain is set too high, resulting in an overly aggressive response with insufficient damping.Gradually reduce POSITION_KP in steps of 0.1 until oscillation stops, and if necessary increase VELOCITY_KP to add damping and stabilize the response.
Sensor Data Not ReadyThe sensor test or flight initialization fails with a “Sensor data not ready” error message.The error indicates that the CRTP logging configuration failed due to incompatible firmware, missing log variables, improper shield installation, or network communication issues.Ensure the drone is running shield-compatible firmware with the required log variables, confirm the shield is properly connected, verify network connectivity by pinging the drone’s IP address, and restart both the drone and the PC before retrying.
Battery WarningsThe system refuses to arm or start flight and reports a low battery condition.The measured battery voltage is below the defined safety threshold required for reliable operation.Fully charge the battery before attempting flight, as operating below the LOW_BATTERY_THRESHOLD can lead to unexpected power loss and crashes.
Large Position ErrorsThe 2D trajectory plot shows the estimated position rapidly diverging from the origin.This behavior is usually caused by poor optical flow tracking due to an inappropriate surface, incorrect height, insufficient lighting, or a malfunctioning sensor.Move to a flight area with better surface texture and lighting, verify the height is within the supported range, and inspect the optical flow sensor if the issue persists.
Debug ModeControl logic and sensor data appear active while the motors remain inactive.This occurs when debug mode is intentionally enabled to prevent motor output during testing.Enable DEBUG_MODE in the GUI to safely test sensor logging, position estimation, and control algorithms without spinning the motors.

Document Index

Document

Description 

Height Measurement Guide

Understanding VL53L1X Time-of-Flight height sensing, data acquisition, and CRTP telemetry integration.

Optical Flow Tracking Guide

Using the PMW3901MB sensor for horizontal motion tracking, velocity estimation, and ground-referenced positioning.

IMU Sensor Guide

Overview of LiteWing’s onboard IMU, including orientation, angular rate, and acceleration data essential for stabilization and control.

NeoPixel Status Control Guide

Controlling WS2812B RGB LEDs on the Drone Flight Positioning Module for displaying flight status and visual debugging.

Joystick Control Guide 

Implementing joystick-based manual control and mapping inputs to LiteWing position behaviors.

Position Hold Guide

Sensor fusion and control strategies enabling the LiteWing drone to maintain a fixed position using onboard Flight Positioning sensors.

Maneuver Control Guide

Configuring and executing stabilized drone maneuvers using height, flow, and IMU feedback loops.

Complete Project Code

"""
Dead Reckoning Optical Position Hold - Standalone Script
=========================================================
A simplified standalone script for optical flow-based position hold.
Uses dead reckoning with PID controllers to maintain the drone's position
after takeoff.

This script is designed for learners to understand the core concepts
of position hold using optical flow sensors.

Features:
- Sensor Test to arm/prepare the drone
- Optical flow-based position hold
- Adjustable TRIM values, Height, and PID parameters
- Real-time position, velocity, and correction feedback
- Safety checks for battery and sensors

Usage:
1. Connect to the drone via UDP
2. Click "Sensor Test" to arm and verify sensors
3. Adjust parameters if needed
4. Click "Start Position Hold" to takeoff and hover
5. The drone will maintain its position automatically
6. Click "Stop" or press Enter for emergency stop

Author: Dharageswaran S
Version: 1.0
"""

import time
import threading
import cflib.crtp
from cflib.crazyflie import Crazyflie
from cflib.crazyflie.log import LogConfig
from cflib.crazyflie.syncCrazyflie import SyncCrazyflie
import tkinter as tk
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation
import csv
from datetime import datetime

# === CONFIGURATION PARAMETERS ===
DRONE_URI = "udp://192.168.43.42"
TARGET_HEIGHT = 0.3  # Target hover height in meters
TAKEOFF_TIME = 0.5  # Time to takeoff and stabilize
HOVER_DURATION = 60.0  # How long to hover with position hold (seconds)
LANDING_TIME = 0.5  # Time to land
DEBUG_MODE = False  # Set to True to disable motors (sensors and logging still work)

# Velocity and control parameters
VELOCITY_SMOOTHING_ALPHA = 0.9  # Filtering strength for velocity smoothing
VELOCITY_THRESHOLD = 0.005  # Consider drone "stationary" below this velocity
CONTROL_UPDATE_RATE = 0.02  # 50Hz control loop
SENSOR_PERIOD_MS = 10  # Motion sensor update rate
DT = SENSOR_PERIOD_MS / 1000.0

# Basic trim corrections (adjustable via UI)
TRIM_VX = 0.0  # Forward/backward trim correction
TRIM_VY = 0.0  # Left/right trim correction

# Battery monitoring
LOW_BATTERY_THRESHOLD = 2.9  # Low battery warning threshold in volts

# Height sensor safety
HEIGHT_SENSOR_MIN_CHANGE = 0.015  # Minimum height change expected during takeoff

# === DEAD RECKONING POSITION CONTROL PARAMETERS ===
# PID Controller Parameters (adjustable via UI)
POSITION_KP = 1.2
POSITION_KI = 0.0
POSITION_KD = 0.0
VELOCITY_KP = 1.2
VELOCITY_KI = 0.0
VELOCITY_KD = 0.0

# Control limits
MAX_CORRECTION = 0.1  # Maximum control correction allowed
DRIFT_COMPENSATION_RATE = 0.004  # Gentle pull toward zero when moving slowly
MAX_POSITION_ERROR = 2.0  # Clamp position error to prevent runaway
PERIODIC_RESET_INTERVAL = 90.0  # Reset integrated position periodically

# Velocity calculation constants
DEG_TO_RAD = 3.1415926535 / 180.0
OPTICAL_FLOW_SCALE = 4.4  # Empirical scaling factor
USE_HEIGHT_SCALING = True

# === GLOBAL VARIABLES ===
# Sensor data
current_height = 0.0
motion_delta_x = 0
motion_delta_y = 0
sensor_data_ready = False

# Battery voltage data
current_battery_voltage = 0.0
battery_data_ready = False

# Velocity tracking
current_vx = 0.0
current_vy = 0.0
velocity_x_history = [0.0, 0.0]
velocity_y_history = [0.0, 0.0]

# Dead reckoning position integration
integrated_position_x = 0.0
integrated_position_y = 0.0
last_integration_time = time.time()
last_reset_time = time.time()
position_integration_enabled = False

# Control corrections
current_correction_vx = 0.0
current_correction_vy = 0.0

# PID Controller state variables
position_integral_x = 0.0
position_integral_y = 0.0
last_position_error_x = 0.0
last_position_error_y = 0.0
velocity_integral_x = 0.0
velocity_integral_y = 0.0
last_velocity_error_x = 0.0
last_velocity_error_y = 0.0

# Target position for position hold
target_position_x = 0.0
target_position_y = 0.0

# Flight state
flight_phase = "IDLE"
flight_active = False
sensor_test_active = False
scf_instance = None

# Data history for plotting
max_history_points = 200
time_history = []
velocity_x_history_plot = []
velocity_y_history_plot = []
position_x_history = []
position_y_history = []
correction_vx_history = []
correction_vy_history = []
height_history = []
complete_trajectory_x = []
complete_trajectory_y = []
start_time = None

# CSV logging
log_file = None
log_writer = None
 

# === HELPER FUNCTIONS ===
def calculate_velocity(delta_value, altitude):
  """Convert optical flow delta to linear velocity"""
  if altitude <= 0:
      return 0.0
  if USE_HEIGHT_SCALING:
      velocity_constant = (4.4 * DEG_TO_RAD) / (30.0 * DT)
      velocity = delta_value * altitude * velocity_constant
  else:
      velocity = delta_value * OPTICAL_FLOW_SCALE * DT
  return velocity
 

def smooth_velocity(new_velocity, history):
  """Simple 2-point smoothing filter with adjustable alpha"""
  history[1] = history[0]
  history[0] = new_velocity
  alpha = VELOCITY_SMOOTHING_ALPHA
  smoothed = (history[0] * alpha) + (history[1] * (1 - alpha))
  if abs(smoothed) < VELOCITY_THRESHOLD:
      smoothed = 0.0
  return smoothed
 

def integrate_position(vx, vy, dt):
  """Dead reckoning: integrate velocity to position"""
  global integrated_position_x, integrated_position_y
  if dt <= 0 or dt > 0.1:
      return
  # Simple integration
  integrated_position_x += vx * dt
  integrated_position_y += vy * dt
  # Apply drift compensation when moving slowly
  velocity_magnitude = (vx * vx + vy * vy) ** 0.5
  if velocity_magnitude < VELOCITY_THRESHOLD * 2:
      integrated_position_x -= integrated_position_x * DRIFT_COMPENSATION_RATE * dt
      integrated_position_y -= integrated_position_y * DRIFT_COMPENSATION_RATE * dt
  # Clamp position error
  integrated_position_x = max(-MAX_POSITION_ERROR, min(MAX_POSITION_ERROR, integrated_position_x))
  integrated_position_y = max(-MAX_POSITION_ERROR, min(MAX_POSITION_ERROR, integrated_position_y))
 

def periodic_position_reset():
  """Reset integrated position periodically to prevent drift accumulation"""
  global integrated_position_x, integrated_position_y, last_reset_time
  current_time = time.time()
  if current_time - last_reset_time >= PERIODIC_RESET_INTERVAL:
      integrated_position_x = 0.0
      integrated_position_y = 0.0
      last_reset_time = current_time
      return True
  return False
 

def reset_position_tracking():
  """Reset integrated position tracking"""
  global integrated_position_x, integrated_position_y, last_integration_time, last_reset_time
  global position_integration_enabled
  global position_integral_x, position_integral_y, velocity_integral_x, velocity_integral_y
  global last_position_error_x, last_position_error_y, last_velocity_error_x, last_velocity_error_y
  global target_position_x, target_position_y
 
  integrated_position_x = 0.0
  integrated_position_y = 0.0
  target_position_x = 0.0
  target_position_y = 0.0
  last_integration_time = time.time()
  last_reset_time = time.time()
  position_integration_enabled = True
  # Reset PID state
  position_integral_x = 0.0
  position_integral_y = 0.0
  velocity_integral_x = 0.0
  velocity_integral_y = 0.0
  last_position_error_x = 0.0
  last_position_error_y = 0.0
  last_velocity_error_x = 0.0
  last_velocity_error_y = 0.0
 

def calculate_position_hold_corrections():
  """Calculate control corrections using PID controllers"""
  global current_correction_vx, current_correction_vy
  global position_integral_x, position_integral_y
  global last_position_error_x, last_position_error_y
  global velocity_integral_x, velocity_integral_y
  global last_velocity_error_x, last_velocity_error_y

  if not sensor_data_ready or current_height <= 0:
      current_correction_vx = 0.0
      current_correction_vy = 0.0
      return 0.0, 0.0

  # Calculate position errors (negative because we want to correct toward target)
  position_error_x = -(integrated_position_x - target_position_x)
  position_error_y = -(integrated_position_y - target_position_y)

  # Calculate velocity errors (negative because we want to dampen velocity)
  velocity_error_x = -current_vx
  velocity_error_y = -current_vy

  # Position PID Controller
  # Proportional
  position_p_x = position_error_x * POSITION_KP
  position_p_y = position_error_y * POSITION_KP
  # Integral (with anti-windup)
  position_integral_x += position_error_x * CONTROL_UPDATE_RATE
  position_integral_y += position_error_y * CONTROL_UPDATE_RATE
  position_integral_x = max(-0.1, min(0.1, position_integral_x))
  position_integral_y = max(-0.1, min(0.1, position_integral_y))
  position_i_x = position_integral_x * POSITION_KI
  position_i_y = position_integral_y * POSITION_KI
  # Derivative
  position_derivative_x = (position_error_x - last_position_error_x) / CONTROL_UPDATE_RATE
  position_derivative_y = (position_error_y - last_position_error_y) / CONTROL_UPDATE_RATE
  position_d_x = position_derivative_x * POSITION_KD
  position_d_y = position_derivative_y * POSITION_KD
  last_position_error_x = position_error_x
  last_position_error_y = position_error_y

  # Velocity PID Controller
  # Proportional
  velocity_p_x = velocity_error_x * VELOCITY_KP
  velocity_p_y = velocity_error_y * VELOCITY_KP
  # Integral (with anti-windup)
  velocity_integral_x += velocity_error_x * CONTROL_UPDATE_RATE
  velocity_integral_y += velocity_error_y * CONTROL_UPDATE_RATE
  velocity_integral_x = max(-0.05, min(0.05, velocity_integral_x))
  velocity_integral_y = max(-0.05, min(0.05, velocity_integral_y))
  velocity_i_x = velocity_integral_x * VELOCITY_KI
  velocity_i_y = velocity_integral_y * VELOCITY_KI
  # Derivative
  velocity_derivative_x = (velocity_error_x - last_velocity_error_x) / CONTROL_UPDATE_RATE
  velocity_derivative_y = (velocity_error_y - last_velocity_error_y) / CONTROL_UPDATE_RATE
  velocity_d_x = velocity_derivative_x * VELOCITY_KD
  velocity_d_y = velocity_derivative_y * VELOCITY_KD
  last_velocity_error_x = velocity_error_x
  last_velocity_error_y = velocity_error_y

  # Combine PID outputs
  position_correction_vx = position_p_x + position_i_x + position_d_x
  position_correction_vy = position_p_y + position_i_y + position_d_y
  velocity_correction_vx = velocity_p_x + velocity_i_x + velocity_d_x
  velocity_correction_vy = velocity_p_y + velocity_i_y + velocity_d_y

  # Total corrections
  total_vx = position_correction_vx + velocity_correction_vx
  total_vy = position_correction_vy + velocity_correction_vy

  # Apply limits
  total_vx = max(-MAX_CORRECTION, min(MAX_CORRECTION, total_vx))
  total_vy = max(-MAX_CORRECTION, min(MAX_CORRECTION, total_vy))

  # Store for GUI display
  current_correction_vx = total_vx
  current_correction_vy = total_vy
  return total_vx, total_vy
 

def update_history():
  """Update data history for plotting"""
  global start_time
  if start_time is None:
      start_time = time.time()
  current_time = time.time() - start_time
 
  # Add new data points
  time_history.append(current_time)
  velocity_x_history_plot.append(current_vx)
  velocity_y_history_plot.append(current_vy)
  position_x_history.append(integrated_position_x)
  position_y_history.append(integrated_position_y)
  correction_vx_history.append(current_correction_vx)
  correction_vy_history.append(current_correction_vy)
  height_history.append(current_height)
  complete_trajectory_x.append(integrated_position_x)
  complete_trajectory_y.append(integrated_position_y)
 
  # Trim history to max points
  if len(time_history) > max_history_points:
      time_history.pop(0)
      velocity_x_history_plot.pop(0)
      velocity_y_history_plot.pop(0)
      position_x_history.pop(0)
      position_y_history.pop(0)
      correction_vx_history.pop(0)
      correction_vy_history.pop(0)
      height_history.pop(0)
 

def motion_callback(timestamp, data, logconf):
  """Motion sensor data callback"""
  global current_height, motion_delta_x, motion_delta_y, sensor_data_ready
  global current_vx, current_vy, last_integration_time

  # Get sensor data
  current_height = data.get("stateEstimate.z", 0)
  motion_delta_x = data.get("motion.deltaX", 0)
  motion_delta_y = data.get("motion.deltaY", 0)
  sensor_data_ready = True

  # Calculate velocities
  raw_velocity_x = calculate_velocity(motion_delta_x, current_height)
  raw_velocity_y = calculate_velocity(motion_delta_y, current_height)

  # Apply smoothing
  current_vx = smooth_velocity(raw_velocity_x, velocity_x_history)
  current_vy = smooth_velocity(raw_velocity_y, velocity_y_history)

  # Dead reckoning position integration
  current_time = time.time()
  dt = current_time - last_integration_time
  if 0.001 <= dt <= 0.1 and position_integration_enabled:
      integrate_position(current_vx, current_vy, dt)
  last_integration_time = current_time

  # Update history for GUI
  update_history()
 

def battery_callback(timestamp, data, logconf):
  """Battery voltage data callback"""
  global current_battery_voltage, battery_data_ready
  current_battery_voltage = data.get("pm.vbat", 0.0)
  battery_data_ready = True
 

def setup_logging(cf, logger=None):
  """Setup motion sensor and battery voltage logging"""
  log_motion = LogConfig(name="Motion", period_in_ms=SENSOR_PERIOD_MS)
  log_battery = LogConfig(name="Battery", period_in_ms=500)

  try:
      toc = cf.log.toc.toc
      # Setup motion logging
      motion_variables = [
          ("motion.deltaX", "int16_t"),
          ("motion.deltaY", "int16_t"),
          ("stateEstimate.z", "float"),
      ]
      added_motion_vars = []
      for var_name, var_type in motion_variables:
          group, name = var_name.split(".")
          if group in toc and name in toc[group]:
              try:
                  log_motion.add_variable(var_name, var_type)
                  added_motion_vars.append(var_name)
              except Exception as e:
                  if logger:
                      logger(f"Failed to add motion variable {var_name}: {e}")
          else:
              if logger:
                  logger(f"Motion variable not found: {var_name}")

      if len(added_motion_vars) < 2:
          if logger:
              logger("ERROR: Not enough motion variables found!")
          return None, None

      # Setup battery logging
      battery_variables = [("pm.vbat", "float")]
      added_battery_vars = []
      for var_name, var_type in battery_variables:
          group, name = var_name.split(".")
          if group in toc and name in toc[group]:
              try:
                  log_battery.add_variable(var_name, var_type)
                  added_battery_vars.append(var_name)
              except Exception as e:
                  if logger:
                      logger(f"Failed to add battery variable {var_name}: {e}")
          else:
              if logger:
                  logger(f"Battery variable not found: {var_name}")

      # Setup callbacks
      log_motion.data_received_cb.add_callback(motion_callback)
      if len(added_battery_vars) > 0:
          log_battery.data_received_cb.add_callback(battery_callback)

      # Add configurations
      cf.log.add_config(log_motion)
      if len(added_battery_vars) > 0:
          cf.log.add_config(log_battery)

      time.sleep(0.5)

      # Validate configurations
      if not log_motion.valid:
          if logger:
              logger("ERROR: Motion log configuration invalid!")
          return None, None
      if len(added_battery_vars) > 0 and not log_battery.valid:
          if logger:
              logger("WARNING: Battery log configuration invalid!")
          log_battery = None

      # Start logging
      log_motion.start()
      if log_battery:
          log_battery.start()

      time.sleep(0.5)
      if logger:
          logger(f"Logging started - Motion: {len(added_motion_vars)} vars, Battery: {len(added_battery_vars)} vars")
      return log_motion, log_battery

  except Exception as e:
      error_msg = f"Logging setup failed: {str(e)}"
      if logger:
          logger(error_msg)
      raise Exception(error_msg)
 

def init_csv_logging(logger=None):
  """Initialize CSV logging"""
  global log_file, log_writer
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  log_filename = f"position_hold_log_{timestamp}.csv"
  log_file = open(log_filename, mode="w", newline="")
  log_writer = csv.writer(log_file)
  log_writer.writerow([
      "Timestamp (s)", "Position X (m)", "Position Y (m)",
      "Height (m)", "Velocity X (m/s)", "Velocity Y (m/s)",
      "Correction VX", "Correction VY"
  ])
  if logger:
      logger(f"Logging to CSV: {log_filename}")
 

def log_to_csv():
  """Log current state to CSV"""
  global log_writer, start_time
  if log_writer is None or start_time is None:
      return
  elapsed = time.time() - start_time
  log_writer.writerow([
      f"{elapsed:.3f}", f"{integrated_position_x:.6f}", f"{integrated_position_y:.6f}",
      f"{current_height:.6f}", f"{current_vx:.6f}", f"{current_vy:.6f}",
      f"{current_correction_vx:.6f}", f"{current_correction_vy:.6f}"
  ])
 

def close_csv_logging(logger=None):
  """Close CSV log file"""
  global log_file
  if log_file:
      log_file.close()
      log_file = None
      if logger:
          logger("CSV log closed.")
 

class PositionHoldGUI:
  """
  GUI for Optical Flow Position Hold
 
  This class provides a user interface for testing optical flow-based
  position hold with adjustable PID parameters.
  """
 
  def __init__(self, root):
      self.root = root
      self.root.title("Dead Reckoning Optical Position Hold")
      self.root.geometry("1200x800")

      # Control variables
      self.flight_thread = None
      self.flight_running = False
      self.sensor_test_thread = None
      self.sensor_test_running = False

      self.create_ui()
      self.setup_plots()

      # Start animation
      self.anim = animation.FuncAnimation(
          self.fig, self.update_plots, interval=100, cache_frame_data=False
      )

      # Bind keyboard events
      self.root.bind("<KeyPress>", self.on_key_press)
      self.root.focus_set()

  def create_ui(self):
      """Create the user interface"""
      # Control panel
      control_frame = tk.Frame(self.root)
      control_frame.pack(fill=tk.X, padx=10, pady=5)

      # Sensor Test button
      self.sensor_test_button = tk.Button(
          control_frame, text="Sensor Test (ARM)",
          command=self.start_sensor_test,
          bg="lightblue", fg="black", font=("Arial", 11, "bold"), width=16
      )
      self.sensor_test_button.pack(side=tk.LEFT, padx=5)

      # Start Position Hold button
      self.start_button = tk.Button(
          control_frame, text="Start Position Hold",
          command=self.start_flight,
          bg="green", fg="white", font=("Arial", 11, "bold"), width=16
      )
      self.start_button.pack(side=tk.LEFT, padx=5)

      # Debug mode checkbox
      self.debug_mode_var = tk.BooleanVar(value=DEBUG_MODE)
      tk.Checkbutton(
          control_frame, text="Debug Mode", variable=self.debug_mode_var,
          command=self.toggle_debug_mode, font=("Arial", 9)
      ).pack(side=tk.LEFT, padx=10)

      # Status display
      self.status_var = tk.StringVar(value="Status: Ready - Run Sensor Test to ARM")
      tk.Label(
          control_frame, textvariable=self.status_var,
          font=("Arial", 11, "bold"), fg="blue"
      ).pack(side=tk.LEFT, padx=20)

      # Main frame
      main_frame = tk.Frame(self.root)
      main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

      # Left side - Controls
      left_frame = tk.Frame(main_frame)
      left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))

      # === Flight Parameters Frame ===
      params_frame = tk.LabelFrame(left_frame, text="Flight Parameters", padx=10, pady=10)
      params_frame.pack(fill=tk.X, pady=5)

      # Target Height
      height_frame = tk.Frame(params_frame)
      height_frame.pack(fill=tk.X, pady=2)
      tk.Label(height_frame, text="Target Height (m):", width=16, anchor=tk.W).pack(side=tk.LEFT)
      self.height_entry_var = tk.StringVar(value=str(TARGET_HEIGHT))
      tk.Entry(height_frame, textvariable=self.height_entry_var, width=8).pack(side=tk.LEFT, padx=5)

      # Hover Duration
      duration_frame = tk.Frame(params_frame)
      duration_frame.pack(fill=tk.X, pady=2)
      tk.Label(duration_frame, text="Hover Duration (s):", width=16, anchor=tk.W).pack(side=tk.LEFT)
      self.duration_var = tk.StringVar(value=str(HOVER_DURATION))
      tk.Entry(duration_frame, textvariable=self.duration_var, width=8).pack(side=tk.LEFT, padx=5)

      # TRIM VX
      trim_vx_frame = tk.Frame(params_frame)
      trim_vx_frame.pack(fill=tk.X, pady=2)
      tk.Label(trim_vx_frame, text="TRIM VX:", width=16, anchor=tk.W).pack(side=tk.LEFT)
      self.trim_vx_var = tk.StringVar(value=str(TRIM_VX))
      tk.Entry(trim_vx_frame, textvariable=self.trim_vx_var, width=8).pack(side=tk.LEFT, padx=5)

      # TRIM VY
      trim_vy_frame = tk.Frame(params_frame)
      trim_vy_frame.pack(fill=tk.X, pady=2)
      tk.Label(trim_vy_frame, text="TRIM VY:", width=16, anchor=tk.W).pack(side=tk.LEFT)
      self.trim_vy_var = tk.StringVar(value=str(TRIM_VY))
      tk.Entry(trim_vy_frame, textvariable=self.trim_vy_var, width=8).pack(side=tk.LEFT, padx=5)

      # === PID Parameters Frame ===
      pid_frame = tk.LabelFrame(left_frame, text="PID Parameters", padx=10, pady=10)
      pid_frame.pack(fill=tk.X, pady=5)

      # Container for two columns
      pid_columns = tk.Frame(pid_frame)
      pid_columns.pack(fill=tk.X)

      # Left column - Position PID
      pos_column = tk.Frame(pid_columns)
      pos_column.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))

      tk.Label(pos_column, text="Position PID:", font=("Arial", 9, "bold")).pack(anchor=tk.W)
     
      pos_kp_frame = tk.Frame(pos_column)
      pos_kp_frame.pack(fill=tk.X, pady=1)
      tk.Label(pos_kp_frame, text="Kp:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.pos_kp_var = tk.StringVar(value=str(POSITION_KP))
      tk.Entry(pos_kp_frame, textvariable=self.pos_kp_var, width=6).pack(side=tk.LEFT)

      pos_ki_frame = tk.Frame(pos_column)
      pos_ki_frame.pack(fill=tk.X, pady=1)
      tk.Label(pos_ki_frame, text="Ki:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.pos_ki_var = tk.StringVar(value=str(POSITION_KI))
      tk.Entry(pos_ki_frame, textvariable=self.pos_ki_var, width=6).pack(side=tk.LEFT)

      pos_kd_frame = tk.Frame(pos_column)
      pos_kd_frame.pack(fill=tk.X, pady=1)
      tk.Label(pos_kd_frame, text="Kd:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.pos_kd_var = tk.StringVar(value=str(POSITION_KD))
      tk.Entry(pos_kd_frame, textvariable=self.pos_kd_var, width=6).pack(side=tk.LEFT)

      # Right column - Velocity PID
      vel_column = tk.Frame(pid_columns)
      vel_column.pack(side=tk.LEFT, fill=tk.Y)

      tk.Label(vel_column, text="Velocity PID:", font=("Arial", 9, "bold")).pack(anchor=tk.W)
     
      vel_kp_frame = tk.Frame(vel_column)
      vel_kp_frame.pack(fill=tk.X, pady=1)
      tk.Label(vel_kp_frame, text="Kp:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.vel_kp_var = tk.StringVar(value=str(VELOCITY_KP))
      tk.Entry(vel_kp_frame, textvariable=self.vel_kp_var, width=6).pack(side=tk.LEFT)

      vel_ki_frame = tk.Frame(vel_column)
      vel_ki_frame.pack(fill=tk.X, pady=1)
      tk.Label(vel_ki_frame, text="Ki:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.vel_ki_var = tk.StringVar(value=str(VELOCITY_KI))
      tk.Entry(vel_ki_frame, textvariable=self.vel_ki_var, width=6).pack(side=tk.LEFT)

      vel_kd_frame = tk.Frame(vel_column)
      vel_kd_frame.pack(fill=tk.X, pady=1)
      tk.Label(vel_kd_frame, text="Kd:", width=4, anchor=tk.W).pack(side=tk.LEFT)
      self.vel_kd_var = tk.StringVar(value=str(VELOCITY_KD))
      tk.Entry(vel_kd_frame, textvariable=self.vel_kd_var, width=6).pack(side=tk.LEFT)

      # Apply button (full width below columns)
      tk.Button(
          pid_frame, text="Apply All Values", command=self.apply_values,
          bg="green", fg="white", font=("Arial", 10)
      ).pack(pady=5)

      # === Real-time values display ===
      values_frame = tk.LabelFrame(left_frame, text="Real-Time Values", padx=10, pady=10)
      values_frame.pack(fill=tk.X, pady=5)

      self.height_var = tk.StringVar(value="Height: 0.000m")
      self.battery_var = tk.StringVar(value="Battery: N/A")
      self.vx_var = tk.StringVar(value="VX: 0.000 m/s")
      self.vy_var = tk.StringVar(value="VY: 0.000 m/s")
      self.pos_x_var = tk.StringVar(value="Pos X: 0.000m")
      self.pos_y_var = tk.StringVar(value="Pos Y: 0.000m")
      self.corr_vx_var = tk.StringVar(value="Corr VX: 0.000")
      self.corr_vy_var = tk.StringVar(value="Corr VY: 0.000")
      self.phase_var = tk.StringVar(value="Phase: IDLE")

      tk.Label(values_frame, textvariable=self.height_var, font=("Arial", 10), fg="blue").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.battery_var, font=("Arial", 10), fg="orange").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.vx_var, font=("Arial", 10)).pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.vy_var, font=("Arial", 10)).pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.pos_x_var, font=("Arial", 10), fg="darkgreen").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.pos_y_var, font=("Arial", 10), fg="darkgreen").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.corr_vx_var, font=("Arial", 10), fg="red").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.corr_vy_var, font=("Arial", 10), fg="red").pack(anchor=tk.W)
      tk.Label(values_frame, textvariable=self.phase_var, font=("Arial", 10, "bold"), fg="purple").pack(anchor=tk.W)

      # === Output log ===
      output_frame = tk.LabelFrame(left_frame, text="Output Log", padx=5, pady=5)
      output_frame.pack(fill=tk.BOTH, expand=True, pady=5)

      self.output_text = tk.Text(output_frame, height=8, width=40, font=("Consolas", 9))
      self.output_text.pack(fill=tk.BOTH, expand=True)

      # Right side - Plots
      right_frame = tk.Frame(main_frame)
      right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))

      self.fig = Figure(figsize=(9, 7))
      self.canvas = FigureCanvasTkAgg(self.fig, master=right_frame)
      self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

  def setup_plots(self):
      """Setup matplotlib plots"""
      self.ax1 = self.fig.add_subplot(2, 2, 1)  # Velocities
      self.ax2 = self.fig.add_subplot(2, 2, 2)  # Position (2D)
      self.ax3 = self.fig.add_subplot(2, 2, 3)  # Corrections
      self.ax4 = self.fig.add_subplot(2, 2, 4)  # Height

      # Velocities
      self.ax1.set_title("Velocities")
      self.ax1.set_xlabel("Time (s)")
      self.ax1.set_ylabel("Velocity (m/s)")
      self.ax1.grid(True, alpha=0.3)
      (self.line_vx,) = self.ax1.plot([], [], "b-", label="VX")
      (self.line_vy,) = self.ax1.plot([], [], "r-", label="VY")
      self.ax1.legend()

      # 2D Position
      self.ax2.set_title("Position (Dead Reckoning)")
      self.ax2.set_xlabel("X (m)")
      self.ax2.set_ylabel("Y (m)")
      self.ax2.set_aspect("equal")
      self.ax2.grid(True, alpha=0.3)
      (self.line_pos,) = self.ax2.plot([], [], "purple", alpha=0.7, label="Trajectory")
      (self.current_pos,) = self.ax2.plot([], [], "ro", markersize=8, label="Current")
      self.ax2.plot(0, 0, "ko", markersize=10, markerfacecolor="yellow", label="Target")
      self.ax2.legend()

      # Corrections
      self.ax3.set_title("Control Corrections")
      self.ax3.set_xlabel("Time (s)")
      self.ax3.set_ylabel("Correction")
      self.ax3.grid(True, alpha=0.3)
      (self.line_corr_vx,) = self.ax3.plot([], [], "g-", label="Corr VX")
      (self.line_corr_vy,) = self.ax3.plot([], [], "m-", label="Corr VY")
      self.ax3.legend()

      # Height
      self.ax4.set_title("Height")
      self.ax4.set_xlabel("Time (s)")
      self.ax4.set_ylabel("Height (m)")
      self.ax4.grid(True, alpha=0.3)
      (self.line_height,) = self.ax4.plot([], [], "orange", label="Height")
      self.target_height_line = self.ax4.axhline(y=TARGET_HEIGHT, color="red", linestyle="--", alpha=0.7, label="Target")
      self.ax4.legend()

      self.fig.tight_layout()

  def update_plots(self, frame):
      """Update all plots with new data"""
      if not time_history:
          return []

      # Update value displays
      self.height_var.set(f"Height: {current_height:.3f}m")
      self.phase_var.set(f"Phase: {flight_phase}")
      if current_battery_voltage > 0:
          self.battery_var.set(f"Battery: {current_battery_voltage:.2f}V")
      self.vx_var.set(f"VX: {current_vx:.3f} m/s")
      self.vy_var.set(f"VY: {current_vy:.3f} m/s")
      self.pos_x_var.set(f"Pos X: {integrated_position_x:.3f}m")
      self.pos_y_var.set(f"Pos Y: {integrated_position_y:.3f}m")
      self.corr_vx_var.set(f"Corr VX: {current_correction_vx:.3f}")
      self.corr_vy_var.set(f"Corr VY: {current_correction_vy:.3f}")

      # Update plots
      try:
          self.line_vx.set_data(time_history, velocity_x_history_plot)
          self.line_vy.set_data(time_history, velocity_y_history_plot)

          if complete_trajectory_x and complete_trajectory_y:
              plot_x = [-x for x in complete_trajectory_x]
              self.line_pos.set_data(plot_x, complete_trajectory_y)
              self.current_pos.set_data([-integrated_position_x], [integrated_position_y])

          self.line_corr_vx.set_data(time_history, correction_vx_history)
          self.line_corr_vy.set_data(time_history, correction_vy_history)
          self.line_height.set_data(time_history, height_history)

          # Adjust axis limits
          if len(time_history) > 1:
              for ax in [self.ax1, self.ax3, self.ax4]:
                  ax.set_xlim(min(time_history), max(time_history))

              if velocity_x_history_plot and velocity_y_history_plot:
                  all_vel = velocity_x_history_plot + velocity_y_history_plot
                  if any(v != 0 for v in all_vel):
                      self.ax1.set_ylim(min(all_vel) - 0.01, max(all_vel) + 0.01)

              if complete_trajectory_x and complete_trajectory_y:
                  plot_x = [-x for x in complete_trajectory_x]
                  margin = max(max(plot_x) - min(plot_x), max(complete_trajectory_y) - min(complete_trajectory_y), 0.02) * 0.6
                  center_x = (max(plot_x) + min(plot_x)) / 2
                  center_y = (max(complete_trajectory_y) + min(complete_trajectory_y)) / 2
                  self.ax2.set_xlim(center_x - margin, center_x + margin)
                  self.ax2.set_ylim(center_y - margin, center_y + margin)

              if correction_vx_history and correction_vy_history:
                  all_corr = correction_vx_history + correction_vy_history
                  if any(c != 0 for c in all_corr):
                      self.ax3.set_ylim(min(all_corr) - 0.01, max(all_corr) + 0.01)

              if height_history:
                  self.ax4.set_ylim(min(height_history) - 0.05, max(height_history) + 0.05)

      except Exception:
          pass

      return [self.line_vx, self.line_vy, self.line_pos, self.current_pos,
              self.line_corr_vx, self.line_corr_vy, self.line_height]

  def log_to_output(self, message):
      """Log a message to the output window"""
      timestamp = time.strftime("%H:%M:%S")
      self.output_text.insert(tk.END, f"[{timestamp}] {message}\n")
      self.output_text.see(tk.END)

  def clear_output(self):
      """Clear the output log"""
      self.output_text.delete(1.0, tk.END)

  def clear_graphs(self):
      """Clear all graph data"""
      global time_history, velocity_x_history_plot, velocity_y_history_plot
      global position_x_history, position_y_history, height_history
      global correction_vx_history, correction_vy_history
      global complete_trajectory_x, complete_trajectory_y, start_time

      time_history.clear()
      velocity_x_history_plot.clear()
      velocity_y_history_plot.clear()
      position_x_history.clear()
      position_y_history.clear()
      correction_vx_history.clear()
      correction_vy_history.clear()
      height_history.clear()
      complete_trajectory_x.clear()
      complete_trajectory_y.clear()
      start_time = None

  def apply_values(self):
      """Apply all parameter values from UI"""
      global TARGET_HEIGHT, HOVER_DURATION, TRIM_VX, TRIM_VY
      global POSITION_KP, POSITION_KI, POSITION_KD
      global VELOCITY_KP, VELOCITY_KI, VELOCITY_KD
     
      try:
          TARGET_HEIGHT = float(self.height_entry_var.get())
          HOVER_DURATION = float(self.duration_var.get())
          TRIM_VX = float(self.trim_vx_var.get())
          TRIM_VY = float(self.trim_vy_var.get())
          POSITION_KP = float(self.pos_kp_var.get())
          POSITION_KI = float(self.pos_ki_var.get())
          POSITION_KD = float(self.pos_kd_var.get())
          VELOCITY_KP = float(self.vel_kp_var.get())
          VELOCITY_KI = float(self.vel_ki_var.get())
          VELOCITY_KD = float(self.vel_kd_var.get())
         
          # Update height line in plot
          self.target_height_line.set_ydata([TARGET_HEIGHT, TARGET_HEIGHT])
         
          self.log_to_output(f"Applied: Height={TARGET_HEIGHT:.2f}m, Duration={HOVER_DURATION:.0f}s")
          self.log_to_output(f"TRIM: VX={TRIM_VX:.2f}, VY={TRIM_VY:.2f}")
          self.log_to_output(f"Pos PID: Kp={POSITION_KP}, Ki={POSITION_KI}, Kd={POSITION_KD}")
          self.log_to_output(f"Vel PID: Kp={VELOCITY_KP}, Ki={VELOCITY_KI}, Kd={VELOCITY_KD}")
          self.status_var.set("Status: Values applied")
      except ValueError as e:
          self.status_var.set(f"Status: Invalid value - {str(e)}")
          self.log_to_output(f"Error: {str(e)}")

  def toggle_debug_mode(self):
      """Toggle debug mode"""
      global DEBUG_MODE
      DEBUG_MODE = self.debug_mode_var.get()
      mode_text = "ON (motors disabled)" if DEBUG_MODE else "OFF"
      self.log_to_output(f"Debug mode: {mode_text}")

  # ==================== SENSOR TEST ====================
  def start_sensor_test(self):
      """Start sensor test to ARM the drone"""
      if not self.sensor_test_running and not self.flight_running:
          self.sensor_test_running = True
          self.sensor_test_button.config(text="Stop Sensor Test", command=self.stop_sensor_test, bg="red")
          self.status_var.set("Status: Sensor Test Running - ARMING...")
          self.sensor_test_thread = threading.Thread(target=self.sensor_test_thread_func)
          self.sensor_test_thread.daemon = True
          self.sensor_test_thread.start()
      elif self.flight_running:
          self.status_var.set("Status: Cannot run Sensor Test during flight")

  def stop_sensor_test(self):
      """Stop sensor test"""
      global sensor_test_active
      if self.sensor_test_running:
          sensor_test_active = False
          self.sensor_test_running = False
          if self.sensor_test_thread and self.sensor_test_thread.is_alive():
              self.sensor_test_thread.join(timeout=2.0)
          self.status_var.set("Status: Sensor Test Stopped - ARMED ✓")
          self.sensor_test_button.config(text="Sensor Test (ARM)", command=self.start_sensor_test, bg="lightblue")

  def sensor_test_thread_func(self):
      """Sensor test thread"""
      global flight_phase, sensor_test_active, scf_instance
      global current_battery_voltage, battery_data_ready

      self.root.after(0, self.clear_output)
      self.root.after(0, self.clear_graphs)

      sensor_test_active = True
      flight_phase = "SENSOR_TEST"

      current_battery_voltage = 0.0
      battery_data_ready = False

      cflib.crtp.init_drivers()
      cf = Crazyflie(rw_cache="./cache")
      log_motion = None
      log_battery = None

      try:
          with SyncCrazyflie(DRONE_URI, cf=cf) as scf:
              scf_instance = scf

              log_motion, log_battery = setup_logging(cf, logger=self.log_to_output)
              if log_motion is not None:
                  time.sleep(1.0)

              if not DEBUG_MODE:
                  cf.commander.send_setpoint(0, 0, 0, 0)
                  time.sleep(0.1)
                  cf.param.set_value("commander.enHighLevel", "1")
                  time.sleep(0.5)

              reset_position_tracking()

              self.log_to_output("Sensor test running - reading sensors...")
              while sensor_test_active:
                  if sensor_data_ready:
                      calculate_position_hold_corrections()
                  time.sleep(CONTROL_UPDATE_RATE)

      except Exception as e:
          flight_phase = "ERROR"
          self.log_to_output(f"Sensor Test Error: {str(e)}")
      finally:
          if log_motion:
              try:
                  log_motion.stop()
              except:
                  pass
          if log_battery:
              try:
                  log_battery.stop()
              except:
                  pass
          sensor_test_active = False
          flight_phase = "IDLE"
          self.sensor_test_running = False
          self.root.after(0, lambda: self.sensor_test_button.config(
              text="Sensor Test (ARM)", command=self.start_sensor_test, bg="lightblue"
          ))
          self.root.after(0, lambda: self.status_var.set("Status: Sensor Test Stopped - ARMED ✓"))

  # ==================== POSITION HOLD FLIGHT ====================
  def start_flight(self):
      """Start position hold flight"""
      if self.flight_running or self.sensor_test_running:
          if self.sensor_test_running:
              self.status_var.set("Status: Stop Sensor Test first!")
          return

      # Apply current values
      self.apply_values()

      # Safety checks
      if current_battery_voltage > 0 and current_battery_voltage < LOW_BATTERY_THRESHOLD:
          self.status_var.set(f"Status: Battery too low ({current_battery_voltage:.2f}V)!")
          return
      elif current_battery_voltage == 0.0:
          self.log_to_output("WARNING: Battery unknown - run Sensor Test first!")
          self.status_var.set("Status: Run Sensor Test first!")
          return

      if not sensor_data_ready:
          self.status_var.set("Status: Sensor data not ready - Run Sensor Test first!")
          return

      if current_height <= 0.0:
          self.status_var.set("Status: Invalid height reading!")
          return

      # Start flight
      self.flight_running = True
      self.start_button.config(text="Stop Flight", command=self.emergency_stop, bg="red")
      self.status_var.set("Status: Starting Position Hold Flight...")
      self.log_to_output("Position Hold flight started")

      self.flight_thread = threading.Thread(target=self.flight_thread_func)
      self.flight_thread.daemon = True
      self.flight_thread.start()

  def emergency_stop(self):
      """Emergency stop"""
      global flight_active, sensor_test_active
      flight_active = False
      sensor_test_active = False
      self.flight_running = False
      self.sensor_test_running = False

      self.start_button.config(text="Start Position Hold", command=self.start_flight, bg="green")
      self.sensor_test_button.config(text="Sensor Test (ARM)", command=self.start_sensor_test, bg="lightblue")
      self.status_var.set("Status: EMERGENCY STOPPED")
      self.log_to_output("EMERGENCY STOP!")

  def flight_thread_func(self):
      """Position hold flight thread"""
      global flight_active, flight_phase
      global current_battery_voltage, battery_data_ready
      global position_integration_enabled

      self.root.after(0, self.clear_output)
      self.root.after(0, self.clear_graphs)

      cflib.crtp.init_drivers()
      cf = Crazyflie(rw_cache="./cache")
      log_motion = None
      log_battery = None

      current_battery_voltage = 0.0
      battery_data_ready = False

      try:
          with SyncCrazyflie(DRONE_URI, cf=cf) as scf:
              scf_instance = scf
              flight_active = True

              log_motion, log_battery = setup_logging(cf, logger=self.log_to_output)
              use_position_hold = log_motion is not None
              if use_position_hold:
                  time.sleep(1.0)

              reset_position_tracking()

              if not DEBUG_MODE:
                  cf.commander.send_setpoint(0, 0, 0, 0)
                  time.sleep(0.1)
                  cf.param.set_value("commander.enHighLevel", "1")
                  time.sleep(0.5)
              else:
                  self.log_to_output("DEBUG MODE: Motors disabled")

              # Takeoff
              flight_phase = "TAKEOFF"
              start_time_local = time.time()
              init_csv_logging(logger=self.log_to_output)

              while time.time() - start_time_local < TAKEOFF_TIME and flight_active:
                  if not DEBUG_MODE:
                      cf.commander.send_hover_setpoint(TRIM_VX, TRIM_VY, 0, TARGET_HEIGHT)
                  log_to_csv()
                  time.sleep(0.01)

              # Stabilization
              flight_phase = "STABILIZING"
              stabilization_start = time.time()
              while time.time() - stabilization_start < 3.0 and flight_active:
                  if use_position_hold and sensor_data_ready:
                      motion_vx, motion_vy = calculate_position_hold_corrections()
                  else:
                      motion_vx, motion_vy = 0.0, 0.0
                  log_to_csv()
                  total_vx = TRIM_VX + motion_vy
                  total_vy = TRIM_VY + motion_vx
                  if not DEBUG_MODE:
                      cf.commander.send_hover_setpoint(total_vx, total_vy, 0, TARGET_HEIGHT)
                  time.sleep(CONTROL_UPDATE_RATE)

              # Position Hold
              flight_phase = "POSITION_HOLD"
              hover_start = time.time()
              self.log_to_output(f"Position Hold active for {HOVER_DURATION:.0f}s")

              while time.time() - hover_start < HOVER_DURATION and flight_active:
                  if use_position_hold and sensor_data_ready:
                      motion_vx, motion_vy = calculate_position_hold_corrections()
                      if periodic_position_reset():
                          flight_phase = "POSITION_HOLD (RESET)"
                          self.log_to_output("Position reset to origin")
                      else:
                          flight_phase = "POSITION_HOLD"
                  else:
                      motion_vx, motion_vy = 0.0, 0.0

                  log_to_csv()
                  total_vx = TRIM_VX + motion_vy
                  total_vy = TRIM_VY + motion_vx
                  if not DEBUG_MODE:
                      cf.commander.send_hover_setpoint(total_vx, total_vy, 0, TARGET_HEIGHT)
                  time.sleep(CONTROL_UPDATE_RATE)

              # Landing
              flight_phase = "LANDING"
              landing_start = time.time()
              while time.time() - landing_start < LANDING_TIME and flight_active:
                  if not DEBUG_MODE:
                      cf.commander.send_hover_setpoint(TRIM_VX, TRIM_VY, 0, 0)
                  log_to_csv()
                  time.sleep(0.01)

              if not DEBUG_MODE:
                  cf.commander.send_setpoint(0, 0, 0, 0)
              flight_phase = "COMPLETE"
              self.log_to_output("Flight complete")

      except Exception as e:
          flight_phase = "ERROR"
          self.log_to_output(f"Error: {str(e)}")
      finally:
          close_csv_logging(logger=self.log_to_output)
          if log_motion:
              try:
                  log_motion.stop()
              except:
                  pass
          if log_battery:
              try:
                  log_battery.stop()
              except:
                  pass
          flight_active = False
          self.flight_running = False
          self.root.after(0, lambda: self.start_button.config(
              text="Start Position Hold", command=self.start_flight, bg="green"
          ))
          self.root.after(0, lambda: self.status_var.set("Status: Flight Complete"))

  def on_key_press(self, event):
      """Handle key press events"""
      if event.keysym in ("Return", "KP_Enter"):
          self.emergency_stop()
          self.log_to_output("EMERGENCY STOP: Enter key pressed")
 

def main():
  """Main entry point"""
  try:
      cflib.crtp.init_drivers()
      print("Crazyflie CRTP drivers initialized")
  except Exception as e:
      print(f"Warning: cflib.crtp.init_drivers() failed: {e}")

  root = tk.Tk()
  app = PositionHoldGUI(root)

  def on_closing():
      global flight_active, sensor_test_active
      flight_active = False
      sensor_test_active = False
      root.quit()
      root.destroy()

  root.protocol("WM_DELETE_WINDOW", on_closing)
  root.mainloop()
 

if __name__ == "__main__":
  main()
 
Have any question related to this Article?

Add New Comment

Login to Comment Sign in with Google Log in with Facebook Sign in with GitHub