inav-toolkit 2.17.0__tar.gz → 2.19.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.
Files changed (24) hide show
  1. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/PKG-INFO +1 -1
  2. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/__init__.py +1 -1
  3. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/blackbox_analyzer.py +214 -80
  4. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/flight_db.py +1 -1
  5. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/msp.py +12 -1
  6. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/param_analyzer.py +119 -22
  7. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/vtol_configurator.py +1 -1
  8. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/wizard.py +1 -1
  9. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/PKG-INFO +1 -1
  10. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/pyproject.toml +1 -1
  11. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/LICENSE +0 -0
  12. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/README.md +0 -0
  13. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/autotune.py +0 -0
  14. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/i18n.py +0 -0
  15. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/locales/en.json +0 -0
  16. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/locales/es.json +0 -0
  17. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit/locales/pt_BR.json +0 -0
  18. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/SOURCES.txt +0 -0
  19. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/dependency_links.txt +0 -0
  20. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/entry_points.txt +0 -0
  21. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/requires.txt +0 -0
  22. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/inav_toolkit.egg-info/top_level.txt +0 -0
  23. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/setup.cfg +0 -0
  24. {inav_toolkit-2.17.0 → inav_toolkit-2.19.0}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inav-toolkit
3
- Version: 2.17.0
3
+ Version: 2.19.0
4
4
  Summary: Blackbox analyzer, parameter checker, and tuning wizard for INAV flight controllers
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/agoliveira/INAV-Toolkit
@@ -1,3 +1,3 @@
1
1
  """INAV Toolkit - Blackbox analyzer, parameter checker, and tuning wizard for INAV flight controllers."""
2
2
 
3
- __version__ = "2.17.0"
3
+ __version__ = "2.19.0"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- INAV Blackbox Analyzer - Multirotor Tuning Tool v2.17.0
3
+ INAV Blackbox Analyzer - Multirotor Tuning Tool v2.19.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.17.0"
107
+ REPORT_VERSION = "2.19.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, diff_raw):
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
- diff_raw: Raw 'diff all' output string
898
+ config_raw: Raw 'diff all' output string
899
899
 
900
900
  Returns:
901
901
  Number of settings merged
902
902
  """
903
- if not diff_raw:
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(diff_raw)
910
+ diff_settings = parse_diff_output(config_raw)
911
911
 
912
912
  merged = 0
913
913
  mismatches = []
@@ -6098,10 +6098,95 @@ def count_blackbox_logs(filepath):
6098
6098
  return max(1, count)
6099
6099
 
6100
6100
 
6101
+ # ─── Post-Analysis Cleanup ────────────────────────────────────────────────────
6102
+
6103
+ def _post_analysis_cleanup(blackbox_dir, raw_download, split_files, analyzed_file,
6104
+ archive=False, keep_logs=False):
6105
+ """Clean up blackbox directory after successful analysis.
6106
+
6107
+ Default: delete raw download and all split files.
6108
+ --archive: compress analyzed flight to archive/ first.
6109
+ --keep-logs: skip everything.
6110
+
6111
+ Also cleans stale .bbl files from previous sessions.
6112
+ """
6113
+ if keep_logs:
6114
+ return
6115
+
6116
+ R, B, C, G, Y, RED, DIM = _colors()
6117
+ deleted = 0
6118
+ archived = 0
6119
+
6120
+ # Archive the analyzed flight if requested
6121
+ if archive and analyzed_file and os.path.isfile(analyzed_file):
6122
+ import gzip
6123
+ archive_dir = os.path.join(blackbox_dir, "archive")
6124
+ os.makedirs(archive_dir, exist_ok=True)
6125
+ gz_name = os.path.basename(analyzed_file) + ".gz"
6126
+ gz_path = os.path.join(archive_dir, gz_name)
6127
+ try:
6128
+ with open(analyzed_file, "rb") as f_in:
6129
+ with gzip.open(gz_path, "wb") as f_out:
6130
+ f_out.write(f_in.read())
6131
+ archived_size = os.path.getsize(gz_path)
6132
+ original_size = os.path.getsize(analyzed_file)
6133
+ ratio = archived_size / original_size * 100 if original_size > 0 else 0
6134
+ print(f" Archived: {gz_name} ({archived_size // 1024}KB, {ratio:.0f}% of original)")
6135
+ archived = 1
6136
+ except Exception as e:
6137
+ print(f" Warning: archive failed: {e}")
6138
+
6139
+ # Delete the raw download file
6140
+ if raw_download and os.path.isfile(raw_download):
6141
+ try:
6142
+ sz = os.path.getsize(raw_download) // 1024
6143
+ os.remove(raw_download)
6144
+ deleted += 1
6145
+ except Exception:
6146
+ pass
6147
+
6148
+ # Delete all split files
6149
+ if split_files:
6150
+ for sf in split_files:
6151
+ if sf and os.path.isfile(sf):
6152
+ try:
6153
+ os.remove(sf)
6154
+ deleted += 1
6155
+ except Exception:
6156
+ pass
6157
+
6158
+ # Clean stale .bbl files from previous sessions
6159
+ # (anything not from the current download)
6160
+ current_base = os.path.basename(raw_download) if raw_download else ""
6161
+ if os.path.isdir(blackbox_dir):
6162
+ for fname in os.listdir(blackbox_dir):
6163
+ if not fname.endswith(".bbl"):
6164
+ continue
6165
+ fpath = os.path.join(blackbox_dir, fname)
6166
+ if fpath == raw_download:
6167
+ continue # already handled
6168
+ if any(fpath == sf for sf in (split_files or [])):
6169
+ continue # already handled
6170
+ # This is a stale .bbl from a previous session
6171
+ try:
6172
+ os.remove(fpath)
6173
+ deleted += 1
6174
+ except Exception:
6175
+ pass
6176
+
6177
+ if deleted > 0 or archived > 0:
6178
+ parts = []
6179
+ if deleted > 0:
6180
+ parts.append(f"{deleted} log files removed")
6181
+ if archived > 0:
6182
+ parts.append(f"1 archived")
6183
+ print(f" Cleanup: {', '.join(parts)}")
6184
+
6185
+
6101
6186
  # ─── Main ─────────────────────────────────────────────────────────────────────
6102
6187
 
6103
6188
  def main():
6104
- parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.17.0 - Prescriptive Tuning",
6189
+ parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.19.0 - Prescriptive Tuning",
6105
6190
  formatter_class=argparse.RawDescriptionHelpFormatter)
6106
6191
  parser.add_argument("--version", action="version", version=f"inav-analyze {REPORT_VERSION}")
6107
6192
  parser.add_argument("logfile", nargs="?", default=None,
@@ -6115,8 +6200,14 @@ def main():
6115
6200
  parser.add_argument("--device", metavar="PORT",
6116
6201
  help="Download blackbox from FC via serial. Use 'auto' to scan "
6117
6202
  "or specify port (e.g., /dev/ttyACM0, COM3).")
6118
- parser.add_argument("--erase", action="store_true",
6119
- help="Erase dataflash after successful download.")
6203
+ parser.add_argument("--no-erase", action="store_true",
6204
+ help="Don't erase FC flash after successful download and analysis. "
6205
+ "Default: flash is erased automatically after pipeline completes.")
6206
+ parser.add_argument("--archive", action="store_true",
6207
+ help="Compress the analyzed flight log to blackbox/archive/ instead of deleting. "
6208
+ "Builds a compressed history for re-analysis with future versions.")
6209
+ parser.add_argument("--keep-logs", action="store_true",
6210
+ help="Skip all cleanup - keep raw downloads, splits, and don't erase flash.")
6120
6211
  parser.add_argument("--download-only", action="store_true",
6121
6212
  help="Download blackbox from device but don't analyze.")
6122
6213
  parser.add_argument("--blackbox-dir", default="./blackbox",
@@ -6141,11 +6232,11 @@ def main():
6141
6232
  help="Omit the plain-English description of the quad's behavior.")
6142
6233
  parser.add_argument("--no-color", action="store_true",
6143
6234
  help="Disable colored terminal output.")
6144
- parser.add_argument("--diff", metavar="FILE", default=None,
6145
- help="Path to a CLI 'diff all' file for enriched analysis. "
6235
+ parser.add_argument("--config", metavar="FILE", default=None,
6236
+ help="Path to a CLI 'dump all' or 'diff all' file for enriched analysis. "
6146
6237
  "Adds config cross-referencing to nav and tuning results. "
6147
6238
  "Not needed with --device (config is pulled automatically). "
6148
- "Also auto-discovered if a *_diff.txt file sits next to the BBL.")
6239
+ "Also auto-discovered if a *_dump.txt or *_diff.txt file sits next to the BBL.")
6149
6240
  parser.add_argument("--nav", action="store_true",
6150
6241
  help="Enable navigation health analysis (compass, GPS, baro, "
6151
6242
  "estimator). Works on any flight with nav fields in the log.")
@@ -6187,7 +6278,8 @@ def main():
6187
6278
 
6188
6279
  # ── Device mode: download blackbox from FC ──
6189
6280
  logfile = args.logfile
6190
- diff_raw = None
6281
+ config_raw = None
6282
+ device_port = None
6191
6283
  if args.device:
6192
6284
  try:
6193
6285
  try:
@@ -6257,19 +6349,18 @@ def main():
6257
6349
  pct = summary['used_size'] * 100 // summary['total_size'] if summary['total_size'] > 0 else 0
6258
6350
  print(f" Dataflash: {used_kb:.0f}KB / {total_kb:.0f}KB ({pct}% used)")
6259
6351
 
6260
- # Pull CLI diff (always when connected - enriches analysis)
6261
- diff_raw = None
6262
- print(" Pulling configuration (diff all)...", end="", flush=True)
6263
- diff_raw = fc.get_diff_all(timeout=10.0)
6264
- if diff_raw:
6265
- n_settings = len([l for l in diff_raw.splitlines() if l.strip().startswith("set ")])
6266
- print(f" {n_settings} settings")
6267
- # Save diff to file alongside blackbox
6268
- diff_path = os.path.join(args.blackbox_dir, f"{info['craft_name'] or 'fc'}_diff.txt")
6352
+ # Pull full config dump (all parameters used for analysis, fingerprinting, and backup)
6353
+ config_raw = None
6354
+ print(" Pulling configuration (dump all)...", end="", flush=True)
6355
+ config_raw = fc.get_dump_all(timeout=30.0)
6356
+ if config_raw:
6357
+ n_settings = len([l for l in config_raw.splitlines() if l.strip().startswith("set ")])
6358
+ print(f" {n_settings} parameters")
6359
+ config_path = os.path.join(args.blackbox_dir, f"{info['craft_name'] or 'fc'}_dump.txt")
6269
6360
  os.makedirs(args.blackbox_dir, exist_ok=True)
6270
- with open(diff_path, "w") as f:
6271
- f.write(diff_raw)
6272
- print(f" Saved: {diff_path}")
6361
+ with open(config_path, "w") as f:
6362
+ f.write(config_raw)
6363
+ print(f" Saved: {config_path}")
6273
6364
  else:
6274
6365
  print(" no response (FC may not support CLI over MSP)")
6275
6366
 
@@ -6280,13 +6371,16 @@ def main():
6280
6371
  print()
6281
6372
  filepath = fc.download_blackbox(
6282
6373
  output_dir=args.blackbox_dir,
6283
- erase_after=args.erase,
6374
+ erase_after=False, # erase happens after successful analysis
6284
6375
  )
6285
6376
 
6286
6377
  if not filepath:
6287
6378
  print(" ERROR: Download failed.")
6288
6379
  sys.exit(1)
6289
6380
 
6381
+ # Store port for post-analysis erase
6382
+ device_port = fc.port_path if hasattr(fc, 'port_path') else args.device
6383
+
6290
6384
  if args.download_only:
6291
6385
  print(f"\n To analyze:\n python3 {sys.argv[0]} {filepath}")
6292
6386
  sys.exit(0)
@@ -6311,39 +6405,42 @@ def main():
6311
6405
  print(f"ERROR: File not found: {logfile}"); sys.exit(1)
6312
6406
 
6313
6407
  # ── Load diff from file (when not using --device) ──
6314
- if diff_raw is None:
6315
- diff_file = None
6316
- if args.diff:
6317
- # Explicit file path: --diff my_diff.txt
6318
- diff_file = args.diff
6408
+ if config_raw is None:
6409
+ config_file = None
6410
+ if args.config:
6411
+ # Explicit file path: --config my_dump.txt
6412
+ config_file = args.config
6319
6413
  else:
6320
- # Auto-discover diff files in same directory as BBL
6414
+ # Auto-discover config files in same directory as BBL
6415
+ # Prefer dump (complete) over diff (only changed params)
6321
6416
  log_dir = os.path.dirname(os.path.abspath(logfile))
6322
6417
  candidates = []
6323
6418
  for fname in os.listdir(log_dir):
6324
- if fname.endswith("_diff.txt") or fname == "diff.txt" or fname == "diff_all.txt":
6419
+ if (fname.endswith("_dump.txt") or fname.endswith("_diff.txt")
6420
+ or fname in ("dump.txt", "dump_all.txt", "diff.txt", "diff_all.txt")):
6325
6421
  candidates.append(os.path.join(log_dir, fname))
6326
- if len(candidates) == 1:
6327
- diff_file = candidates[0]
6328
- elif len(candidates) > 1:
6329
- # Pick the most recently modified
6330
- diff_file = max(candidates, key=os.path.getmtime)
6331
-
6332
- if diff_file:
6333
- if os.path.isfile(diff_file):
6422
+ if candidates:
6423
+ # Prefer dump over diff, then most recent
6424
+ def _score(path):
6425
+ is_dump = "_dump" in path or "dump" in os.path.basename(path)
6426
+ return (1 if is_dump else 0, os.path.getmtime(path))
6427
+ config_file = max(candidates, key=_score)
6428
+
6429
+ if config_file:
6430
+ if os.path.isfile(config_file):
6334
6431
  try:
6335
- with open(diff_file, "r", errors="ignore") as f:
6336
- diff_raw = f.read()
6337
- n_settings = len([l for l in diff_raw.splitlines()
6432
+ with open(config_file, "r", errors="ignore") as f:
6433
+ config_raw = f.read()
6434
+ n_settings = len([l for l in config_raw.splitlines()
6338
6435
  if l.strip().startswith("set ")])
6339
6436
  if n_settings > 0:
6340
- print(f" Config: {os.path.basename(diff_file)} ({n_settings} settings)")
6437
+ print(f" Config: {os.path.basename(config_file)} ({n_settings} settings)")
6341
6438
  else:
6342
- diff_raw = None # not a valid diff file
6439
+ config_raw = None # not a valid config file
6343
6440
  except Exception:
6344
- diff_raw = None
6441
+ config_raw = None
6345
6442
  else:
6346
- print(f" Warning: Diff file not found: {diff_file}")
6443
+ print(f" Warning: Config file not found: {config_file}")
6347
6444
 
6348
6445
  # ── History mode: show progression and exit ──
6349
6446
  if args.history or args.trend:
@@ -6374,14 +6471,14 @@ def main():
6374
6471
  if not os.path.isfile(args.compare):
6375
6472
  print(f"ERROR: Comparison file not found: {args.compare}")
6376
6473
  sys.exit(1)
6377
- _run_comparison(logfile, args.compare, args, diff_raw)
6474
+ _run_comparison(logfile, args.compare, args, config_raw)
6378
6475
  return
6379
6476
 
6380
6477
  # ── Replay mode: interactive HTML time-series ──
6381
6478
  if args.replay:
6382
6479
  if not logfile:
6383
6480
  parser.error("logfile required for --replay")
6384
- _run_replay(logfile, args, diff_raw)
6481
+ _run_replay(logfile, args, config_raw)
6385
6482
  return
6386
6483
 
6387
6484
  # ── Log quality check mode ──
@@ -6415,20 +6512,48 @@ def main():
6415
6512
  # Flash may contain flights from multiple sessions (config changes between
6416
6513
  # arm cycles). Only the LATEST session's best flight deserves full analysis.
6417
6514
  # Earlier flights are stored in DB for progression but shown as one-liners.
6515
+ analyzed_file = None
6418
6516
  if getattr(args, 'nav', False):
6419
6517
  # Nav mode: skip multi-flight tuning scan, analyze last flight directly
6420
6518
  target = log_files[-1]
6421
6519
  if len(log_files) > 1:
6422
6520
  print(f"\n Nav mode: analyzing last flight ({len(log_files)} in flash)")
6423
- _analyze_single_log(target, args, diff_raw)
6521
+ _analyze_single_log(target, args, config_raw)
6522
+ analyzed_file = target
6424
6523
  elif len(log_files) > 1:
6425
- _process_multi_log(log_files, args, diff_raw)
6524
+ analyzed_file = _process_multi_log(log_files, args, config_raw)
6426
6525
  else:
6427
6526
  # Single flight - full analysis
6428
- _analyze_single_log(log_files[0], args, diff_raw)
6527
+ _analyze_single_log(log_files[0], args, config_raw)
6528
+ analyzed_file = log_files[0]
6529
+
6530
+ # ── Post-analysis cleanup (only in device mode) ──
6531
+ if device_port and not args.keep_logs:
6532
+ # raw_download is the original downloaded file (pre-split)
6533
+ # For single-flight, it's the same as the analyzed file
6534
+ raw_download = logfile
6535
+ split_files = log_files if n_logs > 1 else []
6536
+
6537
+ _post_analysis_cleanup(
6538
+ args.blackbox_dir, raw_download, split_files, analyzed_file,
6539
+ archive=args.archive, keep_logs=args.keep_logs)
6540
+
6541
+ # Erase FC flash
6542
+ if not args.no_erase:
6543
+ try:
6544
+ try:
6545
+ from inav_toolkit.msp import INAVDevice
6546
+ except ImportError:
6547
+ from inav_msp import INAVDevice
6548
+ fc2 = INAVDevice(device_port)
6549
+ fc2.open()
6550
+ fc2.erase_dataflash()
6551
+ fc2.close()
6552
+ except Exception as e:
6553
+ print(f" Erase failed: {e}")
6429
6554
 
6430
6555
 
6431
- def _process_multi_log(log_files, args, diff_raw):
6556
+ def _process_multi_log(log_files, args, config_raw):
6432
6557
  """Handle multiple flights from a single flash download.
6433
6558
 
6434
6559
  Strategy:
@@ -6444,7 +6569,7 @@ def _process_multi_log(log_files, args, diff_raw):
6444
6569
  print(f" Scanning {len(log_files)} flights...")
6445
6570
  summaries = []
6446
6571
  for lf in log_files:
6447
- s = _analyze_single_log(lf, args, diff_raw, summary_only=True)
6572
+ s = _analyze_single_log(lf, args, config_raw, summary_only=True)
6448
6573
  if s:
6449
6574
  summaries.append(s)
6450
6575
 
@@ -6457,7 +6582,7 @@ def _process_multi_log(log_files, args, diff_raw):
6457
6582
  # header config matches the diff are "current session" (post-change),
6458
6583
  # flights that don't match are "old session" (pre-change).
6459
6584
  # Without a diff, fall back to comparing consecutive flights.
6460
- current_fp = _fingerprint_from_diff(diff_raw)
6585
+ current_fp = _fingerprint_from_diff(config_raw)
6461
6586
  is_current = [] # True/False per flight
6462
6587
 
6463
6588
  if current_fp:
@@ -6581,7 +6706,7 @@ def _process_multi_log(log_files, args, diff_raw):
6581
6706
 
6582
6707
  # ── Phase 5: Full analysis on the selected flight ──
6583
6708
  print(f"{'═' * 70}")
6584
- _analyze_single_log(log_files[best_idx], args, diff_raw)
6709
+ _analyze_single_log(log_files[best_idx], args, config_raw)
6585
6710
 
6586
6711
  # ── Phase 6: Show cross-session progression ──
6587
6712
  if not args.no_db:
@@ -6611,6 +6736,8 @@ def _process_multi_log(log_files, args, diff_raw):
6611
6736
  except Exception:
6612
6737
  pass
6613
6738
 
6739
+ return log_files[best_idx]
6740
+
6614
6741
 
6615
6742
  def _verdict_short(verdict):
6616
6743
  """Short display string for verdict codes."""
@@ -6635,7 +6762,7 @@ def _config_fingerprint(config):
6635
6762
  return "|".join(parts) if parts else ""
6636
6763
 
6637
6764
 
6638
- def _fingerprint_from_diff(diff_raw):
6765
+ def _fingerprint_from_diff(config_raw):
6639
6766
  """Build a config fingerprint from CLI 'diff all' output.
6640
6767
 
6641
6768
  This represents the FC's CURRENT config - the ground truth.
@@ -6643,14 +6770,14 @@ def _fingerprint_from_diff(diff_raw):
6643
6770
  session; flights that don't match are from before the user applied
6644
6771
  changes.
6645
6772
  """
6646
- if not diff_raw:
6773
+ if not config_raw:
6647
6774
  return ""
6648
6775
 
6649
6776
  try:
6650
6777
  from inav_toolkit.flight_db import parse_diff_output
6651
6778
  except ImportError:
6652
6779
  from inav_flight_db import parse_diff_output
6653
- diff_settings = parse_diff_output(diff_raw)
6780
+ diff_settings = parse_diff_output(config_raw)
6654
6781
 
6655
6782
  # Map CLI names → config keys (same mapping as merge_diff_into_config)
6656
6783
  config = {}
@@ -6666,8 +6793,12 @@ def _fingerprint_from_diff(diff_raw):
6666
6793
  return _config_fingerprint(config)
6667
6794
 
6668
6795
 
6669
- def _print_config_review(diff_raw, config, frame_inches, plan):
6670
- """Run parameter analyzer on FC diff and show findings not covered by flight analysis.
6796
+ def _print_config_review(config_raw, config, frame_inches, plan):
6797
+ """Run parameter analyzer on FC config and show findings not covered by flight analysis.
6798
+
6799
+ Accepts either 'dump all' or 'diff all' output. Dump is preferred since it
6800
+ includes every parameter including unchanged defaults, which matters for
6801
+ nav PID checks on large frames.
6671
6802
 
6672
6803
  Only shows CRITICAL and WARNING findings from categories that the blackbox
6673
6804
  analyzer doesn't cover (safety, nav, motor protocol, GPS, battery, RX).
@@ -6676,6 +6807,9 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
6676
6807
  """
6677
6808
  R, B, C, G, Y, RED, DIM = _colors()
6678
6809
 
6810
+ if not config_raw:
6811
+ return
6812
+
6679
6813
  try:
6680
6814
  try:
6681
6815
  from inav_toolkit.param_analyzer import parse_diff_all, run_all_checks, CRITICAL, WARNING
@@ -6685,7 +6819,7 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
6685
6819
  return # param analyzer not available
6686
6820
 
6687
6821
  try:
6688
- parsed = parse_diff_all(diff_raw)
6822
+ parsed = parse_diff_all(config_raw)
6689
6823
  except Exception:
6690
6824
  return
6691
6825
 
@@ -6956,15 +7090,15 @@ footer {{ text-align:center; color:#555; margin-top:30px; padding:10px; border-t
6956
7090
 
6957
7091
  # ─── Comparison Mode ─────────────────────────────────────────────────────────
6958
7092
 
6959
- def _analyze_for_compare(logfile, args, diff_raw=None):
7093
+ def _analyze_for_compare(logfile, args, config_raw=None):
6960
7094
  """Run analysis pipeline on a single file and return structured results.
6961
7095
  Returns dict with: config, data, noise_results, pid_results, motor_analysis,
6962
7096
  dterm_results, plan, noise_fp, hover_osc, profile
6963
7097
  """
6964
7098
  raw_params = parse_headers_from_bbl(logfile)
6965
7099
  config = extract_fc_config(raw_params)
6966
- if diff_raw:
6967
- merge_diff_into_config(config, diff_raw)
7100
+ if config_raw:
7101
+ merge_diff_into_config(config, config_raw)
6968
7102
 
6969
7103
  # Auto-detect frame
6970
7104
  craft = config.get("craft_name", "")
@@ -7249,7 +7383,7 @@ footer{{text-align:center;color:var(--dm);font-size:.7rem;padding:24px 0;border-
7249
7383
  </div><footer>INAV Blackbox Analyzer v{REPORT_VERSION} - Comparison Report</footer></body></html>"""
7250
7384
 
7251
7385
 
7252
- def _run_comparison(file_a, file_b, args, diff_raw):
7386
+ def _run_comparison(file_a, file_b, args, config_raw):
7253
7387
  """Run comparative analysis on two flight logs."""
7254
7388
  R, B, C, G, Y, RED, DIM = _colors()
7255
7389
 
@@ -7259,12 +7393,12 @@ def _run_comparison(file_a, file_b, args, diff_raw):
7259
7393
  print()
7260
7394
 
7261
7395
  print(f" Analyzing flight A...", end=" ", flush=True)
7262
- res_a = _analyze_for_compare(file_a, args, diff_raw)
7396
+ res_a = _analyze_for_compare(file_a, args, config_raw)
7263
7397
  sa = res_a["plan"]["scores"]
7264
7398
  print(f"score {sa['overall']:.0f}/100")
7265
7399
 
7266
7400
  print(f" Analyzing flight B...", end=" ", flush=True)
7267
- res_b = _analyze_for_compare(file_b, args, diff_raw)
7401
+ res_b = _analyze_for_compare(file_b, args, config_raw)
7268
7402
  sb = res_b["plan"]["scores"]
7269
7403
  print(f"score {sb['overall']:.0f}/100")
7270
7404
 
@@ -7774,7 +7908,7 @@ allPlotIds.forEach(srcId => {{
7774
7908
  </script></body></html>"""
7775
7909
 
7776
7910
 
7777
- def _run_replay(logfile, args, diff_raw):
7911
+ def _run_replay(logfile, args, config_raw):
7778
7912
  """Generate interactive replay HTML for a single flight."""
7779
7913
  R, B, C, G, Y, RED, DIM = _colors()
7780
7914
 
@@ -7783,8 +7917,8 @@ def _run_replay(logfile, args, diff_raw):
7783
7917
 
7784
7918
  raw_params = parse_headers_from_bbl(logfile)
7785
7919
  config = extract_fc_config(raw_params)
7786
- if diff_raw:
7787
- merge_diff_into_config(config, diff_raw)
7920
+ if config_raw:
7921
+ merge_diff_into_config(config, config_raw)
7788
7922
 
7789
7923
  ext = os.path.splitext(logfile)[1].lower()
7790
7924
  is_blackbox = ext in (".bbl", ".bfl", ".bbs")
@@ -7833,13 +7967,13 @@ def _run_replay(logfile, args, diff_raw):
7833
7967
  print()
7834
7968
 
7835
7969
 
7836
- def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
7970
+ def _analyze_single_log(logfile, args, config_raw=None, summary_only=False):
7837
7971
  """Analyze a single blackbox log file.
7838
7972
 
7839
7973
  Args:
7840
7974
  logfile: Path to log file
7841
7975
  args: Command line arguments
7842
- diff_raw: Optional CLI diff text
7976
+ config_raw: Optional CLI diff text
7843
7977
  summary_only: If True, skip verbose output/reports but still analyze
7844
7978
  and store in DB. Returns a summary dict.
7845
7979
 
@@ -7878,8 +8012,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
7878
8012
  config = extract_fc_config(raw_params)
7879
8013
 
7880
8014
  # ── Merge CLI diff if available ──
7881
- if diff_raw:
7882
- n_merged = merge_diff_into_config(config, diff_raw)
8015
+ if config_raw:
8016
+ n_merged = merge_diff_into_config(config, config_raw)
7883
8017
  mismatches = config.get("_diff_mismatches", [])
7884
8018
  if n_merged > 0 or mismatches:
7885
8019
  parts = [f"{n_merged} new settings from CLI diff"]
@@ -8226,7 +8360,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
8226
8360
  db = FlightDB(args.db_path)
8227
8361
  flight_id, is_new = db.store_flight(
8228
8362
  plan, config, data, hover_osc, motor_analysis,
8229
- pid_results, noise_results, log_file=logfile, diff_raw=diff_raw)
8363
+ pid_results, noise_results, log_file=logfile, config_raw=config_raw)
8230
8364
  db.close()
8231
8365
  summary["flight_id"] = flight_id
8232
8366
  summary["is_new"] = is_new
@@ -8241,8 +8375,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
8241
8375
  # ── Config review from diff (if available) ──
8242
8376
  # Runs parameter analyzer checks on the FC's current config.
8243
8377
  # Catches safety, nav, motor protocol issues that flight data alone can't detect.
8244
- if diff_raw and not args.no_terminal and plan["verdict"] != "GROUND_ONLY":
8245
- _print_config_review(diff_raw, config, frame_inches, plan)
8378
+ if config_raw and not args.no_terminal and plan["verdict"] != "GROUND_ONLY":
8379
+ _print_config_review(config_raw, config, frame_inches, plan)
8246
8380
 
8247
8381
  nav_results = None # nav analysis only runs in --nav mode
8248
8382
  if not args.no_html and plan["verdict"] != "GROUND_ONLY":
@@ -8284,7 +8418,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
8284
8418
  db = FlightDB(args.db_path)
8285
8419
  flight_id, is_new = db.store_flight(
8286
8420
  plan, config, data, hover_osc, motor_analysis,
8287
- pid_results, noise_results, log_file=logfile, diff_raw=diff_raw)
8421
+ pid_results, noise_results, log_file=logfile, config_raw=config_raw)
8288
8422
  craft = config.get("craft_name", "unknown")
8289
8423
  n_flights = db.get_flight_count(craft)
8290
8424
  if is_new:
@@ -21,7 +21,7 @@ import json
21
21
  import os
22
22
  from datetime import datetime
23
23
 
24
- VERSION = "2.17.0"
24
+ VERSION = "2.19.0"
25
25
 
26
26
  SCHEMA_VERSION = 1
27
27
 
@@ -33,7 +33,7 @@ try:
33
33
  except ImportError:
34
34
  serial = None # Checked in open()
35
35
 
36
- VERSION = "2.17.0"
36
+ VERSION = "2.19.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.17.0"
24
+ VERSION = "2.19.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
- # Nav PID values from active profile
1350
- pos_p = profile.get("nav_mc_pos_xy_p", get_setting(parsed, "nav_mc_pos_xy_p", None))
1351
- heading_p = profile.get("nav_mc_heading_p", get_setting(parsed, "nav_mc_heading_p", None))
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
- if pos_p is not None and isinstance(pos_p, (int, float)):
1354
- if pos_p > 50:
1355
- findings.append(Finding(
1356
- WARNING, "Navigation", f"Position hold P = {pos_p} - aggressive",
1357
- "High position P gain can cause oscillation (salad bowling) in position hold and RTH. "
1358
- "The quad overcorrects, overshoots, and oscillates around the target position.",
1359
- setting="nav_mc_pos_xy_p",
1360
- current=str(pos_p),
1361
- recommended="20-35",
1362
- cli_fix=f"set nav_mc_pos_xy_p = 30"))
1363
- elif pos_p < 15:
1364
- findings.append(Finding(
1365
- INFO, "Navigation", f"Position hold P = {pos_p} - conservative",
1366
- "Low position P may result in slow corrections and drifting in wind.",
1367
- setting="nav_mc_pos_xy_p",
1368
- current=str(pos_p)))
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:
@@ -25,7 +25,7 @@ import re
25
25
  import sys
26
26
  import textwrap
27
27
 
28
- VERSION = "2.17.0"
28
+ VERSION = "2.19.0"
29
29
 
30
30
 
31
31
  def _enable_ansi_colors():
@@ -22,7 +22,7 @@ import time
22
22
  try:
23
23
  from inav_toolkit import __version__ as VERSION
24
24
  except ImportError:
25
- VERSION = "2.17.0"
25
+ VERSION = "2.19.0"
26
26
 
27
27
  # Module paths for subprocess invocation (package-aware)
28
28
  ANALYZER_MODULE = "inav_toolkit.blackbox_analyzer"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inav-toolkit
3
- Version: 2.17.0
3
+ Version: 2.19.0
4
4
  Summary: Blackbox analyzer, parameter checker, and tuning wizard for INAV flight controllers
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/agoliveira/INAV-Toolkit
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inav-toolkit"
7
- version = "2.17.0"
7
+ version = "2.19.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