I Built a Tool That Tells You Exactly Which PID Parameters to Change — And My AI Agent Reads It
Three hours staring at step response graphs manually. Four test flights. That’s what it used to cost me to get a roll axis tuned. Now my agent runs stune analyze flight_15.bin and comes back with exact parameter deltas — while I eat lunch.
pip install smarttune-cli && stune analyze flight_15.bin --platform ardupilot
Every suggestion is capped at ±20%. It tells you what to change, never writes a parameter. You verify, you fly safe.
What’s happening under the hood (and why agents can consume it directly):
SmartTune has a platform abstraction layer — PlatformAdapter is an abstract base class with parse(), capabilities(), map_param_to_platform(). ArduPilot .bin, Betaflight .bbl (parsed by a 1000+ line pure-Python decoder that handles I/P/S/E frame types, variable-byte encoding, Elias delta predictors), and PX4 .ulg all feed into one FlightData dataclass. Every analyzer only reads FlightData. Adding a new platform means writing one adapter, not touching any analysis code.
The core algorithms:
-
Step response: On ArduPilot, SmartTune replicates the WebTools PIDReview.js pipeline exactly — Hanning-windowed FFT → single-to-double-sided spectral conversion → SNR regularization via cumulative Gaussian integral → Wiener deconvolution (H = Pyx / (Pxx + sn)) → per-window step averaging with 93.75% overlap. On low sample-rate data (<20Hz, common in real APM logs), it falls back to a time-domain step extractor — detects step edges via first-order differences, normalizes per-step-by-amplitude, averages windows in the time domain. Both paths output the same signal shape so downstream analysis is identical.
-
Vibration: 3-axis independent Welch FFT (Hanning window) → max envelope across axes — this preserves per-axis peak features rather than collapsing into an L2-norm that smears motor peaks into DC. Peak detection uses median-based noise floor estimation + 30dB SNR threshold +
scipy.find_peakswith 15dB prominence. Source identification maps peak frequencies against KB-defined bands (motor 30-250Hz, prop blade pass 251-400Hz, structural resonance 401-500Hz, high-frequency >500Hz) with harmonic inheritance — a 160Hz peak with a 320Hz harmonic is tagged “motor + 2nd harmonic”, not two separate sources. -
PID metrics: 10%→90% rise time, overshoot %, settling time (±10% band), zero-crossing oscillation count, steady-state error %. Evaluated against per-axis per-platform thresholds with a weighted scoring function. Each axis gets an EXCELLENT/GOOD/MARGINAL/POOR/UNUSABLE rating.
-
Tuning rules: Knowledge-base-driven, not hardcoded. Each rule defines a symptom → (parameter, action, factor) mapping. “HIGH_OS” → decrease P by ×0.85 and D by ×0.90. “SLOW_RISE” → increase P by ×1.15. Rules are ranked by severity, deduplicated across conflicts (same parameter can’t get two opposing adjustments), clamped to per-axis bounds, and the result is a concrete list of
ParamRefobjects with current → suggested values.
The knowledge base is programmable. Six layers deep-merge at load time: builtin_common → builtin_{platform} → ~/.smarttune/knowledge/common → ~/.smarttune/knowledge/{platform} → pro_common → pro_{platform}. Thresholds, tuning rules, frequency bands, vibration levels — all in JSON files that agents can read and write. Your agent can modify the tuning rules without touching Python code.
Agent-first from day one:
| Feature | Why it matters for agents |
|---|---|
--format json / --format markdown |
Agents don’t parse ANSI escape codes |
| Semantic exit codes (E10xx–E50xx) | Agent gets E1001 = file not found, not a wall of text |
FlightData dataclass |
Platform-agnostic input; all analyzers share one contract |
ParamRef suggestions |
Output has exact current → suggested values, not vague “try lowering P” |
| 6-layer programmable KB | Agents can read rules and write new ones |
| ±20% cap, read-only | Safety constraint baked in at the architecture level |
| No cloud deps | Works in a container, a sandbox, or a tent |
My agent workflow (OpenClaw, but any agent framework works):
# Claude Code / Codex / QwenPaw / Hermes / OpenClaw — all the same
result = subprocess.run(["stune", "analyze", "-i", "log.bin", "--platform", "ardupilot"])
if result.returncode == 0: # E0000 = success
report = json.load(open("output/analysis.md"))
# Write diagnostic with per-parameter recommendations
My own agent does this every time I come back from a test flight — reads the stune output, writes a full diagnostic, and tells me “roll P is too low by 15%, suggested ATC_RAT_RLL_P = 0.098.”
I’m Raylan — Monash Malaysia freshman, part of an APM firmware dev group with Nanjing University. I work on ArduPilot firmware daily (ParallelFC multi-FC redundancy, self-learning PID, STM32H743/H753 custom hardware). This tool is built on 2+ years of firmware-level understanding — it knows which ArduPilot parameters exist, how the notch filter chain interacts with the gyro LPF, and what a real motor noise peak looks like vs. a structural resonance.
GitHub: https://github.com/raylanlin/smarttune-cli — MIT, open-source. 309 tests passing.
If this saves your agent even one test flight,
it.