inav-toolkit 2.17.0__tar.gz → 2.18.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/PKG-INFO +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/__init__.py +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/blackbox_analyzer.py +86 -77
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/flight_db.py +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/msp.py +12 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/param_analyzer.py +119 -22
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/vtol_configurator.py +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/wizard.py +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/PKG-INFO +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/pyproject.toml +1 -1
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/LICENSE +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/README.md +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/autotune.py +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/i18n.py +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/locales/en.json +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/locales/es.json +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit/locales/pt_BR.json +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/SOURCES.txt +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/dependency_links.txt +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/entry_points.txt +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/requires.txt +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/top_level.txt +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/setup.cfg +0 -0
- {inav_toolkit-2.17.0 → inav_toolkit-2.18.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
INAV Blackbox Analyzer - Multirotor Tuning Tool v2.
|
|
3
|
+
INAV Blackbox Analyzer - Multirotor Tuning Tool v2.18.0
|
|
4
4
|
=====================================================
|
|
5
5
|
Analyzes INAV blackbox logs and tells you EXACTLY what to change.
|
|
6
6
|
|
|
@@ -104,7 +104,7 @@ def _disable_colors():
|
|
|
104
104
|
AXIS_NAMES = ["Roll", "Pitch", "Yaw"]
|
|
105
105
|
AXIS_COLORS = ["#FF6B6B", "#4ECDC4", "#FFD93D"]
|
|
106
106
|
MOTOR_COLORS = ["#FF6B6B", "#4ECDC4", "#FFD93D", "#A78BFA"]
|
|
107
|
-
REPORT_VERSION = "2.
|
|
107
|
+
REPORT_VERSION = "2.18.0"
|
|
108
108
|
|
|
109
109
|
# ─── Frame and Prop Profiles ─────────────────────────────────────────────────
|
|
110
110
|
# Two separate concerns:
|
|
@@ -882,7 +882,7 @@ CLI_BOOL_KEYS = {
|
|
|
882
882
|
}
|
|
883
883
|
|
|
884
884
|
|
|
885
|
-
def merge_diff_into_config(config,
|
|
885
|
+
def merge_diff_into_config(config, config_raw):
|
|
886
886
|
"""Merge INAV CLI 'diff all' output into the analysis config dict.
|
|
887
887
|
|
|
888
888
|
Strategy:
|
|
@@ -895,19 +895,19 @@ def merge_diff_into_config(config, diff_raw):
|
|
|
895
895
|
|
|
896
896
|
Args:
|
|
897
897
|
config: Existing config dict from extract_fc_config()
|
|
898
|
-
|
|
898
|
+
config_raw: Raw 'diff all' output string
|
|
899
899
|
|
|
900
900
|
Returns:
|
|
901
901
|
Number of settings merged
|
|
902
902
|
"""
|
|
903
|
-
if not
|
|
903
|
+
if not config_raw:
|
|
904
904
|
return 0
|
|
905
905
|
|
|
906
906
|
try:
|
|
907
907
|
from inav_toolkit.flight_db import parse_diff_output
|
|
908
908
|
except ImportError:
|
|
909
909
|
from inav_flight_db import parse_diff_output
|
|
910
|
-
diff_settings = parse_diff_output(
|
|
910
|
+
diff_settings = parse_diff_output(config_raw)
|
|
911
911
|
|
|
912
912
|
merged = 0
|
|
913
913
|
mismatches = []
|
|
@@ -6101,7 +6101,7 @@ def count_blackbox_logs(filepath):
|
|
|
6101
6101
|
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
6102
6102
|
|
|
6103
6103
|
def main():
|
|
6104
|
-
parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.
|
|
6104
|
+
parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.18.0 - Prescriptive Tuning",
|
|
6105
6105
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
6106
6106
|
parser.add_argument("--version", action="version", version=f"inav-analyze {REPORT_VERSION}")
|
|
6107
6107
|
parser.add_argument("logfile", nargs="?", default=None,
|
|
@@ -6141,11 +6141,11 @@ def main():
|
|
|
6141
6141
|
help="Omit the plain-English description of the quad's behavior.")
|
|
6142
6142
|
parser.add_argument("--no-color", action="store_true",
|
|
6143
6143
|
help="Disable colored terminal output.")
|
|
6144
|
-
parser.add_argument("--
|
|
6145
|
-
help="Path to a CLI 'diff all' file for enriched analysis. "
|
|
6144
|
+
parser.add_argument("--config", metavar="FILE", default=None,
|
|
6145
|
+
help="Path to a CLI 'dump all' or 'diff all' file for enriched analysis. "
|
|
6146
6146
|
"Adds config cross-referencing to nav and tuning results. "
|
|
6147
6147
|
"Not needed with --device (config is pulled automatically). "
|
|
6148
|
-
"Also auto-discovered if a *_diff.txt file sits next to the BBL.")
|
|
6148
|
+
"Also auto-discovered if a *_dump.txt or *_diff.txt file sits next to the BBL.")
|
|
6149
6149
|
parser.add_argument("--nav", action="store_true",
|
|
6150
6150
|
help="Enable navigation health analysis (compass, GPS, baro, "
|
|
6151
6151
|
"estimator). Works on any flight with nav fields in the log.")
|
|
@@ -6187,7 +6187,7 @@ def main():
|
|
|
6187
6187
|
|
|
6188
6188
|
# ── Device mode: download blackbox from FC ──
|
|
6189
6189
|
logfile = args.logfile
|
|
6190
|
-
|
|
6190
|
+
config_raw = None
|
|
6191
6191
|
if args.device:
|
|
6192
6192
|
try:
|
|
6193
6193
|
try:
|
|
@@ -6257,19 +6257,18 @@ def main():
|
|
|
6257
6257
|
pct = summary['used_size'] * 100 // summary['total_size'] if summary['total_size'] > 0 else 0
|
|
6258
6258
|
print(f" Dataflash: {used_kb:.0f}KB / {total_kb:.0f}KB ({pct}% used)")
|
|
6259
6259
|
|
|
6260
|
-
# Pull
|
|
6261
|
-
|
|
6262
|
-
print(" Pulling configuration (
|
|
6263
|
-
|
|
6264
|
-
if
|
|
6265
|
-
n_settings = len([l for l in
|
|
6266
|
-
print(f" {n_settings}
|
|
6267
|
-
|
|
6268
|
-
diff_path = os.path.join(args.blackbox_dir, f"{info['craft_name'] or 'fc'}_diff.txt")
|
|
6260
|
+
# Pull full config dump (all parameters — used for analysis, fingerprinting, and backup)
|
|
6261
|
+
config_raw = None
|
|
6262
|
+
print(" Pulling configuration (dump all)...", end="", flush=True)
|
|
6263
|
+
config_raw = fc.get_dump_all(timeout=30.0)
|
|
6264
|
+
if config_raw:
|
|
6265
|
+
n_settings = len([l for l in config_raw.splitlines() if l.strip().startswith("set ")])
|
|
6266
|
+
print(f" {n_settings} parameters")
|
|
6267
|
+
config_path = os.path.join(args.blackbox_dir, f"{info['craft_name'] or 'fc'}_dump.txt")
|
|
6269
6268
|
os.makedirs(args.blackbox_dir, exist_ok=True)
|
|
6270
|
-
with open(
|
|
6271
|
-
f.write(
|
|
6272
|
-
print(f" Saved: {
|
|
6269
|
+
with open(config_path, "w") as f:
|
|
6270
|
+
f.write(config_raw)
|
|
6271
|
+
print(f" Saved: {config_path}")
|
|
6273
6272
|
else:
|
|
6274
6273
|
print(" no response (FC may not support CLI over MSP)")
|
|
6275
6274
|
|
|
@@ -6311,39 +6310,42 @@ def main():
|
|
|
6311
6310
|
print(f"ERROR: File not found: {logfile}"); sys.exit(1)
|
|
6312
6311
|
|
|
6313
6312
|
# ── Load diff from file (when not using --device) ──
|
|
6314
|
-
if
|
|
6315
|
-
|
|
6316
|
-
if args.
|
|
6317
|
-
# Explicit file path: --
|
|
6318
|
-
|
|
6313
|
+
if config_raw is None:
|
|
6314
|
+
config_file = None
|
|
6315
|
+
if args.config:
|
|
6316
|
+
# Explicit file path: --config my_dump.txt
|
|
6317
|
+
config_file = args.config
|
|
6319
6318
|
else:
|
|
6320
|
-
# Auto-discover
|
|
6319
|
+
# Auto-discover config files in same directory as BBL
|
|
6320
|
+
# Prefer dump (complete) over diff (only changed params)
|
|
6321
6321
|
log_dir = os.path.dirname(os.path.abspath(logfile))
|
|
6322
6322
|
candidates = []
|
|
6323
6323
|
for fname in os.listdir(log_dir):
|
|
6324
|
-
if fname.endswith("
|
|
6324
|
+
if (fname.endswith("_dump.txt") or fname.endswith("_diff.txt")
|
|
6325
|
+
or fname in ("dump.txt", "dump_all.txt", "diff.txt", "diff_all.txt")):
|
|
6325
6326
|
candidates.append(os.path.join(log_dir, fname))
|
|
6326
|
-
if
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6327
|
+
if candidates:
|
|
6328
|
+
# Prefer dump over diff, then most recent
|
|
6329
|
+
def _score(path):
|
|
6330
|
+
is_dump = "_dump" in path or "dump" in os.path.basename(path)
|
|
6331
|
+
return (1 if is_dump else 0, os.path.getmtime(path))
|
|
6332
|
+
config_file = max(candidates, key=_score)
|
|
6333
|
+
|
|
6334
|
+
if config_file:
|
|
6335
|
+
if os.path.isfile(config_file):
|
|
6334
6336
|
try:
|
|
6335
|
-
with open(
|
|
6336
|
-
|
|
6337
|
-
n_settings = len([l for l in
|
|
6337
|
+
with open(config_file, "r", errors="ignore") as f:
|
|
6338
|
+
config_raw = f.read()
|
|
6339
|
+
n_settings = len([l for l in config_raw.splitlines()
|
|
6338
6340
|
if l.strip().startswith("set ")])
|
|
6339
6341
|
if n_settings > 0:
|
|
6340
|
-
print(f" Config: {os.path.basename(
|
|
6342
|
+
print(f" Config: {os.path.basename(config_file)} ({n_settings} settings)")
|
|
6341
6343
|
else:
|
|
6342
|
-
|
|
6344
|
+
config_raw = None # not a valid config file
|
|
6343
6345
|
except Exception:
|
|
6344
|
-
|
|
6346
|
+
config_raw = None
|
|
6345
6347
|
else:
|
|
6346
|
-
print(f" Warning:
|
|
6348
|
+
print(f" Warning: Config file not found: {config_file}")
|
|
6347
6349
|
|
|
6348
6350
|
# ── History mode: show progression and exit ──
|
|
6349
6351
|
if args.history or args.trend:
|
|
@@ -6374,14 +6376,14 @@ def main():
|
|
|
6374
6376
|
if not os.path.isfile(args.compare):
|
|
6375
6377
|
print(f"ERROR: Comparison file not found: {args.compare}")
|
|
6376
6378
|
sys.exit(1)
|
|
6377
|
-
_run_comparison(logfile, args.compare, args,
|
|
6379
|
+
_run_comparison(logfile, args.compare, args, config_raw)
|
|
6378
6380
|
return
|
|
6379
6381
|
|
|
6380
6382
|
# ── Replay mode: interactive HTML time-series ──
|
|
6381
6383
|
if args.replay:
|
|
6382
6384
|
if not logfile:
|
|
6383
6385
|
parser.error("logfile required for --replay")
|
|
6384
|
-
_run_replay(logfile, args,
|
|
6386
|
+
_run_replay(logfile, args, config_raw)
|
|
6385
6387
|
return
|
|
6386
6388
|
|
|
6387
6389
|
# ── Log quality check mode ──
|
|
@@ -6420,15 +6422,15 @@ def main():
|
|
|
6420
6422
|
target = log_files[-1]
|
|
6421
6423
|
if len(log_files) > 1:
|
|
6422
6424
|
print(f"\n Nav mode: analyzing last flight ({len(log_files)} in flash)")
|
|
6423
|
-
_analyze_single_log(target, args,
|
|
6425
|
+
_analyze_single_log(target, args, config_raw)
|
|
6424
6426
|
elif len(log_files) > 1:
|
|
6425
|
-
_process_multi_log(log_files, args,
|
|
6427
|
+
_process_multi_log(log_files, args, config_raw)
|
|
6426
6428
|
else:
|
|
6427
6429
|
# Single flight - full analysis
|
|
6428
|
-
_analyze_single_log(log_files[0], args,
|
|
6430
|
+
_analyze_single_log(log_files[0], args, config_raw)
|
|
6429
6431
|
|
|
6430
6432
|
|
|
6431
|
-
def _process_multi_log(log_files, args,
|
|
6433
|
+
def _process_multi_log(log_files, args, config_raw):
|
|
6432
6434
|
"""Handle multiple flights from a single flash download.
|
|
6433
6435
|
|
|
6434
6436
|
Strategy:
|
|
@@ -6444,7 +6446,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6444
6446
|
print(f" Scanning {len(log_files)} flights...")
|
|
6445
6447
|
summaries = []
|
|
6446
6448
|
for lf in log_files:
|
|
6447
|
-
s = _analyze_single_log(lf, args,
|
|
6449
|
+
s = _analyze_single_log(lf, args, config_raw, summary_only=True)
|
|
6448
6450
|
if s:
|
|
6449
6451
|
summaries.append(s)
|
|
6450
6452
|
|
|
@@ -6457,7 +6459,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6457
6459
|
# header config matches the diff are "current session" (post-change),
|
|
6458
6460
|
# flights that don't match are "old session" (pre-change).
|
|
6459
6461
|
# Without a diff, fall back to comparing consecutive flights.
|
|
6460
|
-
current_fp = _fingerprint_from_diff(
|
|
6462
|
+
current_fp = _fingerprint_from_diff(config_raw)
|
|
6461
6463
|
is_current = [] # True/False per flight
|
|
6462
6464
|
|
|
6463
6465
|
if current_fp:
|
|
@@ -6581,7 +6583,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6581
6583
|
|
|
6582
6584
|
# ── Phase 5: Full analysis on the selected flight ──
|
|
6583
6585
|
print(f"{'═' * 70}")
|
|
6584
|
-
_analyze_single_log(log_files[best_idx], args,
|
|
6586
|
+
_analyze_single_log(log_files[best_idx], args, config_raw)
|
|
6585
6587
|
|
|
6586
6588
|
# ── Phase 6: Show cross-session progression ──
|
|
6587
6589
|
if not args.no_db:
|
|
@@ -6635,7 +6637,7 @@ def _config_fingerprint(config):
|
|
|
6635
6637
|
return "|".join(parts) if parts else ""
|
|
6636
6638
|
|
|
6637
6639
|
|
|
6638
|
-
def _fingerprint_from_diff(
|
|
6640
|
+
def _fingerprint_from_diff(config_raw):
|
|
6639
6641
|
"""Build a config fingerprint from CLI 'diff all' output.
|
|
6640
6642
|
|
|
6641
6643
|
This represents the FC's CURRENT config - the ground truth.
|
|
@@ -6643,14 +6645,14 @@ def _fingerprint_from_diff(diff_raw):
|
|
|
6643
6645
|
session; flights that don't match are from before the user applied
|
|
6644
6646
|
changes.
|
|
6645
6647
|
"""
|
|
6646
|
-
if not
|
|
6648
|
+
if not config_raw:
|
|
6647
6649
|
return ""
|
|
6648
6650
|
|
|
6649
6651
|
try:
|
|
6650
6652
|
from inav_toolkit.flight_db import parse_diff_output
|
|
6651
6653
|
except ImportError:
|
|
6652
6654
|
from inav_flight_db import parse_diff_output
|
|
6653
|
-
diff_settings = parse_diff_output(
|
|
6655
|
+
diff_settings = parse_diff_output(config_raw)
|
|
6654
6656
|
|
|
6655
6657
|
# Map CLI names → config keys (same mapping as merge_diff_into_config)
|
|
6656
6658
|
config = {}
|
|
@@ -6666,8 +6668,12 @@ def _fingerprint_from_diff(diff_raw):
|
|
|
6666
6668
|
return _config_fingerprint(config)
|
|
6667
6669
|
|
|
6668
6670
|
|
|
6669
|
-
def _print_config_review(
|
|
6670
|
-
"""Run parameter analyzer on FC
|
|
6671
|
+
def _print_config_review(config_raw, config, frame_inches, plan):
|
|
6672
|
+
"""Run parameter analyzer on FC config and show findings not covered by flight analysis.
|
|
6673
|
+
|
|
6674
|
+
Accepts either 'dump all' or 'diff all' output. Dump is preferred since it
|
|
6675
|
+
includes every parameter including unchanged defaults, which matters for
|
|
6676
|
+
nav PID checks on large frames.
|
|
6671
6677
|
|
|
6672
6678
|
Only shows CRITICAL and WARNING findings from categories that the blackbox
|
|
6673
6679
|
analyzer doesn't cover (safety, nav, motor protocol, GPS, battery, RX).
|
|
@@ -6676,6 +6682,9 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
|
|
|
6676
6682
|
"""
|
|
6677
6683
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
6678
6684
|
|
|
6685
|
+
if not config_raw:
|
|
6686
|
+
return
|
|
6687
|
+
|
|
6679
6688
|
try:
|
|
6680
6689
|
try:
|
|
6681
6690
|
from inav_toolkit.param_analyzer import parse_diff_all, run_all_checks, CRITICAL, WARNING
|
|
@@ -6685,7 +6694,7 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
|
|
|
6685
6694
|
return # param analyzer not available
|
|
6686
6695
|
|
|
6687
6696
|
try:
|
|
6688
|
-
parsed = parse_diff_all(
|
|
6697
|
+
parsed = parse_diff_all(config_raw)
|
|
6689
6698
|
except Exception:
|
|
6690
6699
|
return
|
|
6691
6700
|
|
|
@@ -6956,15 +6965,15 @@ footer {{ text-align:center; color:#555; margin-top:30px; padding:10px; border-t
|
|
|
6956
6965
|
|
|
6957
6966
|
# ─── Comparison Mode ─────────────────────────────────────────────────────────
|
|
6958
6967
|
|
|
6959
|
-
def _analyze_for_compare(logfile, args,
|
|
6968
|
+
def _analyze_for_compare(logfile, args, config_raw=None):
|
|
6960
6969
|
"""Run analysis pipeline on a single file and return structured results.
|
|
6961
6970
|
Returns dict with: config, data, noise_results, pid_results, motor_analysis,
|
|
6962
6971
|
dterm_results, plan, noise_fp, hover_osc, profile
|
|
6963
6972
|
"""
|
|
6964
6973
|
raw_params = parse_headers_from_bbl(logfile)
|
|
6965
6974
|
config = extract_fc_config(raw_params)
|
|
6966
|
-
if
|
|
6967
|
-
merge_diff_into_config(config,
|
|
6975
|
+
if config_raw:
|
|
6976
|
+
merge_diff_into_config(config, config_raw)
|
|
6968
6977
|
|
|
6969
6978
|
# Auto-detect frame
|
|
6970
6979
|
craft = config.get("craft_name", "")
|
|
@@ -7249,7 +7258,7 @@ footer{{text-align:center;color:var(--dm);font-size:.7rem;padding:24px 0;border-
|
|
|
7249
7258
|
</div><footer>INAV Blackbox Analyzer v{REPORT_VERSION} - Comparison Report</footer></body></html>"""
|
|
7250
7259
|
|
|
7251
7260
|
|
|
7252
|
-
def _run_comparison(file_a, file_b, args,
|
|
7261
|
+
def _run_comparison(file_a, file_b, args, config_raw):
|
|
7253
7262
|
"""Run comparative analysis on two flight logs."""
|
|
7254
7263
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
7255
7264
|
|
|
@@ -7259,12 +7268,12 @@ def _run_comparison(file_a, file_b, args, diff_raw):
|
|
|
7259
7268
|
print()
|
|
7260
7269
|
|
|
7261
7270
|
print(f" Analyzing flight A...", end=" ", flush=True)
|
|
7262
|
-
res_a = _analyze_for_compare(file_a, args,
|
|
7271
|
+
res_a = _analyze_for_compare(file_a, args, config_raw)
|
|
7263
7272
|
sa = res_a["plan"]["scores"]
|
|
7264
7273
|
print(f"score {sa['overall']:.0f}/100")
|
|
7265
7274
|
|
|
7266
7275
|
print(f" Analyzing flight B...", end=" ", flush=True)
|
|
7267
|
-
res_b = _analyze_for_compare(file_b, args,
|
|
7276
|
+
res_b = _analyze_for_compare(file_b, args, config_raw)
|
|
7268
7277
|
sb = res_b["plan"]["scores"]
|
|
7269
7278
|
print(f"score {sb['overall']:.0f}/100")
|
|
7270
7279
|
|
|
@@ -7774,7 +7783,7 @@ allPlotIds.forEach(srcId => {{
|
|
|
7774
7783
|
</script></body></html>"""
|
|
7775
7784
|
|
|
7776
7785
|
|
|
7777
|
-
def _run_replay(logfile, args,
|
|
7786
|
+
def _run_replay(logfile, args, config_raw):
|
|
7778
7787
|
"""Generate interactive replay HTML for a single flight."""
|
|
7779
7788
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
7780
7789
|
|
|
@@ -7783,8 +7792,8 @@ def _run_replay(logfile, args, diff_raw):
|
|
|
7783
7792
|
|
|
7784
7793
|
raw_params = parse_headers_from_bbl(logfile)
|
|
7785
7794
|
config = extract_fc_config(raw_params)
|
|
7786
|
-
if
|
|
7787
|
-
merge_diff_into_config(config,
|
|
7795
|
+
if config_raw:
|
|
7796
|
+
merge_diff_into_config(config, config_raw)
|
|
7788
7797
|
|
|
7789
7798
|
ext = os.path.splitext(logfile)[1].lower()
|
|
7790
7799
|
is_blackbox = ext in (".bbl", ".bfl", ".bbs")
|
|
@@ -7833,13 +7842,13 @@ def _run_replay(logfile, args, diff_raw):
|
|
|
7833
7842
|
print()
|
|
7834
7843
|
|
|
7835
7844
|
|
|
7836
|
-
def _analyze_single_log(logfile, args,
|
|
7845
|
+
def _analyze_single_log(logfile, args, config_raw=None, summary_only=False):
|
|
7837
7846
|
"""Analyze a single blackbox log file.
|
|
7838
7847
|
|
|
7839
7848
|
Args:
|
|
7840
7849
|
logfile: Path to log file
|
|
7841
7850
|
args: Command line arguments
|
|
7842
|
-
|
|
7851
|
+
config_raw: Optional CLI diff text
|
|
7843
7852
|
summary_only: If True, skip verbose output/reports but still analyze
|
|
7844
7853
|
and store in DB. Returns a summary dict.
|
|
7845
7854
|
|
|
@@ -7878,8 +7887,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
7878
7887
|
config = extract_fc_config(raw_params)
|
|
7879
7888
|
|
|
7880
7889
|
# ── Merge CLI diff if available ──
|
|
7881
|
-
if
|
|
7882
|
-
n_merged = merge_diff_into_config(config,
|
|
7890
|
+
if config_raw:
|
|
7891
|
+
n_merged = merge_diff_into_config(config, config_raw)
|
|
7883
7892
|
mismatches = config.get("_diff_mismatches", [])
|
|
7884
7893
|
if n_merged > 0 or mismatches:
|
|
7885
7894
|
parts = [f"{n_merged} new settings from CLI diff"]
|
|
@@ -8226,7 +8235,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8226
8235
|
db = FlightDB(args.db_path)
|
|
8227
8236
|
flight_id, is_new = db.store_flight(
|
|
8228
8237
|
plan, config, data, hover_osc, motor_analysis,
|
|
8229
|
-
pid_results, noise_results, log_file=logfile,
|
|
8238
|
+
pid_results, noise_results, log_file=logfile, config_raw=config_raw)
|
|
8230
8239
|
db.close()
|
|
8231
8240
|
summary["flight_id"] = flight_id
|
|
8232
8241
|
summary["is_new"] = is_new
|
|
@@ -8241,8 +8250,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8241
8250
|
# ── Config review from diff (if available) ──
|
|
8242
8251
|
# Runs parameter analyzer checks on the FC's current config.
|
|
8243
8252
|
# Catches safety, nav, motor protocol issues that flight data alone can't detect.
|
|
8244
|
-
if
|
|
8245
|
-
_print_config_review(
|
|
8253
|
+
if config_raw and not args.no_terminal and plan["verdict"] != "GROUND_ONLY":
|
|
8254
|
+
_print_config_review(config_raw, config, frame_inches, plan)
|
|
8246
8255
|
|
|
8247
8256
|
nav_results = None # nav analysis only runs in --nav mode
|
|
8248
8257
|
if not args.no_html and plan["verdict"] != "GROUND_ONLY":
|
|
@@ -8284,7 +8293,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8284
8293
|
db = FlightDB(args.db_path)
|
|
8285
8294
|
flight_id, is_new = db.store_flight(
|
|
8286
8295
|
plan, config, data, hover_osc, motor_analysis,
|
|
8287
|
-
pid_results, noise_results, log_file=logfile,
|
|
8296
|
+
pid_results, noise_results, log_file=logfile, config_raw=config_raw)
|
|
8288
8297
|
craft = config.get("craft_name", "unknown")
|
|
8289
8298
|
n_flights = db.get_flight_count(craft)
|
|
8290
8299
|
if is_new:
|
|
@@ -33,7 +33,7 @@ try:
|
|
|
33
33
|
except ImportError:
|
|
34
34
|
serial = None # Checked in open()
|
|
35
35
|
|
|
36
|
-
VERSION = "2.
|
|
36
|
+
VERSION = "2.18.0"
|
|
37
37
|
|
|
38
38
|
# ─── MSP Command IDs ─────────────────────────────────────────────────────────
|
|
39
39
|
|
|
@@ -1005,6 +1005,17 @@ class INAVDevice:
|
|
|
1005
1005
|
"""
|
|
1006
1006
|
return self.cli_command("diff all", timeout=timeout)
|
|
1007
1007
|
|
|
1008
|
+
def get_dump_all(self, timeout=30.0):
|
|
1009
|
+
"""Pull the full 'dump all' configuration from the FC.
|
|
1010
|
+
|
|
1011
|
+
This is a complete backup of every parameter — much larger output
|
|
1012
|
+
than 'diff all' which only shows non-defaults.
|
|
1013
|
+
|
|
1014
|
+
Returns:
|
|
1015
|
+
Raw dump output string, or None on error
|
|
1016
|
+
"""
|
|
1017
|
+
return self.cli_command("dump all", timeout=timeout)
|
|
1018
|
+
|
|
1008
1019
|
def cli_batch(self, commands, timeout=5.0, save=True):
|
|
1009
1020
|
"""Send multiple CLI commands in a single CLI session.
|
|
1010
1021
|
|
|
@@ -21,7 +21,7 @@ import sys
|
|
|
21
21
|
import textwrap
|
|
22
22
|
from datetime import datetime
|
|
23
23
|
|
|
24
|
-
VERSION = "2.
|
|
24
|
+
VERSION = "2.18.0"
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _enable_ansi_colors():
|
|
@@ -811,7 +811,7 @@ def run_all_checks(parsed, frame_inches=None, blackbox_state=None):
|
|
|
811
811
|
findings.extend(check_motors_protocol(parsed))
|
|
812
812
|
findings.extend(check_filters(parsed, frame_inches))
|
|
813
813
|
findings.extend(check_pid_config(parsed, frame_inches))
|
|
814
|
-
findings.extend(check_navigation(parsed))
|
|
814
|
+
findings.extend(check_navigation(parsed, frame_inches))
|
|
815
815
|
findings.extend(check_gps(parsed))
|
|
816
816
|
findings.extend(check_blackbox(parsed))
|
|
817
817
|
findings.extend(check_battery(parsed))
|
|
@@ -1280,7 +1280,7 @@ def check_pid_config(parsed, frame_inches=None):
|
|
|
1280
1280
|
|
|
1281
1281
|
# ─── Navigation Checks ───────────────────────────────────────────────────────
|
|
1282
1282
|
|
|
1283
|
-
def check_navigation(parsed):
|
|
1283
|
+
def check_navigation(parsed, frame_inches=None):
|
|
1284
1284
|
findings = []
|
|
1285
1285
|
|
|
1286
1286
|
rth_alt = get_setting(parsed, "nav_rth_altitude", 5000)
|
|
@@ -1346,26 +1346,123 @@ def check_navigation(parsed):
|
|
|
1346
1346
|
setting="nav_mc_hover_thr",
|
|
1347
1347
|
current=str(hover)))
|
|
1348
1348
|
|
|
1349
|
-
#
|
|
1350
|
-
|
|
1351
|
-
|
|
1349
|
+
# ── Frame-aware nav PID checks ──
|
|
1350
|
+
# INAV defaults are tuned for 5" quads. Larger frames have more inertia
|
|
1351
|
+
# and need softer nav PIDs and longer deceleration time. The default
|
|
1352
|
+
# values cause oscillation on deceleration, overshoot on RTH arrival,
|
|
1353
|
+
# and bouncy position hold on 10"+ frames.
|
|
1354
|
+
#
|
|
1355
|
+
# Recommended ranges by frame size:
|
|
1356
|
+
# 5": pos_p=50-65 vel_p=35-50 vel_d=80-120 decel=100-150
|
|
1357
|
+
# 7": pos_p=40-55 vel_p=30-40 vel_d=80-120 decel=120-180
|
|
1358
|
+
# 10": pos_p=30-45 vel_p=20-30 vel_d=80-120 decel=180-280
|
|
1359
|
+
# 12": pos_p=25-40 vel_p=15-25 vel_d=80-120 decel=220-350
|
|
1360
|
+
# 15": pos_p=20-35 vel_p=10-20 vel_d=80-120 decel=280-400
|
|
1361
|
+
|
|
1362
|
+
nav_rec = None
|
|
1363
|
+
if frame_inches and frame_inches >= 7:
|
|
1364
|
+
if frame_inches >= 15:
|
|
1365
|
+
nav_rec = {"pos_p_max": 35, "vel_p_max": 20, "decel_min": 280,
|
|
1366
|
+
"pos_p_rec": 25, "vel_p_rec": 15, "vel_i_rec": 8, "decel_rec": 350}
|
|
1367
|
+
elif frame_inches >= 12:
|
|
1368
|
+
nav_rec = {"pos_p_max": 40, "vel_p_max": 25, "decel_min": 220,
|
|
1369
|
+
"pos_p_rec": 30, "vel_p_rec": 20, "vel_i_rec": 10, "decel_rec": 280}
|
|
1370
|
+
elif frame_inches >= 10:
|
|
1371
|
+
nav_rec = {"pos_p_max": 45, "vel_p_max": 30, "decel_min": 180,
|
|
1372
|
+
"pos_p_rec": 40, "vel_p_rec": 25, "vel_i_rec": 10, "decel_rec": 250}
|
|
1373
|
+
elif frame_inches >= 7:
|
|
1374
|
+
nav_rec = {"pos_p_max": 55, "vel_p_max": 40, "decel_min": 120,
|
|
1375
|
+
"pos_p_rec": 45, "vel_p_rec": 35, "vel_i_rec": 15, "decel_rec": 150}
|
|
1376
|
+
|
|
1377
|
+
# INAV defaults for nav PIDs (these won't appear in diff all if unchanged)
|
|
1378
|
+
INAV_NAV_DEFAULTS = {
|
|
1379
|
+
"nav_mc_pos_xy_p": 65,
|
|
1380
|
+
"nav_mc_vel_xy_p": 40,
|
|
1381
|
+
"nav_mc_vel_xy_i": 15,
|
|
1382
|
+
"nav_mc_pos_deceleration_time": 120,
|
|
1383
|
+
"nav_mc_heading_p": 60,
|
|
1384
|
+
}
|
|
1352
1385
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1386
|
+
pos_p = profile.get("nav_mc_pos_xy_p",
|
|
1387
|
+
get_setting(parsed, "nav_mc_pos_xy_p", INAV_NAV_DEFAULTS["nav_mc_pos_xy_p"]))
|
|
1388
|
+
vel_p = profile.get("nav_mc_vel_xy_p",
|
|
1389
|
+
get_setting(parsed, "nav_mc_vel_xy_p", INAV_NAV_DEFAULTS["nav_mc_vel_xy_p"]))
|
|
1390
|
+
vel_i = profile.get("nav_mc_vel_xy_i",
|
|
1391
|
+
get_setting(parsed, "nav_mc_vel_xy_i", INAV_NAV_DEFAULTS["nav_mc_vel_xy_i"]))
|
|
1392
|
+
decel = get_setting(parsed, "nav_mc_pos_deceleration_time",
|
|
1393
|
+
INAV_NAV_DEFAULTS["nav_mc_pos_deceleration_time"])
|
|
1394
|
+
heading_p = profile.get("nav_mc_heading_p",
|
|
1395
|
+
get_setting(parsed, "nav_mc_heading_p", INAV_NAV_DEFAULTS["nav_mc_heading_p"]))
|
|
1396
|
+
|
|
1397
|
+
if nav_rec:
|
|
1398
|
+
is_default_pos = (pos_p == INAV_NAV_DEFAULTS["nav_mc_pos_xy_p"])
|
|
1399
|
+
is_default_vel = (vel_p == INAV_NAV_DEFAULTS["nav_mc_vel_xy_p"])
|
|
1400
|
+
is_default_decel = (decel == INAV_NAV_DEFAULTS["nav_mc_pos_deceleration_time"])
|
|
1401
|
+
default_note = " (INAV default - tuned for 5-inch)"
|
|
1402
|
+
|
|
1403
|
+
# Frame-aware checks
|
|
1404
|
+
if pos_p is not None and isinstance(pos_p, (int, float)):
|
|
1405
|
+
if pos_p > nav_rec["pos_p_max"]:
|
|
1406
|
+
val_note = default_note if is_default_pos else ""
|
|
1407
|
+
findings.append(Finding(
|
|
1408
|
+
WARNING, "Navigation",
|
|
1409
|
+
f"Position P = {pos_p}{val_note} - too aggressive for {frame_inches}-inch",
|
|
1410
|
+
f"On {frame_inches}-inch with more "
|
|
1411
|
+
f"inertia, high position P causes overshoot and oscillation on RTH arrival "
|
|
1412
|
+
f"and position hold. The quad overshoots the target position, corrects back, "
|
|
1413
|
+
f"overshoots again.",
|
|
1414
|
+
setting="nav_mc_pos_xy_p",
|
|
1415
|
+
current=str(pos_p),
|
|
1416
|
+
recommended=str(nav_rec["pos_p_rec"]),
|
|
1417
|
+
cli_fix=f"set nav_mc_pos_xy_p = {nav_rec['pos_p_rec']}"))
|
|
1418
|
+
|
|
1419
|
+
if vel_p is not None and isinstance(vel_p, (int, float)):
|
|
1420
|
+
if vel_p > nav_rec["vel_p_max"]:
|
|
1421
|
+
val_note = default_note if is_default_vel else ""
|
|
1422
|
+
findings.append(Finding(
|
|
1423
|
+
WARNING, "Navigation",
|
|
1424
|
+
f"Velocity XY P = {vel_p}{val_note} - too aggressive for {frame_inches}-inch",
|
|
1425
|
+
f"Controls how hard the quad brakes when decelerating. "
|
|
1426
|
+
f"On {frame_inches}-inch, the quad can't stop as fast due to momentum, "
|
|
1427
|
+
f"so high velocity P causes oscillation in the direction of travel when "
|
|
1428
|
+
f"stopping or changing direction.",
|
|
1429
|
+
setting="nav_mc_vel_xy_p",
|
|
1430
|
+
current=str(vel_p),
|
|
1431
|
+
recommended=str(nav_rec["vel_p_rec"]),
|
|
1432
|
+
cli_fix=f"set nav_mc_vel_xy_p = {nav_rec['vel_p_rec']}"))
|
|
1433
|
+
|
|
1434
|
+
if decel is not None and isinstance(decel, (int, float)):
|
|
1435
|
+
if decel < nav_rec["decel_min"]:
|
|
1436
|
+
val_note = default_note if is_default_decel else ""
|
|
1437
|
+
findings.append(Finding(
|
|
1438
|
+
WARNING, "Navigation",
|
|
1439
|
+
f"Deceleration time = {decel} ({decel/100:.1f}s){val_note} - too short for {frame_inches}-inch",
|
|
1440
|
+
f"This controls how quickly the quad tries to stop from cruise speed. "
|
|
1441
|
+
f"A {frame_inches}-inch has much more "
|
|
1442
|
+
f"momentum and needs more distance/time to decelerate smoothly. "
|
|
1443
|
+
f"Too short causes overshoot and oscillation on RTH and position hold transitions.",
|
|
1444
|
+
setting="nav_mc_pos_deceleration_time",
|
|
1445
|
+
current=f"{decel} ({decel/100:.1f}s)",
|
|
1446
|
+
recommended=f"{nav_rec['decel_rec']} ({nav_rec['decel_rec']/100:.1f}s)",
|
|
1447
|
+
cli_fix=f"set nav_mc_pos_deceleration_time = {nav_rec['decel_rec']}"))
|
|
1448
|
+
else:
|
|
1449
|
+
# Generic checks (no frame size or small frame)
|
|
1450
|
+
if pos_p is not None and isinstance(pos_p, (int, float)):
|
|
1451
|
+
if pos_p > 50:
|
|
1452
|
+
findings.append(Finding(
|
|
1453
|
+
WARNING, "Navigation", f"Position hold P = {pos_p} - aggressive",
|
|
1454
|
+
"High position P gain can cause oscillation in position hold and RTH. "
|
|
1455
|
+
"The quad overcorrects, overshoots, and oscillates around the target position.",
|
|
1456
|
+
setting="nav_mc_pos_xy_p",
|
|
1457
|
+
current=str(pos_p),
|
|
1458
|
+
recommended="20-35",
|
|
1459
|
+
cli_fix=f"set nav_mc_pos_xy_p = 30"))
|
|
1460
|
+
elif pos_p < 15:
|
|
1461
|
+
findings.append(Finding(
|
|
1462
|
+
INFO, "Navigation", f"Position hold P = {pos_p} - conservative",
|
|
1463
|
+
"Low position P may result in slow corrections and drifting in wind.",
|
|
1464
|
+
setting="nav_mc_pos_xy_p",
|
|
1465
|
+
current=str(pos_p)))
|
|
1369
1466
|
|
|
1370
1467
|
if heading_p is not None and isinstance(heading_p, (int, float)):
|
|
1371
1468
|
if heading_p > 60:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inav-toolkit"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.18.0"
|
|
8
8
|
description = "Blackbox analyzer, parameter checker, and tuning wizard for INAV flight controllers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|