AI Grand Prix — Race Pipeline Deep Dive

State machine, gate tracking, control modes, and timing budgets

1. State Machine Overview

The race controller is a finite state machine with six discrete phases. Every frame, exactly one phase is active. Transitions are deterministic — no priority conflicts, no deferred transitions.

INIT TAKEOFF SEEK_GATE APPROACH_GATE TRANSIT_GATE EMERGENCY FINISHED telemetry OK alt OK gate <15m lost 15 frames <1.5m + closing gate passed, next gate 30s timeout all gates passed 30s after last transit Normal phase Transition phase Emergency Finished Conditional / fallback
Design note: There is no RECOVERY phase. When gate detection is lost during APPROACH, the machine transitions directly back to SEEK_GATE. Recovery-style re-acquisition is handled implicitly by the SEEK yaw scan. This keeps the state machine minimal and eliminates ambiguous intermediate states.

2. Phase Descriptions

INIT STARTUP

Establishes connection to the flight controller via MAVSDK. Verifies telemetry streams (position, attitude, battery), arms the drone, and confirms pre-arm checks pass.

ActionDetail
ConnectMAVSDK system.connect() — waits for heartbeat
Verify telemetryPosition, attitude, and battery streams must report non-null within 2s
ArmSend arm command; verify is_armed == true
Exit: Telemetry confirmed and drone armed → TAKEOFF

TAKEOFF STARTUP

Executes a vertical climb to the race altitude of 5m AGL. Throttle is managed by the altitude controller. No lateral movement.

Exit: Altitude ≥ 5m reached and stable → SEEK_GATE

SEEK_GATE ACTIVE RACING

The drone enters a rapid yaw scan to locate the next gate. Vision pipeline processes every frame looking for gate detections.

ParameterValue
Yaw rate180 deg/s continuous rotation
Approach distance15m — gate must be within this range to trigger approach
Seek timeout30s with no detection → EMERGENCY
Finish timeout30s after last gate pass with no new gate → FINISHED
Gate < 15m:APPROACH_GATE
30s no detection:EMERGENCY

APPROACH_GATE TRANSITION

Proportional pursuit toward the tracked gate. The controller outputs pitch, yaw, and roll commands to close distance while keeping the gate centered in the frame.

BehaviorDetail
Forward pitchProportional to gate distance — aggressive far, gentle close
Yaw trackingBearing error drives yaw correction
Roll trackingLateral bearing error drives roll compensation
Distance trackingRecords prev_gate_distance each frame for derivative (closing detection)
No-detection limitmax_no_detection = 15 frames
Transit ready: distance < 1.5m AND closing 3+ frames AND fresh detection → TRANSIT_GATE
Lost gate: 15 consecutive frames with no detection → SEEK_GATE (direct, no recovery phase)
Gate re-acquisition: If the gate is re-detected before hitting the 15-frame limit, no_detection_count resets to 0 and approach continues seamlessly.

TRANSIT_GATE TRANSITION

A single-frame event marking successful gate passage. Not a sustained phase — it fires and immediately transitions.

ActionDetail
Incrementgates_passed++
Record timeLog split time for this gate
Reset trackerClear tracked gate data — fresh start for next gate
Cooldown0.3s cooldown to prevent double-counting same gate
More gates remaining:SEEK_GATE
All gates passed:FINISHED

EMERGENCY TERMINAL

Hover in place with zero velocity. The drone holds position and altitude. Triggered when SEEK_GATE times out after 30s without finding any gate.

Cause: SEEK timeout — 30 seconds of continuous scanning with no gate detection.

FINISHED TERMINAL

Race complete. Triggered by one of two conditions:

3. Predictive Transit Detection

Transit through a gate is not triggered by a simple distance threshold. Raw distance measurements are noisy — a jittery reading could falsely trigger transit while the drone is still meters away. Instead, the system requires consistent closing behavior before accepting a transit.

Algorithm

  1. Each frame with a gate detection, compute: dist_delta = prev_distance - current_distance
  2. If dist_delta > 0 (closing): increment distance_closing_count
  3. If dist_delta ≤ 0 (opening or jitter): reset distance_closing_count = 0
  4. Transit triggers only when ALL conditions are met:
Why this matters: Distance estimates from PnP solve can fluctuate ±0.5m frame-to-frame due to corner detection noise. Requiring 3 consecutive closing frames ensures the drone is genuinely passing through the gate, not bouncing around in front of it.

Timing Diagram

F1 F2 F3 F4 F5 F6 F7 F8 F9 Distance Delta Closing # Transit? 4.2m 3.5m 3.7m 2.9m 2.1m 1.4m 1.1m 0.7m 0.3m -- +0.7 -0.2 +0.8 +0.8 +0.7 +0.3 +0.4 +0.4 0 1 0 1 2 3 4 5 6 -- -- -- -- -- YES cooldown cooldown cooldown Transit fires: <1.5m + closing>=3 jitter reset
Note: Frame F3 shows distance increasing (jitter). This resets the closing count to 0, forcing the system to re-establish 3 consecutive closing frames before transit can fire. This is the core protection against false transits.

4. Gate Tracker (EMA Smoothing)

Raw gate detections are noisy. The tracker applies Exponential Moving Average (EMA) smoothing to produce stable bearing, distance, and position estimates that the controller can act on without oscillation.

TrackedGate Fields

FieldTypeDescription
bearing_xfloatHorizontal bearing to gate center (normalized, -1 to +1)
bearing_yfloatVertical bearing to gate center (normalized, -1 to +1)
distancefloatEstimated distance to gate plane (meters)
confidencefloatDetection confidence score (0.0 to 1.0)
positionvec33D world-frame position estimate of gate center
ageintNumber of frames this gate has been tracked
staleintFrames since last detection update (0 = fresh this frame)

EMA Formula

smoothed = alpha * new_measurement + (1 - alpha) * prev_smoothed
ParameterValueRationale
alpha0.65Racing default — fast response to gate movement, minimal lag. Higher than typical (0.3-0.5) because racing demands quick reaction.
Staleness limit10 framesGate is dropped after 10 consecutive frames with no detection. At 120Hz, this is ~83ms.
Max tracking distance80mDetections beyond 80m are discarded — too unreliable for tracking.
Why alpha = 0.65? At 120Hz, a 0.65 alpha gives an effective time constant of ~2.3 frames (~19ms). The gate position responds almost immediately to new detections while still filtering out single-frame outliers. Lower alpha values (e.g., 0.3) introduce tracking lag that becomes dangerous at high approach speeds.

Staleness Lifecycle

Frame N:   detection arrives  →  stale = 0,  EMA update applied
Frame N+1: no detection       →  stale = 1,  use last smoothed values
Frame N+2: no detection       →  stale = 2,  use last smoothed values
...
Frame N+9: no detection       →  stale = 9,  use last smoothed values
Frame N+10: no detection      →  stale = 10, GATE DROPPED — tracker cleared

5. Controller Modes

Two control modes are available, selectable per-mission. Attitude mode is the default for aggressive racing; velocity body mode is available for smoother, more conservative approaches.

Attitude Mode AGGRESSIVE

Sends SET_ATTITUDE_TARGET MAVLink messages. Direct control over roll, pitch, yaw rate, and thrust. No position or velocity controller in the loop — the race code is the outer controller.

Gains
kp_yaw50 — deg/s per unit bearing error
kp_pitch30 — degrees per unit distance error
kp_roll25 — degrees per unit lateral error
kp_throttle0.45 — thrust per unit altitude error
Key Setpoints
Cruise pitch-25 deg — aggressive forward tilt during approach
Approach pitch (close)-15 deg at 2m distance — late braking
Seek yaw rate180 deg/s — full rotation every 2 seconds
Limits (Safety Clamps)
Pitch[-45, +15] deg
Roll[-45, +45] deg
Thrust[0.15, 0.85] (normalized 0-1)
Warning: Attitude mode has no velocity damping. Overshooting a gate at high speed means there is no automatic deceleration — the state machine must transition to SEEK and re-acquire. Tune gains carefully in simulation before flight.

Velocity Body Mode SMOOTH

Sends SET_POSITION_TARGET_LOCAL_NED with velocity setpoints in the body frame. The flight controller's velocity PID handles the inner loop, giving smoother trajectories at the cost of peak speed.

Velocity Gains
Forward (body-X)Proportional to gate distance, max 15 m/s
Lateral (body-Y)5.0 m/s per unit horizontal bearing error
Vertical (body-Z)3.0 m/s per unit vertical bearing error
Yaw rate80 deg/s per unit bearing error
Pros: Built-in velocity damping, smoother trajectories, easier to tune, more forgiving of gain errors.
Cons: Velocity controller adds latency (~50ms), lower peak speed, less responsive to fast gate transitions.

6. Timing Budget

The race loop targets 120Hz (8.33ms per frame). Every millisecond matters — a frame overrun means stale commands and degraded tracking. Below is the per-frame budget breakdown.

StageTimeNotes
Camera capture~1.0 msUSB3 frame grab, pre-allocated buffer
U-Net inference~5.0 msGate segmentation network, FP16 on GPU
RANSAC corner extraction~1-3 msFit gate corners from segmentation mask; varies with mask complexity
PnP solve~0.5 mssolvePnP with 4 corner correspondences → distance + bearing
State machine + controller~0.5 msPhase logic, EMA update, command computation
MAVLink send~0.1 msSerialize and transmit attitude/velocity target
Total ~8-10 ms Fits 120Hz with margin; RANSAC is the variable component
8.33ms frame budget (120Hz) Camera 1ms U-Net Inference ~5ms RANSAC ~1.5ms PnP+SM +MAV 1.1ms ~8.6ms typical 0.7ms headroom at 120Hz
Critical path: U-Net inference dominates at ~5ms. If the model grows beyond this budget, drop to 60Hz or optimize the network. RANSAC is the only variable-time component — worst case ~3ms on complex masks.

7. Race Loop Pseudocode

The main async loop runs at 120Hz. Each iteration follows a strict pipeline: sense, decide, act. No deferred work, no background threads for control — everything is synchronous within the frame.

async def race_loop():
    # Initialize state machine
    phase = Phase.INIT
    gates_passed = 0
    tracked_gate = None
    prev_gate_distance = None
    distance_closing_count = 0
    no_detection_count = 0
    last_transit_time = 0
    transit_cooldown = 0.3  # seconds

    while phase not in (Phase.FINISHED, Phase.EMERGENCY):
        frame_start = time.monotonic()

        # ── SENSE ──────────────────────────────────────
        telemetry = await get_telemetry()       # attitude, position, velocity
        frame     = await get_frame()            # camera capture (~1ms)
        detection = detect_gates(frame)          # U-Net + RANSAC + PnP (~7ms)

        # ── TRACK ──────────────────────────────────────
        if detection:
            tracked_gate = ema_update(tracked_gate, detection, alpha=0.65)
            tracked_gate.stale = 0
            no_detection_count = 0
        else:
            if tracked_gate:
                tracked_gate.stale += 1
                if tracked_gate.stale >= 10:
                    tracked_gate = None          # drop stale gate
            no_detection_count += 1

        # ── DECIDE (state machine) ─────────────────────
        if phase == Phase.INIT:
            if telemetry.armed:
                phase = Phase.TAKEOFF

        elif phase == Phase.TAKEOFF:
            if telemetry.altitude >= 5.0:
                phase = Phase.SEEK_GATE

        elif phase == Phase.SEEK_GATE:
            if tracked_gate and tracked_gate.distance < 15.0:
                phase = Phase.APPROACH_GATE
                distance_closing_count = 0
            elif seek_timeout_exceeded(30):
                phase = Phase.EMERGENCY
            elif gates_passed > 0 and since_last_transit() > 30:
                phase = Phase.FINISHED

        elif phase == Phase.APPROACH_GATE:
            # Distance derivative for transit detection
            if tracked_gate and prev_gate_distance:
                dist_delta = prev_gate_distance - tracked_gate.distance
                if dist_delta > 0:
                    distance_closing_count += 1
                else:
                    distance_closing_count = 0
            if tracked_gate:
                prev_gate_distance = tracked_gate.distance

            # Check transit conditions
            if (tracked_gate
                and tracked_gate.distance < 1.5
                and distance_closing_count >= 3
                and tracked_gate.stale == 0
                and time.monotonic() - last_transit_time > transit_cooldown):
                phase = Phase.TRANSIT_GATE

            # Lost gate → back to SEEK (no recovery phase)
            elif no_detection_count >= 15:
                phase = Phase.SEEK_GATE
                tracked_gate = None
                prev_gate_distance = None

        elif phase == Phase.TRANSIT_GATE:
            gates_passed += 1
            last_transit_time = time.monotonic()
            log_split_time(gates_passed)
            tracked_gate = None               # reset tracker
            prev_gate_distance = None
            distance_closing_count = 0
            if gates_passed >= expected_gates:
                phase = Phase.FINISHED
            else:
                phase = Phase.SEEK_GATE

        # ── ACT ────────────────────────────────────────
        command = compute_command(phase, tracked_gate, telemetry)
        await send_command(command)              # MAVLink (~0.1ms)

        # ── RATE LIMIT ─────────────────────────────────
        elapsed = time.monotonic() - frame_start
        await asyncio.sleep(max(0, 1/120 - elapsed))  # 120Hz target
Key invariant: The TRANSIT_GATE phase is a single-frame event. It increments gates_passed, resets the tracker, and immediately transitions to SEEK_GATE (or FINISHED). It never persists across multiple frames.
Thread safety: The entire sense-decide-act pipeline runs in a single async coroutine. No locks, no shared mutable state between threads. Telemetry arrives via MAVSDK callbacks into a latest-value cache that the loop reads atomically.

Quick Reference Card

Thresholds

Approach distance15m
Transit distance1.5m
Closing frames req.3
Transit cooldown0.3s
Max no-detection15 frames
Stale gate drop10 frames
Seek timeout30s
Finish timeout30s
Max tracking dist.80m
EMA alpha0.65

Attitude Limits

Pitch range[-45, +15] deg
Roll range[-45, +45] deg
Thrust range[0.15, 0.85]
Cruise pitch-25 deg
Close pitch-15 deg @ 2m
Seek yaw rate180 deg/s
Max forward vel.15 m/s
Loop rate120 Hz
Race altitude5m AGL