Imported from xanderwasserman/ArduFlite (
AGENTS.md). Install upstream withnpx skills add xanderwasserman/ArduFlite. Copyright stays with the author.
Agents Playbook
Purpose
Provide concise, enforceable guidelines for any AI or human agent contributing to this repository. The goal is to keep the codebase maintainable, modular, and easy to navigate while continuously improving quality.
Core Principles
- Preserve architecture boundaries. Never reintroduce duplicated logic; extend or refactor shared helpers instead.
- Leave code better than you found it. Apply boy-scout rules: clean up related smells, improve clarity, and add missing tests when practical.
- Favor readability over cleverness. Small, well-named functions/modules beat large "spaghetti" blocks.
- Be explicit and documented. Update relevant docs/configs whenever behavior or public APIs change.
- Validate changes. Run targeted tests or provide verification steps; never assume success.
- Fail loudly. If an assumption or dependency is missing, log/return early so issues are obvious.
- Prefer library built-ins over custom code. Before implementing functionality, check if the library provides a built-in solution. Use native features when available.
- Never skip documentation updates. Architectural changes, new modules, or API changes require updates to AGENTS.md and/or README.md before task completion.
Workflow Checklist
- Before coding
- Read the latest instructions and this playbook.
- Review
git statusto understand the working tree. - Identify every file you expect to touch; plan reads in batches.
- While coding
- Keep modules focused: orchestration vs. rendering vs. data helpers.
- Use shared helpers instead of local fallbacks.
- Add succinct comments only when logic is non-obvious.
- Update or create tests/docs together with functional changes.
- After coding
- Re-run relevant tests or provide precise manual verification steps.
- Summarize changes clearly (what/why/where) and mention follow-up actions.
- Ensure diffs are minimal and files stay formatted.
Module Boundaries
Core Architecture Layers
ArduFlite follows a strict layered architecture. Respect these boundaries:
-
Configuration Layer (
include/+src/utils/Config*)- Runtime-tunable parameters use
ConfigRegistrysingleton with NVS persistence - Compile-time constants (sensor types, hardware pins) remain in
include/*.h - Keys defined in
include/ConfigKeys.h, defaults ininclude/ConfigSchema.h - Controllers use
initFromConfig()pattern: default constructor + deferred init after ConfigRegistry loads - Hot-reload via observer pattern:
ConfigRegistry::subscribe("rate.roll.*", callback)
- Runtime-tunable parameters use
-
Control Layer (
src/controller/)- ArduFliteController: Top-level orchestrator managing cascade control loops
- ArduFliteAttitudeController: Outer loop (attitude → rate setpoints)
- ArduFliteRateController: Inner loop (rate setpoints → servo commands)
- PID: Low-level PID implementation with anti-windup
- Controllers must not directly access hardware — use abstractions (IMU, ServoManager)
-
Sensor/Actuator Layer (
src/orientation/,src/actuators/)- ArduFliteIMU: Sensor fusion, flight state detection, baro auto-calibration
- ServoManager: Wing geometry abstraction (CONVENTIONAL, DELTA_WING, V_TAIL)
- These classes own the hardware interfaces
- Use FreeRTOS tasks for time-critical operations (e.g., IMU @ high Hz)
- BMP280 barometer sampling is decimated inside the IMU task (read every
BARO_DECIMATION_FACTORticks ≈ 50 Hz) and feeds altitude/climb rate into the IMU snapshot. The IMU task is the sole owner of the I2C bus; do not add a second task that sharesimuMutex/Wire, as that reintroduces priority inversion and "sensor mutex busy" update skips.
-
Communication Layer (
src/receiver/,src/telemetry/)- Receiver: Input from pilot (CRSF/PWM) with failsafe callbacks
- Telemetry: Output to ground station/transmitter (Serial, CRSF, Flash)
- Each backend runs in its own FreeRTOS task
- Use thread-safe
TelemetryDatasnapshots; config queries go toConfigRegistry
-
Utilities Layer (
src/utils/)- ConfigRegistry: Singleton for runtime config with type-safe get/set, validation, observers
- ConfigPersistence: NVS-backed storage with schema versioning and JSON export/import
- ConfigTask: Background FreeRTOS task for periodic dirty-save and import queue
- ConfigHelpers:
buildPIDConfig()converts Ti/Td time constants to Ki/Kd gains - ConfigObservers: Bridges config changes to CommandSystem for thread-safe updates
- ControlMixer: Mode-dependent scaling and mixing (Attitude/Rate/Manual)
- CommandSystem: Thread-safe command queue using FreeRTOS queues
- Logging: Singleton logger with pluggable handlers (
LOG_INF,LOG_ERR, etc.) - Button Managers: Input handling (HoldButton, MultiTapButton)
- StatusLED: Visual feedback patterns
-
CLI Layer (
src/cli/)- Command-line interface for runtime diagnostics and tuning
- Uses
CommandSystemto send thread-safe commands to other modules - Never directly modify controller state — always go through the command queue
- Command implementations are split by concern:
CLICommands.cpp: command table and help outputCLICommandsSystem.cpp: reset/stats/tasks/setmode/calibrateCLICommandsConfig.cpp: configuration registry commands onlyCLICommandsFlash.cpp: flash log commandsCLICommandsTelemetry.cpp: serial telemetry streamingCLICommandsTests.cpp: field-safe integration tests
- Shared CLI parsing belongs in
CLICommandUtils.*; shared CLI dependencies and ground-safety checks belong inCLICommandContext.*
-
Web Layer (
src/web/)- WiFiManager: Singleton for WiFi Access Point management
- ArduFliteWebServer: REST API for configuration (GET/PUT params, export/import JSON)
- WebUI.h: Embedded responsive HTML/CSS/JS frontend in PROGMEM
- Enabled via
web.enabledconfig key; creates AP with configurable SSID and WPA2 password (web.ap_passmust be 8+ characters) - Full builds run captive DNS so phone/laptop captive-portal probes resolve to the Web UI
- Mutating REST requests require the per-boot same-origin token from
/api/session - REST endpoints:
/api/config,/api/system/status,/api/flash - Runs in its own FreeRTOS task at priority 1 (lowest, non-blocking)
- Compile-time toggle:
ENABLE_WEB_SERVERininclude/WebConfiguration.h- Full build:
./build.sh lolin(~1.3MB, includes WiFi/HTTP stack) - Lite build:
./build.sh lolin lite(~620KB, flight-only, no WiFi) - Builds use per-board/per-variant output directories (
build/lolin-full,build/lolin-lite) - WiFi/TCP/HTTP libraries add ~500KB; lite build excludes them entirely
- Full build:
Dependency Rules
- Higher layers can depend on lower layers, but NOT vice versa
- Controllers depend on IMU/ServoManager, but IMU/ServoManager are independent
- Telemetry observes state but never modifies it
- Use dependency injection: pass pointers to dependencies in constructors
Thread Safety
- ArduFlite uses FreeRTOS extensively with multiple concurrent tasks
- Always protect shared state with mutexes or use FreeRTOS queues
- Use
SemaphoreLockRAII wrapper (defined inArduFlite.h) for automatic mutex management - Take snapshots of data structures (like
TelemetryData) to avoid holding locks too long - Never block in ISRs or high-priority tasks
ArduFliteIMUuses a versioned snapshot for lock-free reads: the IMU task marks the snapshot version odd while writing and even when complete, while readers retry if the version changes mid-copy
Folder Structure Overview
ArduFlite/
├── include/ # Headers and compile-time constants
│ ├── ArduFlite.h # Main header, SemaphoreLock RAII
│ ├── ConfigKeys.h # Config key #defines (hierarchical dot notation)
│ ├── ConfigSchema.h # Parameter registration with defaults/ranges
│ ├── ControllerTypes.h # Shared enums (ControlLoopType)
│ ├── AircraftConfiguration.h # Compile-time aircraft type (powered vs glider)
│ ├── CSRFConfiguration.h # CRSF receiver pin/channel mapping
│ ├── PinConfiguration.h # Pin assignments (compile-time)
│ ├── ReceiverConfiguration.h # Receiver type and failsafe config
│ ├── IMUConfiguration.h # IMU/Baro type selection macros only
│ ├── MissionConfiguration.h # Mission planner parameters
│ └── WebConfiguration.h # ENABLE_WEB_SERVER compile-time flag
│
├── src/
│ ├── controller/ # Cascade PID control system
│ │ ├── ArduFliteController.* # Top-level orchestrator (Outer+Inner loops)
│ │ ├── ArduFliteAttitudeController.* # Attitude → Rate (outer loop)
│ │ ├── ArduFliteRateController.* # Rate → Servo (inner loop)
│ │ └── pid.* # Generic PID with anti-windup
│ │
│ ├── orientation/ # Sensor fusion and state estimation
│ │ ├── ArduFliteIMU.* # IMU wrapper (FastIMU + Madgwick + BMP280)
│ │ └── FliteQuaternion.* # Quaternion math helpers
│ │
│ ├── actuators/ # Servo output and mixing
│ │ └── ServoManager.* # Wing geometry abstraction
│ │
│ ├── receiver/ # Pilot input (RC link)
│ │ ├── crsf/ # CRSF (ELRS/Crossfire) receiver
│ │ └── pwm/ # PWM receiver (legacy)
│ │
│ ├── telemetry/ # Data output to ground station
│ │ ├── TelemetryData.h # Shared data structure
│ │ ├── serial/ # Debug and quaternion serial output
│ │ ├── flash/ # On-board flash logging
│ │ └── crsf/ # CRSF telemetry uplink
│ │
│ ├── cli/ # Command-line interface
│ │ ├── ArduFliteCLI.* # CLI task and command router
│ │ ├── CLICommands.* # Command table and command declarations
│ │ ├── CLICommandContext.* # Shared CLI dependencies and safety checks
│ │ ├── CLICommandUtils.* # Generic CLI parsing helpers
│ │ └── CLICommands*.* # Concern-specific command implementations
│ │
│ ├── web/ # Web configuration interface
│ │ ├── WiFiManager.* # WiFi Access Point singleton
│ │ ├── ArduFliteWebServer.* # REST API and web server
│ │ └── WebUI.h # Embedded HTML/CSS/JS in PROGMEM
│ │
│ ├── mission_planner/ # Autonomous mission execution
│ │ └── MissionPlanner.* # Future: waypoint navigation
│ │
│ ├── state/ # State machines
│ │ └── StateManagement.* # Mode and flight state handlers
│ │
│ ├── tests/ # Test sequences
│ │ ├── AttitudeTests.* # Wing wiggle tests
│ │ └── ReceiverTests.* # Receiver input validation
│ │
│ └── utils/ # Shared utilities
│ ├── ConfigRegistry.* # Singleton config store with observers
│ ├── ConfigPersistence.* # NVS load/save with schema versioning
│ ├── ConfigTask.* # Background task for periodic saves
│ ├── ConfigHelpers.h # PID config builders (Ti/Td → Ki/Kd)
│ ├── ConfigObservers.* # Observer registrations for hot-reload
│ ├── CommandSystem.* # Thread-safe command queue
│ ├── ControlMixer.* # Mode-dependent input mixing
│ ├── Logging.* # Singleton logger with colors
│ ├── StatusLED.* # Visual feedback patterns
│ └── Button*.* # Input handling (hold, multi-tap)
│
├── docs/ # Project documentation
│ ├── CONFIG_REFERENCE.md # Runtime parameter reference
│ └── flight_logs/ # Chronological flight test records (FL001, FL002, ...)
│
├── tools/ # Ground station and analysis scripts
│ ├── data_analysis/ # Python: flight data analysis
│ ├── visualisation/ # Python: 3D attitude visualization
│ └── flash_dump/ # Python: extract flight logs from flash
│
├── ArduFlite.ino # Arduino entry point (calls arduflite_init/loop)
├── ArdufliteApp.cpp # Main application logic
└── README.md # Project documentation
Key Design Patterns
-
Persistent Configuration System
ConfigRegistry: Singleton storing all runtime-tunable parametersConfigPersistence: NVS-backed save/load with JSON export/importConfigSchema.h: Static registration macros (CONFIG_FLOAT,CONFIG_INT, etc.)ConfigKeys.h: Hierarchical keys with IDE autocomplete (e.g.,CONFIG_KEY_RATE_ROLL_KP)- Components use
initFromConfig()pattern for deferred initialization after FreeRTOS
-
Deferred Initialization Pattern
- Controllers have default constructors (safe/zero values)
- After
ConfigRegistry::init()+ConfigPersistence::load(), callinitFromConfig() - Enables global objects while respecting FreeRTOS startup order
-
Manager Pattern**
ServoManager,HoldButtonManager,MultiTapButtonManager- Managers own hardware resources and provide high-level APIs
- Encapsulate geometry/mixing logic (e.g., delta wing vs. conventional)
-
Command Pattern
CommandSystemwith FreeRTOS queue for thread-safe inter-task communication- Commands are POD structs (
SystemCommand) with type discriminator - Prevents direct state mutation across task boundaries
-
Observer Pattern
- Telemetry modules observe state without modifying it
- Use snapshot pattern: copy data under lock, then process outside lock
-
RAII for Locks
SemaphoreLockautomatically releases mutexes on scope exit- Prevents deadlocks from early returns or exceptions
Ongoing Improvements
Code Quality Guidelines
-
Configuration Changes
- Runtime-tunable parameters: Add to
ConfigKeys.handConfigSchema.h - Compile-time constants (hardware pins, sensor types, aircraft type): Add to appropriate
*Configuration.h - Use
ConfigHelpers::buildPIDConfig()for PID-related configs - Document units and ranges in comments and schema description
- Register observers in
ConfigObservers.cppif hot-reload is needed
- Runtime-tunable parameters: Add to
-
Adding New Control Features
- Extend
ControlLoopTypeenum (inControllerTypes.h) andSystemCommandTypeif needed - Add PID configs to
ConfigSchema.hwithCONFIG_FLOATmacros - Update
ArduFliteController::processCommands()to handle new commands - Add CLI commands in
CLICommands*.cppfor runtime tuning
- Extend
-
New Telemetry Backends
- Inherit from base telemetry interface (if one exists, or create it)
- Run in a separate FreeRTOS task with configurable update rate
- Use
TelemetryData::update()to get a consistent snapshot - Never hold locks during network I/O or slow operations
-
New Sensor Integration
- Add sensor to
ArduFliteIMUor create a new manager class - Use FreeRTOS tasks for high-rate sensors
- Provide thread-safe getter methods
- Document calibration procedures in comments and README
- Add sensor to
-
Testing
- Add test functions in
src/tests/for new control modes - Use test sequences (like
runAttitudeTest_wiggle) to verify hardware - Prefer automated validation over manual "it looks okay"
- Update
README.mdwith new test procedures
- Add test functions in
Common Pitfalls to Avoid
-
❌ Don't bypass the CommandSystem
- Bad: Directly calling
controller.setMode()from a button callback - Good: Push
CMD_SET_MODEcommand, let main loop process it
- Bad: Directly calling
-
❌ Don't hold locks during I/O
- Bad: Lock mutex, then
Serial.print()orWiFi.send() - Good: Take snapshot under lock, release lock, then perform I/O
- Bad: Lock mutex, then
-
❌ Don't use raw pointers without ownership clarity
- If a class stores a pointer, document who owns the object
- Prefer references for mandatory non-null dependencies
-
❌ Don't hard-code magic numbers
- Bad:
if (roll > 45.0f)in source code - Good:
if (roll > ControlMixerConfig::MAX_ROLL_DEGREES)
- Bad:
-
❌ Don't replicate mixing logic
- Bad: Implementing delta-wing mixing in both Controller and ServoManager
- Good: ServoManager owns all geometry-specific mixing
-
❌ Don't ignore FreeRTOS task priorities
- IMU Task (highest priority) → Inner Loop → Outer Loop → Telemetry → CLI
- Critical tasks must pre-empt slower ones to maintain loop rates
-
❌ Don't use
Serial.printdirectly- Bad:
Serial.println("Debug message") - Good:
LOG_DBG("Debug message")(uses Logger singleton with colors)
- Bad:
Performance Considerations
-
Loop Timing
- Outer loop: ~100 Hz (10 ms period)
- Inner loop: ~500 Hz (2 ms period)
- Avoid dynamic allocation in control loops (pre-allocate)
- Monitor loop stats via
LoopStatsand CLIstatscommand
-
Memory Usage
- ESP32 has limited RAM (~320 KB)
- Use
constexprto move data to flash when possible - Be mindful of FreeRTOS task stack sizes (typically 4096 bytes)
-
Float vs. Double
- ESP32 has hardware FPU for
float, notdouble - Use
floatfor performance-critical code - Use
doubleonly when precision is essential (e.g., GPS coordinates)
- ESP32 has hardware FPU for
Documentation Standards
-
File Headers
- All files must include copyright header with author, version, date
- Use MIT License boilerplate
-
Function Comments
- Use Doxygen-style
@brief,@param,@return - Document units (seconds, degrees, radians, etc.)
- Explain non-obvious algorithms or magic numbers
- Use Doxygen-style
-
Inline Comments
- Keep them succinct and only where logic is non-obvious
- Prefer self-documenting code (good names) over excessive comments
-
README and AGENTS.md
- Update README when adding user-facing features
- Update AGENTS.md when changing architecture or patterns
Following this playbook is mandatory: if instructions ever conflict, pause and clarify before proceeding.
