Self-contained onboard autonomy for the simplified final-round arena. The drone follows a yellow path, identifies airports via AprilTags, and lands once at the country-2 airport. No retakeoff is required.
All runtime paths are relative so this directory can be moved into a new repository without code changes.
The overall flow is the same as the semi-final task — follow the yellow path, decode AprilTags, land on the requested country — but the rules are simplified for the final arena:
| Aspect | Semi-final | Final round |
|---|---|---|
| Arena layout | Multi-loop course | Simplified path (per arena image) |
| Required landings | One per requested country | Exactly one — at a country 2 airport |
| Retakeoff after landing | Yes, to find the next country | No — mission ends after landing |
| Takeoff pad marker | None | AprilTag 302 placed on the takeoff pad |
A 3-digit ID XYZ encodes:
X(hundreds) — country codeY(tens) — airport status:1= safe,0= unsafeZ(ones) — reachable airports (informational)
Examples:
| Tag ID | Country | Status | Meaning |
|---|---|---|---|
213 |
2 | safe | Valid landing target |
212 |
2 | safe | Valid landing target |
202 |
2 | unsafe | Skipped |
115 |
1 | safe | Wrong country — skipped |
302 |
3 | unsafe | Takeoff pad — filtered out before mission logic |
Tag 302 sits on the takeoff pad and is provided to help the autopilot
stabilise during the initial climb. Teams are not required to modify it.
This stack lists 302 in apriltag.ignore_tag_ids so the perception layer
drops it before it can ever influence the mission FSM:
- It is never added to the airport graph.
- It never triggers visual servo or landing.
- The mission only acts on tags that pass the country/status check.
If your local arena uses a different takeoff-pad ID, override
apriltag.ignore_tag_ids in your config/local.json.
INIT → FOLLOW_LINE ⇄ VISUAL_SERVO → LAND_WAIT → COMPLETE
↓
(any state) → FAILSAFE on timeout / land timeout
There is no RETAKEOFF state in the final-round runner — the mission is
complete the moment the country-2 landing succeeds.
src/— modular onboard autonomy stackconfig/— tunable JSON parametersscripts/— startup and diagnostics utilitiestests/— host-runnable tests (no MAVLink/camera hardware needed)requirements.txt— Python dependencies.gitignore— Python and Raspberry Pi development ignore rules
- Raspberry Pi 5
- Alpine Linux
- MAVLink endpoint on UDP port
14550 - Camera stream on TCP port
9000(raw RGB frames)
From your host machine:
ssh root@localhost -p 2222After login, move to the project directory on the Pi:
cd /path/to/final_taskapk add --no-cache \
python3 \
py3-pip \
python3-dev \
gcc \
g++ \
make \
musl-dev \
linux-headerspython3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txtFor SSH/headless operation on Raspberry Pi this project uses
opencv-contrib-python-headless by default, so GUI/X11 libraries are not
required for mission runtime.
If you previously installed GUI OpenCV wheels, clean them first to avoid
mixed-wheel conflicts such as ImportError: libxcb.so.1:
python3 -m pip uninstall -y \
opencv-python \
opencv-contrib-python \
opencv-python-headless \
opencv-contrib-python-headless
python3 -m pip install -r requirements.txtDefault config lives at config/defaults.json. The default
mission.requested_countries is [2], matching the final-round
single-landing requirement.
For real hardware tuning:
- Copy
config/hardware.example.jsontoconfig/local.json - Adjust camera, HSV thresholds, PID gains, and landing tolerances
- Run mission with the override file
cp config/hardware.example.json config/local.json
python3 -m src.mission --config config/local.json --debugIf config/local.json is missing, mission startup fails with a hint that
includes the exact cp command above.
You can override the country at the command line (rarely needed for the final round):
python3 -m src.mission --config config/local.json --countries 2sh scripts/start.sh config/defaults.jsonWith country override:
COUNTRIES=2 sh scripts/start.sh config/local.jsonpython3 -m src.mission --config config/defaults.jsonRun these checks in order before live flight:
-
Validate config and camera socket reachability:
python3 scripts/diagnostics.py
-
Confirm MAVLink heartbeat and telemetry access:
python3 scripts/test_mavlink.py
-
Preview camera and detection overlays (
qto quit, requires GUI):python3 scripts/camera_preview.py
-
Run mission with conservative speeds first:
python3 -m src.mission --config config/local.json --debug
The tests/ folder contains hardware-free tests. They verify:
- AprilTag metadata decoding (country / status / reachable digits)
- Tag
302filtering (takeoff pad must never trigger landing) - Mission FSM behaviour: country-2 safe airport → land → COMPLETE
- Country-mismatch and unsafe-status tags do not trigger landing
- Single-landing flow (no
arm/takeoffcalls happen afterland)
Run them all with:
python3 tests/run_all.pyOr individually:
python3 tests/test_apriltag_decode.py
python3 tests/test_mission_flow.pyThese tests need only numpy, opencv-contrib-python-headless, and
pymavlink installed (the same as requirements.txt); they replace
MAVLink and the camera with in-process fakes.
src/mavlink_client.py— MAVLink connection and arm / takeoff / velocity / land commandssrc/camera_stream.py— TCP frame ingestion from port 9000src/apriltag_detector.py— AprilTag detection +ignore_tag_idsfilter for takeoff padsrc/line_follower.py— Adaptive yellow-path detectionsrc/landing_controller.py— Landing command fusion (tag alignment + line assist)src/mission.py— Simplified single-landing FSM
- Frame buffering is intentionally small to reduce control lag.
- Vision uses simple morphology and contour logic to limit CPU usage.
- Avoid running extra GUI tools during flight unless needed for diagnostics.
- Keep camera resolution moderate (for example 640x480) if CPU load is high.
- The mission TCP camera client expects raw RGB frames at 640x480 by default.
- If dimensions differ from the configured expected size, that frame is dropped to prevent OpenCV crashes from malformed payloads.
- If you intentionally use a different camera size, override
camera.expected_widthandcamera.expected_heightin your local config.
- No hardcoded absolute paths.
- Config and script paths are relative to the project root.
- This folder can be copied into a new repository and run as-is after dependency installation.