Skip to content

Commit 2e997fe

Browse files
committed
feat: add stune params — firmware parameter table query & validation
Knowledge base driven architecture: - Parameter definitions live in smarttune/knowledge/params/<platform>.json Data scraped from official firmware repos (not hand-written) - ParamTable loads from JSON via ParamTable.from_knowledge(platform) - PlatformAdapter gains abstract param_table() method CLI subcommands: stune params <platform> List all params (ap/bf/px4) stune params <PARAM_NAME> Show param details (auto-detect platform) stune params --search <keyword> Search across all platforms stune params --validate <NAME> <VALUE> -p <platform> Validate existence + range Parameter data: - ArduPilot: 2517 params from @PARAM: annotations + Parameters.cpp - Betaflight: 45 params from official settings.c (BF 4.5+ parameter names) - PX4: 20 params from official YAML definitions (mc_rate_control + mc_att_control + imu_gyro) Key fix: prevents agents from recommending parameters that don't exist in the target firmware (stune params --validate checks before applying). 141 tests pass.
1 parent 07c51a3 commit 2e997fe

10 files changed

Lines changed: 25601 additions & 0 deletions

File tree

‎smarttune/cli.py‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ def main():
6666
sysid ARX system identification
6767
hardware Hardware configuration report
6868
magfit Magnetometer calibration analysis
69+
params Query/validate firmware parameter tables
70+
71+
\b
72+
Parameter query examples:
73+
stune params ap # List all ArduPilot parameters
74+
stune params ATC_RAT_RLL_P # Show param details
75+
stune params --search notch # Search across platforms
76+
stune params --validate ATC_RAT_RLL_P 0.15 -p ardupilot
6977
7078
\b
7179
The platform is auto-detected from the log file format.
@@ -921,5 +929,193 @@ def _run_single_analysis(capability: str, log_file: Path, platform_name: str,
921929
# 入口
922930
# ---------------------------------------------------------------------------
923931

932+
@main.command()
933+
@click.argument("query", required=False)
934+
@click.option("--platform", "-p", default=None, help="Target platform: ardupilot, betaflight, px4")
935+
@click.option("--search", "-s", "search_term", default=None, help="Search keyword across all platforms")
936+
@click.option("--category", "-c", default="all", help="Filter by category")
937+
@click.option("--validate", "-v", "validate_pair", nargs=2, default=None, metavar="NAME VALUE",
938+
help="Validate a parameter name and value")
939+
def params(query, platform, search_term, category, validate_pair):
940+
"""Query and validate flight controller parameters.
941+
942+
\b
943+
Query parameter tables loaded from knowledge base JSON files
944+
(smarttune/knowledge/params/). Data scraped from official firmware repos.
945+
946+
\b
947+
Examples:
948+
stune params ap # List all ArduPilot parameters
949+
stune params bf # List all Betaflight parameters
950+
stune params px4 # List all PX4 parameters
951+
stune params ATC_RAT_RLL_P # Show detail for a specific param
952+
stune params --search notch # Search for "notch" across all platforms
953+
stune params --validate ATC_RAT_RLL_P 0.15 --platform ardupilot
954+
stune params --category pid --platform betaflight
955+
"""
956+
from smarttune.platform.params import ParamTable
957+
from rich.table import Table
958+
from rich.panel import Panel
959+
import json
960+
961+
_PLATFORM_ALIASES = {
962+
"ap": "ardupilot", "ardupilot": "ardupilot",
963+
"bf": "betaflight", "betaflight": "betaflight",
964+
"px4": "px4",
965+
}
966+
967+
# ── validate mode ──────────────────────────────────────
968+
if validate_pair:
969+
name, val_str = validate_pair
970+
try:
971+
value = float(val_str)
972+
except ValueError:
973+
_console.print(f"[red]Invalid value: {val_str}[/red]")
974+
sys.exit(1)
975+
976+
if not platform:
977+
_console.print("[red]--platform required for --validate[/red]")
978+
sys.exit(1)
979+
980+
platform = _PLATFORM_ALIASES.get(platform, platform)
981+
try:
982+
tbl = ParamTable.from_knowledge(platform)
983+
except FileNotFoundError as e:
984+
_console.print(f"[red]{e}[/red]")
985+
sys.exit(1)
986+
987+
ok, msg = tbl.validate(name, value)
988+
if ok:
989+
_console.print(f"[green]✓ {msg}[/green]")
990+
sys.exit(0)
991+
else:
992+
_console.print(f"[red]✗ {msg}[/red]")
993+
sys.exit(1)
994+
995+
# ── search mode ────────────────────────────────────────
996+
if search_term:
997+
available = ParamTable.available_platforms()
998+
if platform:
999+
platform = _PLATFORM_ALIASES.get(platform, platform)
1000+
available = [platform] if platform in available else []
1001+
1002+
if not available:
1003+
_console.print("[red]No platforms available[/red]")
1004+
sys.exit(1)
1005+
1006+
total = 0
1007+
for plat in available:
1008+
tbl = ParamTable.from_knowledge(plat)
1009+
results = tbl.search(search_term)
1010+
if not results:
1011+
continue
1012+
total += len(results)
1013+
_console.print(f"\n[bold cyan]{tbl.platform}[/bold cyan] — {len(results)} match(es)")
1014+
t = Table(show_header=True, box=None)
1015+
t.add_column("Parameter", style="cyan")
1016+
t.add_column("Category")
1017+
t.add_column("Type")
1018+
t.add_column("Range")
1019+
t.add_column("Description")
1020+
for r in results:
1021+
rng = ""
1022+
if r.min is not None or r.max is not None:
1023+
rng = f"[{r.min}, {r.max}]"
1024+
if r.unit:
1025+
rng += f" {r.unit}"
1026+
t.add_row(r.name, r.category, r.type, rng, r.description[:60])
1027+
_console.print(t)
1028+
1029+
if total == 0:
1030+
_console.print(f"[yellow]No parameters matching '{search_term}'[/yellow]")
1031+
return
1032+
1033+
# ── query mode (specific param or platform list) ───────
1034+
if query:
1035+
# Try as platform alias first
1036+
plat_guess = _PLATFORM_ALIASES.get(query.lower())
1037+
if plat_guess:
1038+
# List all for this platform
1039+
platform = plat_guess
1040+
else:
1041+
# Treat as parameter name — try all platforms
1042+
available = ParamTable.available_platforms()
1043+
found = []
1044+
for plat in available:
1045+
tbl = ParamTable.from_knowledge(plat)
1046+
pd = tbl.query(query)
1047+
if pd:
1048+
found.append((plat, pd))
1049+
1050+
if found:
1051+
for plat, pd in found:
1052+
_console.print(Panel.fit(
1053+
f"[bold cyan]{pd.name}[/bold cyan]\n"
1054+
f"[dim]Platform:[/dim] {plat} | "
1055+
f"[dim]Category:[/dim] {pd.category} | "
1056+
f"[dim]Type:[/dim] {pd.type}\n"
1057+
f"[dim]Default:[/dim] {pd.default}\n"
1058+
f"[dim]Range:[/dim] [{pd.min}, {pd.max}]"
1059+
+ (f" {pd.unit}" if pd.unit else "") + "\n"
1060+
f"[dim]Description:[/dim] {pd.description or '—'}",
1061+
title=f"Parameter: {pd.name}"
1062+
))
1063+
return
1064+
else:
1065+
_console.print(f"[yellow]Parameter '{query}' not found in any platform[/yellow]")
1066+
_console.print(f"[dim]Try --search for partial matches[/dim]")
1067+
sys.exit(1)
1068+
1069+
# ── list mode (platform specified or no args) ──────────
1070+
if not platform:
1071+
# No args at all — show available platforms
1072+
available = ParamTable.available_platforms()
1073+
if not available:
1074+
_console.print("[yellow]No parameter tables found in knowledge base[/yellow]")
1075+
return
1076+
_console.print("[bold]Available parameter tables:[/bold]")
1077+
for plat in available:
1078+
tbl = ParamTable.from_knowledge(plat)
1079+
cats = ", ".join(tbl.categories())
1080+
_console.print(f" [cyan]{tbl.platform}[/cyan] — {len(tbl)} params ({cats})")
1081+
_console.print("\n[dim]Usage: stune params ap | stune params bf | stune params px4[/dim]")
1082+
return
1083+
1084+
platform = _PLATFORM_ALIASES.get(platform, platform)
1085+
try:
1086+
tbl = ParamTable.from_knowledge(platform)
1087+
except FileNotFoundError as e:
1088+
_console.print(f"[red]{e}[/red]")
1089+
sys.exit(1)
1090+
1091+
params_list = tbl.list_all()
1092+
if category != "all":
1093+
params_list = tbl.list_by_category(category)
1094+
1095+
cat_counts = {}
1096+
for p in params_list:
1097+
cat_counts[p.category] = cat_counts.get(p.category, 0) + 1
1098+
1099+
_console.print(f"\n[bold]{tbl.platform}[/bold] — {len(params_list)} parameters"
1100+
+ (f" (category: {category})" if category != "all" else "")
1101+
+ (f" [dim]categories: {', '.join(f'{k}({v})' for k,v in sorted(cat_counts.items()))}[/dim]" if category == "all" else ""))
1102+
1103+
t = Table(show_header=True, box=None)
1104+
t.add_column("Parameter", style="cyan")
1105+
t.add_column("Category")
1106+
t.add_column("Type")
1107+
t.add_column("Default")
1108+
t.add_column("Range")
1109+
t.add_column("Description")
1110+
for p in sorted(params_list, key=lambda x: (x.category, x.name)):
1111+
rng = ""
1112+
if p.min is not None or p.max is not None:
1113+
rng = f"[{p.min}, {p.max}]"
1114+
if p.unit:
1115+
rng += f" {p.unit}"
1116+
t.add_row(p.name, p.category, p.type, str(p.default), rng, (p.description or "")[:80])
1117+
_console.print(t)
1118+
1119+
9241120
if __name__ == "__main__":
9251121
main()

‎smarttune/knowledge/params/__init__.py‎

Whitespace-only changes.

0 commit comments

Comments
 (0)