Spec revision · what changed · what we changed in response

VADR-TS-002 deltas.

VADR-TS-002 Issue 00.02 (NT, 2026-05-08) added the camera intrinsics, the vision stream protocol, and the gate / drone dimensions — all of which were "TBD" in TS-001. It also removed ODOMETRY from the supported MAVLink message set. This page is the single source of truth for what the new spec says, what was in our code that's now wrong, and what was changed on 2026-05-12 to fix it.

Spec doc
VADR-TS-002 Issue 00.02
2026-05-08 · supersedes TS-001
Code aligned
11 files · 2026-05-12
UDP receiver + integrator smoke-tested
Open question
§3.8 VFoV = 90° vs intrinsics
clarification draft pending
Blocker
Re-render synth + retrain APEX
multi-hour GPU job

Reference: 260508_Technical_Spec_0002.pdf at repo root. Diff against TS-001 below.

DELTA · 01 / 05 PROTOCOL

Vision stream is now fully specified — UDP:5600, JPEG chunks.

TS-001 §4.6 said "Vision stream parameters will be provided in a separate specification." TS-002 §4.6 fills that in. Client must implement a UDP listener that reassembles JPEG from chunked datagrams.

Transport: UDP, default port 5600, little-endian byte order Header size: 24 bytes per datagram Frame rate: 30 Hz · 640 × 360 JPEG

Per-packet header layout (Python struct format: <I H H I I Q):

FieldTypeSizePurpose
frame_iduint324 BSequence ID for the image frame
chunk_iduint162 BPacket index within the frame (0 … total_chunks − 1)
total_chunksuint162 BPackets needed to reconstruct
jpeg_sizeuint324 BTotal reassembled JPEG byte length
payload_sizeuint324 BJPEG bytes in this packet
sim_time_nsuint648 BSimulator epoch timestamp (ns)
Was (our code)GazeboPipeCamera — subprocess pipe with 16-byte header `<IIII>` (w, h, fmt, sz). camera_adapter.py:49
NowUDPVisionCamera — binds UDP:5600, parses 24-byte header, reassembles by frame_id, cv2.imdecode → BGR. camera_adapter.py: new class

Implementation drops stale partial frames when a newer frame's first chunk arrives, and caps in-flight frame buffers. Loopback smoke test (2-chunk JPEG): decoded back to identical 360×640×3 BGR.

DELTA · 02 / 05 GEOMETRY

Camera intrinsics fully specified — pinhole, no distortion, 20° upward tilt.

TS-001 left the camera parameters undefined. TS-002 §3.8 pins them. The 20° upward pitch is new public info.

Resolution640 × 360 px
fx, fy320, 320
cx, cy320, 180
Distortionnone (pure pinhole)
Camera → bodypitched 20° UPWARD about body Y (NED)
Body → IMUidentity
Was (assumed)640×480 · fx computed from 120° HFoV · fy from 90° VFoV · cam_tilt = 0° everywhere
Now (per spec)640×360 · fx = fy = 320 · cy = 180 · cam_tilt = 20° · pinned in 7 files

The 20° tilt is consequential — gates appear above the optical axis, which the PnP-to-NED transform must account for. The _cam_to_body_rotation() helper in course_mapper.py applies an R_tilt rotation about body Y; cam_tilt_rad default is now math.radians(20.0).

Files updated: vision_pipeline.py (CameraConfig), race_config.py (CameraSettings), drone_mpc_foundation.py (DroneParams), course_mapper.py (CourseMapperConfig.cam_tilt_rad), vq1_completion_pilot.py (CameraIntrinsics defaults), train_apex.py (CAMERA_* constants), synth_aigp_gates.py (renderer FoV constants).
DELTA · 03 / 05 GEOMETRY

Gate dimensions are now exact: 1.5 m inner / 2.7 m outer.

TS-002 §3.7 specifies both the outer frame (the visible blue border) and the inner aperture (what the drone flies through). PnP targets the inner aperture — sharper, well-defined edges.

ElementWidthHeightDepth
Outer frame2700 mm2700 mm260 mm
Inner aperture1500 mm1500 mm260 mm
Border on each side600 mm (with 140 mm corner bevel)
Was (inconsistent across code)vision_pipeline.py: 2.0 m · imu_gate_predictor.py: 2.0 m · race_config.py: 1.5 m · synth_aigp_gates.py: 1.5 m · vq1_completion_pilot.py: 1.5 m
Now (harmonized)All files: inner = 1.5 m (PnP target). Outer 2.7 m + depth 0.26 m added to race_config GateSettings for visibility / collision checks.

Drone chassis is 280 × 280 × 160 mm — clearance per axis at the inner aperture is ~1.22 m wall-to-wall. That's the corridor the controller has to thread.

DELTA · 04 / 05 CRITICAL · MAVLINK

ODOMETRY is gone — position must be dead-reckoned.

TS-001 listed ODOMETRY in §4.3. TS-002 removed it. The supported sim→client message set is now:

HEARTBEATconnection status
ATTITUDEvehicle attitude (Euler / quat)
HIGHRES_IMUbody-frame accel + gyro (FRD)
TIMESYNCclock alignment

Client→sim control: SET_POSITION_TARGET_LOCAL_NED, SET_ATTITUDE_TARGET. Command rate < 100 Hz, heartbeat ≥ 2 Hz, physics 120 Hz.

No GPS, no LOCAL_POSITION_NED, no ODOMETRY. The simulator does not publish any absolute position or velocity message. The 2026-04-19 AIGP update already said "no GPS, no absolute positioning" — TS-002 makes it concrete in the message set.

Our response in mavsdk_bridge.py:

Smoke tests passed: stationary level drone → zero NED drift over 8.3 s. 1 m/s² body-forward for 1 s → 0.99 m/s NED north, 0.49 m position (correct gravity cancellation + Euler integration).

DELTA · 05 / 05 OPEN · SPEC QUIRK

The "VFoV = 90°" prose contradicts the listed intrinsics.

TS-002 §3.8 states VFoV = 90°, but fy = 320 with image height = 360 implies VFoV = 2·atan(180/320) ≈ 58.72° and HFoV = 2·atan(320/320) = 90°. The two can't both be true.

Interpretation we're proceeding with (a): the numeric intrinsics are authoritative. "VFoV" in §3.8 is almost certainly an HFoV label typo. fx = fy = 320 describes a square-pixel pinhole with HFoV = 90°.

Clarification email drafted in documents/vadr-ts-002-clarification-draft.md, sent to NT (author of the camera revision). If they confirm interpretation (b) — true VFoV = 90° with revised intrinsics — we'll need a second pass on synth FoV constants, the renderer, PnP K matrix, and retraining. All the existing tasks already split this dependency cleanly.

§ ΔChange log — 2026-05-12

FileChange
camera_adapter.py+ UDPVisionCamera class (UDP:5600, JPEG-chunk reassembly), wired into create_camera
mavsdk_bridge.py− ODOMETRY subscription. + InertialIntegrator + apply_position_correction hook. Module docstring rewritten for TS-002.
vision_pipeline.pyCameraConfig rewritten: 640×360, fx=fy=320, cx=320 cy=180, tilt_deg=20.0. GATE_OUTER_* + GATE_FRAME_DEPTH added. GATE_WIDTH/HEIGHT → 1.5.
race_config.pyCameraSettings updated to spec. GateSettings + outer_width, outer_height, depth.
imu_gate_predictor.pyGATE_WIDTH/HEIGHT 2.0 → 1.5
drone_mpc_foundation.pyCamera fields converted from FoV-derived to direct intrinsics. cam_tilt 0° → 20°.
course_mapper.pycam_tilt_rad default → math.radians(20.0)
vq1_completion_pilot.pyPlaceholder intrinsics fixed (height 480→360, cy 240→180). CameraIntrinsics gains tilt_deg field.
synthetic_aperture_depth.pyGATE_WIDTH/HEIGHT 2.0 → 1.5
synth_aigp_gates.pyHFoV 120°→90°, VFoV 90°→58.72°. --imgsz default 480→360. GATE_OUTER_M constant added. Module docstring rewritten for TS-002.
train_apex.pyCAMERA_TILT_DEG 45°→20°. FoV constants matched to spec. Image size constants added.

§ TODOWhat's left

  1. Send the clarification email to NT — documents/vadr-ts-002-clarification-draft.md. Asks: is "VFoV = 90°" a label typo or intent?
  2. Re-render synthetic dataset + retrain APEX — current dataset_gates_synthetic* was rendered at the old 640×480 / 120°×90° FoV / 45° tilt. New geometry → new render → new training run. Multi-hour GPU job; intentionally not auto-kicked.
    python synth_aigp_gates.py --output dataset_gates_synthetic_v2 \ --n-train 8000 --n-val 2000 --imgsz 640 360
  3. Validate against live sim as soon as DCL sim package drops. The UDP receiver and integrator pass loopback tests, but only a real sim run confirms the protocol implementation.
  4. Bound the dead-reckoner drift with vision corrections in course_mapper.pySimBridge.apply_position_correction. The integrator is open-loop; without gate-PnP fixes it'll drift over the 8-minute window.
Related reading: winning-tech.html § 04 (camera matches spec) · winning-strategy.html (retired-components, observation-swap) · synthetic-gate-dataset.html (renderer command + new params) · apex-pipeline.html (observation-swap landmine, now urgent for VQ1 too) · quickstart-guide.html (corrected K matrix in the single-file example)