inav-toolkit 2.16.1__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.16.1 → inav_toolkit-2.18.0}/PKG-INFO +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/__init__.py +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/blackbox_analyzer.py +285 -107
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/flight_db.py +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/msp.py +12 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/param_analyzer.py +119 -22
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/vtol_configurator.py +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/wizard.py +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/PKG-INFO +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/pyproject.toml +1 -1
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/LICENSE +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/README.md +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/autotune.py +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/i18n.py +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/locales/en.json +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/locales/es.json +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit/locales/pt_BR.json +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/SOURCES.txt +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/dependency_links.txt +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/entry_points.txt +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/requires.txt +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/inav_toolkit.egg-info/top_level.txt +0 -0
- {inav_toolkit-2.16.1 → inav_toolkit-2.18.0}/setup.cfg +0 -0
- {inav_toolkit-2.16.1 → 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 = []
|
|
@@ -2542,17 +2542,34 @@ def measure_tracking_delay_xcorr(sp, gy, sr, max_delay_ms=200):
|
|
|
2542
2542
|
return float(np.median(delays))
|
|
2543
2543
|
|
|
2544
2544
|
|
|
2545
|
-
def detect_hover_oscillation(data, sr):
|
|
2545
|
+
def detect_hover_oscillation(data, sr, profile=None):
|
|
2546
2546
|
"""Detect oscillation during hover (no/minimal stick input).
|
|
2547
2547
|
|
|
2548
2548
|
Finds segments where setpoint is near zero, then measures gyro
|
|
2549
2549
|
oscillation amplitude and dominant frequency. This catches the most
|
|
2550
2550
|
dangerous tuning problem: a quad that can't hold still.
|
|
2551
2551
|
|
|
2552
|
+
Frame-size aware: larger frames naturally have more gyro activity,
|
|
2553
|
+
and low-frequency wobble (1-5Hz) outdoors is often wind buffeting,
|
|
2554
|
+
not P oscillation. The cause classification accounts for this.
|
|
2555
|
+
|
|
2552
2556
|
Returns list of per-axis dicts:
|
|
2553
2557
|
axis, gyro_rms, gyro_p2p, dominant_freq_hz, severity, cause, hover_seconds
|
|
2554
2558
|
Or empty list if no hover segments found.
|
|
2555
2559
|
"""
|
|
2560
|
+
if profile is None:
|
|
2561
|
+
profile = get_frame_profile(5)
|
|
2562
|
+
|
|
2563
|
+
# Scale severity thresholds by frame size.
|
|
2564
|
+
# Larger frames have more inertia and naturally higher gyro readings.
|
|
2565
|
+
frame_inches = profile.get("frame_inches", 5)
|
|
2566
|
+
size_factor = max(1.0, frame_inches / 7.0) # 1.0 for 7", 1.43 for 10", 2.14 for 15"
|
|
2567
|
+
|
|
2568
|
+
# Severity thresholds (RMS deg/s) — scaled by frame size
|
|
2569
|
+
sev_none = 2.0 * size_factor # 2.0 for 7", 2.9 for 10"
|
|
2570
|
+
sev_mild = 5.0 * size_factor # 5.0 for 7", 7.1 for 10"
|
|
2571
|
+
sev_moderate = 15.0 * size_factor # 15.0 for 7", 21.4 for 10"
|
|
2572
|
+
|
|
2556
2573
|
results = []
|
|
2557
2574
|
min_hover_samples = int(sr * 0.5) # At least 0.5s of hover
|
|
2558
2575
|
|
|
@@ -2609,6 +2626,7 @@ def detect_hover_oscillation(data, sr):
|
|
|
2609
2626
|
|
|
2610
2627
|
# Dominant frequency via FFT
|
|
2611
2628
|
dominant_freq = None
|
|
2629
|
+
peak_prominence = 0 # How much the peak stands out from noise floor
|
|
2612
2630
|
if len(hover_gyro) >= int(sr * 0.25): # Need at least 0.25s for FFT
|
|
2613
2631
|
freqs = rfftfreq(len(hover_gyro), 1.0 / sr)
|
|
2614
2632
|
spectrum = np.abs(rfft(hover_gyro))
|
|
@@ -2621,29 +2639,35 @@ def detect_hover_oscillation(data, sr):
|
|
|
2621
2639
|
dominant_freq = float(masked_freqs[peak_idx])
|
|
2622
2640
|
peak_power = masked_spectrum[peak_idx]
|
|
2623
2641
|
mean_power = np.mean(masked_spectrum)
|
|
2642
|
+
peak_prominence = peak_power / mean_power if mean_power > 0 else 0
|
|
2624
2643
|
# Only report if peak is significantly above noise floor
|
|
2625
|
-
if
|
|
2644
|
+
if peak_prominence < 3:
|
|
2626
2645
|
dominant_freq = None # No clear dominant frequency
|
|
2627
2646
|
|
|
2628
|
-
# Classify severity
|
|
2629
|
-
|
|
2630
|
-
# Mild wobble: rms 2-8, p2p 10-30
|
|
2631
|
-
# Serious oscillation: rms 8-20, p2p 30-80
|
|
2632
|
-
# Dangerous: rms > 20, p2p > 80
|
|
2633
|
-
if gyro_rms < 2:
|
|
2647
|
+
# Classify severity (frame-size-scaled thresholds)
|
|
2648
|
+
if gyro_rms < sev_none:
|
|
2634
2649
|
severity = "none"
|
|
2635
|
-
elif gyro_rms <
|
|
2650
|
+
elif gyro_rms < sev_mild:
|
|
2636
2651
|
severity = "mild"
|
|
2637
|
-
elif gyro_rms <
|
|
2652
|
+
elif gyro_rms < sev_moderate:
|
|
2638
2653
|
severity = "moderate"
|
|
2639
2654
|
else:
|
|
2640
2655
|
severity = "severe"
|
|
2641
2656
|
|
|
2642
|
-
# Classify cause from dominant frequency
|
|
2657
|
+
# Classify cause from dominant frequency — frame-size aware
|
|
2643
2658
|
cause = None
|
|
2644
2659
|
if severity != "none" and dominant_freq is not None:
|
|
2645
2660
|
if dominant_freq < 10:
|
|
2646
|
-
|
|
2661
|
+
# Low-frequency wobble. On large frames (10"+), this is often
|
|
2662
|
+
# wind buffeting or GPS position hold corrections, NOT P oscillation.
|
|
2663
|
+
# True P oscillation has a sharp spectral peak (high prominence).
|
|
2664
|
+
# Wind buffeting is broadband (low prominence).
|
|
2665
|
+
if frame_inches >= 10 and peak_prominence < 6:
|
|
2666
|
+
cause = "wind_buffeting" # Likely environmental, not tuning
|
|
2667
|
+
elif frame_inches >= 10 and dominant_freq < 3:
|
|
2668
|
+
cause = "wind_buffeting" # <3Hz on 10"+ is almost certainly wind
|
|
2669
|
+
else:
|
|
2670
|
+
cause = "P_too_high"
|
|
2647
2671
|
elif dominant_freq < 25:
|
|
2648
2672
|
cause = "PD_interaction" # Mid-freq: P/D fighting
|
|
2649
2673
|
elif dominant_freq < 50:
|
|
@@ -2651,7 +2675,11 @@ def detect_hover_oscillation(data, sr):
|
|
|
2651
2675
|
else:
|
|
2652
2676
|
cause = "filter_gap" # High-freq noise leaking through
|
|
2653
2677
|
elif severity != "none":
|
|
2654
|
-
|
|
2678
|
+
# No dominant frequency — broadband noise, likely environmental
|
|
2679
|
+
if frame_inches >= 10:
|
|
2680
|
+
cause = "wind_buffeting"
|
|
2681
|
+
else:
|
|
2682
|
+
cause = "unknown"
|
|
2655
2683
|
|
|
2656
2684
|
results.append({
|
|
2657
2685
|
"axis": axis,
|
|
@@ -2661,6 +2689,7 @@ def detect_hover_oscillation(data, sr):
|
|
|
2661
2689
|
"severity": severity,
|
|
2662
2690
|
"cause": cause,
|
|
2663
2691
|
"hover_seconds": total_hover_seconds,
|
|
2692
|
+
"peak_prominence": peak_prominence,
|
|
2664
2693
|
})
|
|
2665
2694
|
|
|
2666
2695
|
return results
|
|
@@ -2830,6 +2859,12 @@ def compute_recommended_filter(noise_results, current_hz, filter_type="gyro", pr
|
|
|
2830
2859
|
before noise starts.
|
|
2831
2860
|
3. Avoid placing the cutoff on or near a noise peak (would amplify it).
|
|
2832
2861
|
4. Clamp to the frame profile's min/max range.
|
|
2862
|
+
5. CHECK PHASE LAG: if the recommended cutoff would add excessive phase lag
|
|
2863
|
+
compared to current, don't recommend lowering. Large quads (10"+) are
|
|
2864
|
+
especially sensitive to phase lag — lowering LPF from 65 to 30Hz can
|
|
2865
|
+
make oscillations worse even though noise numbers improve.
|
|
2866
|
+
6. If noise is well above the current LPF (all peaks >2x current cutoff),
|
|
2867
|
+
the LPF can't meaningfully help — recommend notch/RPM filters instead.
|
|
2833
2868
|
|
|
2834
2869
|
Returns the recommended Hz value (int, rounded to 5), or None.
|
|
2835
2870
|
"""
|
|
@@ -2881,7 +2916,7 @@ def compute_recommended_filter(noise_results, current_hz, filter_type="gyro", pr
|
|
|
2881
2916
|
# Apply safety margin: place cutoff below the crossover
|
|
2882
2917
|
ideal = worst_crossover * safety
|
|
2883
2918
|
|
|
2884
|
-
# Avoid placing cutoff near a noise peak (within
|
|
2919
|
+
# Avoid placing cutoff near a noise peak (within +/-15Hz of any strong peak)
|
|
2885
2920
|
all_peaks = []
|
|
2886
2921
|
for nr in valid:
|
|
2887
2922
|
for p in nr["peaks"]:
|
|
@@ -2893,12 +2928,83 @@ def compute_recommended_filter(noise_results, current_hz, filter_type="gyro", pr
|
|
|
2893
2928
|
if abs(ideal - peak_freq) < 15:
|
|
2894
2929
|
ideal = min(ideal, peak_freq - 20)
|
|
2895
2930
|
|
|
2931
|
+
# ── Phase lag guard ──
|
|
2932
|
+
# If lowering the filter would significantly increase phase lag, don't.
|
|
2933
|
+
# This is critical for large frames where phase lag causes more problems
|
|
2934
|
+
# than the noise the filter would remove.
|
|
2935
|
+
if current_hz is not None and ideal < current_hz:
|
|
2936
|
+
signal_freq = profile.get("phase_lag_freq", 50.0) # typical control freq
|
|
2937
|
+
|
|
2938
|
+
current_lag = estimate_filter_phase_lag(current_hz, signal_freq)
|
|
2939
|
+
proposed_lag = estimate_filter_phase_lag(ideal, signal_freq)
|
|
2940
|
+
|
|
2941
|
+
if current_lag and proposed_lag:
|
|
2942
|
+
added_lag_ms = proposed_lag["ms"] - current_lag["ms"]
|
|
2943
|
+
added_lag_deg = proposed_lag["degrees"] - current_lag["degrees"]
|
|
2944
|
+
|
|
2945
|
+
# For 10"+ frames, even 5ms extra lag is significant
|
|
2946
|
+
frame_size = profile.get("frame_inches", 5)
|
|
2947
|
+
max_added_lag_ms = 8.0 if frame_size <= 7 else 5.0 if frame_size <= 10 else 3.0
|
|
2948
|
+
|
|
2949
|
+
if added_lag_ms > max_added_lag_ms:
|
|
2950
|
+
# Lowering would add too much phase lag — don't recommend
|
|
2951
|
+
return None
|
|
2952
|
+
|
|
2953
|
+
# ── Relative change guard ──
|
|
2954
|
+
# On large frames, aggressive LPF cuts cause more harm than good.
|
|
2955
|
+
# The added phase lag and signal attenuation destabilize the PID loop.
|
|
2956
|
+
# Don't recommend cutting the LPF by more than 30% on 10"+.
|
|
2957
|
+
if current_hz is not None and ideal < current_hz:
|
|
2958
|
+
frame_size = profile.get("frame_inches", 5)
|
|
2959
|
+
if frame_size >= 10:
|
|
2960
|
+
max_reduction = 0.30 # 30% max cut for 10"+
|
|
2961
|
+
elif frame_size >= 7:
|
|
2962
|
+
max_reduction = 0.40 # 40% for 7"
|
|
2963
|
+
else:
|
|
2964
|
+
max_reduction = 0.50 # 50% for 5"
|
|
2965
|
+
|
|
2966
|
+
min_allowed = current_hz * (1.0 - max_reduction)
|
|
2967
|
+
if ideal < min_allowed:
|
|
2968
|
+
ideal = min_allowed # Cap the reduction
|
|
2969
|
+
|
|
2970
|
+
# ── Propwash guard ──
|
|
2971
|
+
# If the lowest significant noise is below ~80Hz, it's likely propwash
|
|
2972
|
+
# (aerodynamic turbulence). LPF changes don't effectively address propwash
|
|
2973
|
+
# because it's broadband — you'd have to crush the filter so low that
|
|
2974
|
+
# the phase lag makes everything worse. Propwash is fixed through D-term
|
|
2975
|
+
# tuning and flight technique, not gyro LPF.
|
|
2976
|
+
if current_hz is not None and all_peaks and filter_type == "gyro":
|
|
2977
|
+
lowest_peak = min(all_peaks)
|
|
2978
|
+
frame_size = profile.get("frame_inches", 5)
|
|
2979
|
+
# For large frames, propwash typically sits at 30-80Hz
|
|
2980
|
+
propwash_ceiling = 80 if frame_size >= 10 else 100
|
|
2981
|
+
if lowest_peak <= propwash_ceiling and ideal < current_hz:
|
|
2982
|
+
# The noise driving the recommendation is in the propwash band.
|
|
2983
|
+
# Don't lower the LPF — it won't help and will add lag.
|
|
2984
|
+
# Only recommend if there are also peaks well above propwash.
|
|
2985
|
+
peaks_above_propwash = [p for p in all_peaks if p > propwash_ceiling * 1.5]
|
|
2986
|
+
if not peaks_above_propwash:
|
|
2987
|
+
return None
|
|
2988
|
+
# There are higher peaks too — but don't let propwash drive the cutoff down
|
|
2989
|
+
ideal = max(ideal, current_hz * 0.85) # at most 15% cut
|
|
2990
|
+
|
|
2991
|
+
# ── High-frequency noise guard ──
|
|
2992
|
+
# If all significant noise is well above the current LPF (>2x cutoff),
|
|
2993
|
+
# lowering the LPF won't meaningfully help — the noise needs notch/RPM
|
|
2994
|
+
# filters, not a lower LPF that would crush phase margin.
|
|
2995
|
+
if current_hz is not None and all_peaks:
|
|
2996
|
+
lowest_significant_peak = min(all_peaks)
|
|
2997
|
+
if lowest_significant_peak > current_hz * 2.0:
|
|
2998
|
+
# All noise is far above current LPF — LPF change won't help
|
|
2999
|
+
return None
|
|
3000
|
+
|
|
2896
3001
|
# Clamp and round
|
|
2897
3002
|
result = round(int(np.clip(ideal, min_cutoff, max_cutoff)) / 5) * 5
|
|
2898
3003
|
return result
|
|
2899
3004
|
|
|
2900
3005
|
|
|
2901
|
-
def compute_recommended_pid(pid_result, current_p, current_i, current_d, profile=None
|
|
3006
|
+
def compute_recommended_pid(pid_result, current_p, current_i, current_d, profile=None,
|
|
3007
|
+
current_ff=None):
|
|
2902
3008
|
if pid_result is None:
|
|
2903
3009
|
return None
|
|
2904
3010
|
if profile is None:
|
|
@@ -2918,6 +3024,26 @@ def compute_recommended_pid(pid_result, current_p, current_i, current_d, profile
|
|
|
2918
3024
|
dl_high = delay > ok_dl
|
|
2919
3025
|
dl_bad = delay > bad_dl
|
|
2920
3026
|
|
|
3027
|
+
# ── FF-aware overshoot attribution ──
|
|
3028
|
+
# FF drives initial response proportional to stick speed. High FF + high P
|
|
3029
|
+
# = double overshoot source. When FF is significant (>40), attribute some
|
|
3030
|
+
# overshoot to FF and reduce it before cutting P aggressively.
|
|
3031
|
+
ff_is_high = current_ff is not None and current_ff > 40
|
|
3032
|
+
ff_contributes_overshoot = ff_is_high and os_high
|
|
3033
|
+
|
|
3034
|
+
if ff_contributes_overshoot:
|
|
3035
|
+
# Split the blame: FF handles stick-driven overshoot, P handles error-driven.
|
|
3036
|
+
# With high FF, reduce FF first, smaller P cut.
|
|
3037
|
+
ff_severity = current_ff / 60.0 # 1.0 at FF=60, higher = more blame on FF
|
|
3038
|
+
ff_share = min(0.6, ff_severity * 0.3) # FF takes up to 60% of the blame
|
|
3039
|
+
|
|
3040
|
+
new_ff = max(15, int(current_ff * (1 - min(0.25, ff_share * 0.4))))
|
|
3041
|
+
if new_ff != current_ff:
|
|
3042
|
+
changes["FF"] = {"current": current_ff, "new": new_ff}
|
|
3043
|
+
reasons.append(
|
|
3044
|
+
f"FeedForward is {current_ff} - contributes to overshoot on stick inputs. "
|
|
3045
|
+
f"Reducing FF from {current_ff} to {new_ff} before cutting P.")
|
|
3046
|
+
|
|
2921
3047
|
# ── Severity-proportional P adjustment ──
|
|
2922
3048
|
# How far off from target determines size of correction.
|
|
2923
3049
|
# Profile's max_change caps the adjustment for safety.
|
|
@@ -2925,7 +3051,7 @@ def compute_recommended_pid(pid_result, current_p, current_i, current_d, profile
|
|
|
2925
3051
|
new_p = current_p
|
|
2926
3052
|
|
|
2927
3053
|
if os_high:
|
|
2928
|
-
# severity: 1.0 = at ok threshold, 2.0 =
|
|
3054
|
+
# severity: 1.0 = at ok threshold, 2.0 = 2x the ok threshold, etc.
|
|
2929
3055
|
severity = os_pct / ok_os
|
|
2930
3056
|
if severity > 3.0:
|
|
2931
3057
|
raw_cut = 0.35
|
|
@@ -2936,6 +3062,11 @@ def compute_recommended_pid(pid_result, current_p, current_i, current_d, profile
|
|
|
2936
3062
|
else:
|
|
2937
3063
|
raw_cut = 0.10
|
|
2938
3064
|
|
|
3065
|
+
# If FF is taking some blame, reduce the P cut proportionally
|
|
3066
|
+
if ff_contributes_overshoot:
|
|
3067
|
+
ff_share_actual = min(0.6, (current_ff / 60.0) * 0.3)
|
|
3068
|
+
raw_cut *= (1.0 - ff_share_actual)
|
|
3069
|
+
|
|
2939
3070
|
has_d = current_d is not None and current_d > 0
|
|
2940
3071
|
d_hint = " and raise D" if has_d else ""
|
|
2941
3072
|
if os_high and dl_high:
|
|
@@ -4028,6 +4159,8 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4028
4159
|
ok_noise = profile["ok_noise_db"]
|
|
4029
4160
|
bad_noise = profile["bad_noise_db"]
|
|
4030
4161
|
|
|
4162
|
+
info_items = []
|
|
4163
|
+
|
|
4031
4164
|
# ── Hover oscillation (highest priority - can't tune if quad won't hold still) ──
|
|
4032
4165
|
if hover_osc:
|
|
4033
4166
|
for osc in hover_osc:
|
|
@@ -4047,7 +4180,16 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4047
4180
|
freq_str = f" at ~{freq:.0f}Hz" if freq else ""
|
|
4048
4181
|
amp_str = f"gyro RMS {rms:.1f}°/s, peak-to-peak {p2p:.0f}°/s{freq_str}"
|
|
4049
4182
|
|
|
4050
|
-
if cause == "
|
|
4183
|
+
if cause == "wind_buffeting":
|
|
4184
|
+
# Low-frequency broadband wobble on large frame — likely wind, not P
|
|
4185
|
+
# Don't recommend aggressive P cuts — show as informational
|
|
4186
|
+
info_items.append({
|
|
4187
|
+
"text": f"{axis}: Low-frequency gyro activity ({rms:.1f}°/s RMS at ~{freq:.0f}Hz)",
|
|
4188
|
+
"detail": f"On a {profile.get('frame_inches', '?')}-inch frame, {freq:.0f}Hz wobble is "
|
|
4189
|
+
f"typically wind buffeting or GPS position corrections, not P oscillation. "
|
|
4190
|
+
f"If this happens indoors or in calm air, then P may be too high."})
|
|
4191
|
+
|
|
4192
|
+
elif cause == "P_too_high":
|
|
4051
4193
|
current_p = config.get(f"{axis_l}_p")
|
|
4052
4194
|
if current_p:
|
|
4053
4195
|
new_p = max(10, int(current_p * 0.70)) # Aggressive 30% cut
|
|
@@ -4143,7 +4285,6 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4143
4285
|
f"Consider raising lowpass cutoffs or switching BIQUAD filters to PT1."})
|
|
4144
4286
|
|
|
4145
4287
|
# ═══ 0b. MOTOR RESPONSE INFO (not an action - informational only) ═══
|
|
4146
|
-
info_items = []
|
|
4147
4288
|
if motor_response and motor_response["motor_response_ms"] > profile["ok_delay_ms"]:
|
|
4148
4289
|
mr_ms = motor_response["motor_response_ms"]
|
|
4149
4290
|
info_items.append({
|
|
@@ -4251,9 +4392,10 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4251
4392
|
cur_p = config.get(f"{axis.lower()}_p")
|
|
4252
4393
|
cur_i = config.get(f"{axis.lower()}_i")
|
|
4253
4394
|
cur_d = config.get(f"{axis.lower()}_d")
|
|
4395
|
+
cur_ff = config.get(f"{axis.lower()}_ff")
|
|
4254
4396
|
|
|
4255
4397
|
if has_config:
|
|
4256
|
-
rec = compute_recommended_pid(pid, cur_p, cur_i, cur_d, profile)
|
|
4398
|
+
rec = compute_recommended_pid(pid, cur_p, cur_i, cur_d, profile, current_ff=cur_ff)
|
|
4257
4399
|
if rec and rec["changes"]:
|
|
4258
4400
|
_os = pid["avg_overshoot_pct"] or 0
|
|
4259
4401
|
_dl = pid["tracking_delay_ms"] or 0
|
|
@@ -4263,7 +4405,7 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4263
4405
|
# Build a single merged description for all PID changes on this axis
|
|
4264
4406
|
change_parts = []
|
|
4265
4407
|
sub_actions = [] # for CLI generation
|
|
4266
|
-
for term in ["P", "D", "I"]:
|
|
4408
|
+
for term in ["FF", "P", "D", "I"]:
|
|
4267
4409
|
if term in rec["changes"]:
|
|
4268
4410
|
ch = rec["changes"][term]
|
|
4269
4411
|
direction = "Reduce" if ch["new"] < ch["current"] else "Increase"
|
|
@@ -4418,6 +4560,14 @@ def generate_action_plan(noise_results, pid_results, motor_analysis, dterm_resul
|
|
|
4418
4560
|
for osc in hover_osc:
|
|
4419
4561
|
rms = osc["gyro_rms"]
|
|
4420
4562
|
sev = osc["severity"]
|
|
4563
|
+
cause = osc.get("cause")
|
|
4564
|
+
|
|
4565
|
+
# Wind buffeting gets a much softer penalty — it's environmental,
|
|
4566
|
+
# not a tuning problem. Don't tank the score for flying in wind.
|
|
4567
|
+
if cause == "wind_buffeting":
|
|
4568
|
+
osc_scores.append(max(60, 100 - rms * 2)) # Floor at 60
|
|
4569
|
+
continue
|
|
4570
|
+
|
|
4421
4571
|
if sev == "none":
|
|
4422
4572
|
osc_scores.append(100)
|
|
4423
4573
|
elif sev == "mild":
|
|
@@ -4611,9 +4761,9 @@ def create_dterm_chart(dterm_results):
|
|
|
4611
4761
|
|
|
4612
4762
|
# Map from action plan param names to INAV CLI setting names
|
|
4613
4763
|
INAV_CLI_MAP = {
|
|
4614
|
-
"roll_p": "mc_p_roll", "roll_i": "mc_i_roll", "roll_d": "mc_d_roll",
|
|
4615
|
-
"pitch_p": "mc_p_pitch", "pitch_i": "mc_i_pitch", "pitch_d": "mc_d_pitch",
|
|
4616
|
-
"yaw_p": "mc_p_yaw", "yaw_i": "mc_i_yaw", "yaw_d": "mc_d_yaw",
|
|
4764
|
+
"roll_p": "mc_p_roll", "roll_i": "mc_i_roll", "roll_d": "mc_d_roll", "roll_ff": "mc_cd_roll",
|
|
4765
|
+
"pitch_p": "mc_p_pitch", "pitch_i": "mc_i_pitch", "pitch_d": "mc_d_pitch", "pitch_ff": "mc_cd_pitch",
|
|
4766
|
+
"yaw_p": "mc_p_yaw", "yaw_i": "mc_i_yaw", "yaw_d": "mc_d_yaw", "yaw_ff": "mc_cd_yaw",
|
|
4617
4767
|
"gyro_lowpass_hz": "gyro_main_lpf_hz", "gyro_lpf_hz": "gyro_main_lpf_hz",
|
|
4618
4768
|
"gyro_lowpass2_hz": "gyro_main_lpf2_hz",
|
|
4619
4769
|
"dterm_lpf_hz": "dterm_lpf_hz", "dterm_lpf2_hz": "dterm_lpf2_hz",
|
|
@@ -4626,12 +4776,15 @@ INAV_GUI_MAP = {
|
|
|
4626
4776
|
"roll_p": ("PID Tuning", "Roll → P"),
|
|
4627
4777
|
"roll_i": ("PID Tuning", "Roll → I"),
|
|
4628
4778
|
"roll_d": ("PID Tuning", "Roll → D"),
|
|
4779
|
+
"roll_ff": ("PID Tuning", "Roll → FF"),
|
|
4629
4780
|
"pitch_p": ("PID Tuning", "Pitch → P"),
|
|
4630
4781
|
"pitch_i": ("PID Tuning", "Pitch → I"),
|
|
4631
4782
|
"pitch_d": ("PID Tuning", "Pitch → D"),
|
|
4783
|
+
"pitch_ff": ("PID Tuning", "Pitch → FF"),
|
|
4632
4784
|
"yaw_p": ("PID Tuning", "Yaw → P"),
|
|
4633
4785
|
"yaw_i": ("PID Tuning", "Yaw → I"),
|
|
4634
4786
|
"yaw_d": ("PID Tuning", "Yaw → D"),
|
|
4787
|
+
"yaw_ff": ("PID Tuning", "Yaw → FF"),
|
|
4635
4788
|
"gyro_lowpass_hz": ("Filtering", "Gyro LPF Hz"),
|
|
4636
4789
|
"dterm_lpf_hz": ("Filtering", "D-term LPF Hz"),
|
|
4637
4790
|
}
|
|
@@ -4708,19 +4861,26 @@ def generate_narrative(plan, pid_results, motor_analysis, noise_results, config,
|
|
|
4708
4861
|
|
|
4709
4862
|
# Hover oscillation (most critical - mention first)
|
|
4710
4863
|
hover_osc_data = plan["scores"].get("hover_osc", [])
|
|
4711
|
-
|
|
4864
|
+
# Filter out wind_buffeting — it's environmental, not a tuning problem
|
|
4865
|
+
osc_axes = [o for o in hover_osc_data
|
|
4866
|
+
if o["severity"] in ("moderate", "severe") and o.get("cause") != "wind_buffeting"
|
|
4867
|
+
] if hover_osc_data else []
|
|
4868
|
+
wind_axes = [o for o in hover_osc_data
|
|
4869
|
+
if o["severity"] in ("moderate", "severe") and o.get("cause") == "wind_buffeting"
|
|
4870
|
+
] if hover_osc_data else []
|
|
4871
|
+
|
|
4712
4872
|
if osc_axes:
|
|
4713
4873
|
axis_names = [o["axis"] for o in osc_axes]
|
|
4714
4874
|
worst = max(osc_axes, key=lambda o: o["gyro_rms"])
|
|
4715
4875
|
if worst["severity"] == "severe":
|
|
4716
4876
|
parts.append(
|
|
4717
4877
|
f"CRITICAL: The quad is oscillating during hover on {'/'.join(axis_names)} "
|
|
4718
|
-
f"(worst: {worst['axis']} at {worst['gyro_rms']:.1f}
|
|
4878
|
+
f"(worst: {worst['axis']} at {worst['gyro_rms']:.1f}\u00b0/s RMS). "
|
|
4719
4879
|
f"This must be fixed before any other tuning - the quad is fighting itself just to stay in the air.")
|
|
4720
4880
|
else:
|
|
4721
4881
|
parts.append(
|
|
4722
4882
|
f"The quad shows oscillation during hover on {'/'.join(axis_names)} "
|
|
4723
|
-
f"(worst: {worst['axis']} at {worst['gyro_rms']:.1f}
|
|
4883
|
+
f"(worst: {worst['axis']} at {worst['gyro_rms']:.1f}\u00b0/s RMS). "
|
|
4724
4884
|
f"This should be addressed first as it affects all other measurements.")
|
|
4725
4885
|
if worst.get("dominant_freq_hz"):
|
|
4726
4886
|
freq = worst["dominant_freq_hz"]
|
|
@@ -4733,6 +4893,14 @@ def generate_narrative(plan, pid_results, motor_analysis, noise_results, config,
|
|
|
4733
4893
|
else:
|
|
4734
4894
|
parts.append(f"The oscillation frequency (~{freq:.0f}Hz) suggests noise is leaking through the filters.")
|
|
4735
4895
|
|
|
4896
|
+
if wind_axes and not osc_axes:
|
|
4897
|
+
# Only wind buffeting detected — inform but don't alarm
|
|
4898
|
+
worst_w = max(wind_axes, key=lambda o: o["gyro_rms"])
|
|
4899
|
+
parts.append(
|
|
4900
|
+
f"Some low-frequency gyro activity detected on hover ({worst_w['gyro_rms']:.1f}\u00b0/s RMS at "
|
|
4901
|
+
f"~{worst_w['dominant_freq_hz']:.0f}Hz). On this frame size, this is likely wind buffeting "
|
|
4902
|
+
f"or GPS position corrections rather than a PID problem.")
|
|
4903
|
+
|
|
4736
4904
|
# PID behavior per axis
|
|
4737
4905
|
ok_os = profile["ok_overshoot"]
|
|
4738
4906
|
bad_os = profile["bad_overshoot"]
|
|
@@ -5933,7 +6101,7 @@ def count_blackbox_logs(filepath):
|
|
|
5933
6101
|
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
5934
6102
|
|
|
5935
6103
|
def main():
|
|
5936
|
-
parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.
|
|
6104
|
+
parser = argparse.ArgumentParser(description="INAV Blackbox Analyzer v2.18.0 - Prescriptive Tuning",
|
|
5937
6105
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
5938
6106
|
parser.add_argument("--version", action="version", version=f"inav-analyze {REPORT_VERSION}")
|
|
5939
6107
|
parser.add_argument("logfile", nargs="?", default=None,
|
|
@@ -5973,11 +6141,11 @@ def main():
|
|
|
5973
6141
|
help="Omit the plain-English description of the quad's behavior.")
|
|
5974
6142
|
parser.add_argument("--no-color", action="store_true",
|
|
5975
6143
|
help="Disable colored terminal output.")
|
|
5976
|
-
parser.add_argument("--
|
|
5977
|
-
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. "
|
|
5978
6146
|
"Adds config cross-referencing to nav and tuning results. "
|
|
5979
6147
|
"Not needed with --device (config is pulled automatically). "
|
|
5980
|
-
"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.")
|
|
5981
6149
|
parser.add_argument("--nav", action="store_true",
|
|
5982
6150
|
help="Enable navigation health analysis (compass, GPS, baro, "
|
|
5983
6151
|
"estimator). Works on any flight with nav fields in the log.")
|
|
@@ -6019,7 +6187,7 @@ def main():
|
|
|
6019
6187
|
|
|
6020
6188
|
# ── Device mode: download blackbox from FC ──
|
|
6021
6189
|
logfile = args.logfile
|
|
6022
|
-
|
|
6190
|
+
config_raw = None
|
|
6023
6191
|
if args.device:
|
|
6024
6192
|
try:
|
|
6025
6193
|
try:
|
|
@@ -6089,19 +6257,18 @@ def main():
|
|
|
6089
6257
|
pct = summary['used_size'] * 100 // summary['total_size'] if summary['total_size'] > 0 else 0
|
|
6090
6258
|
print(f" Dataflash: {used_kb:.0f}KB / {total_kb:.0f}KB ({pct}% used)")
|
|
6091
6259
|
|
|
6092
|
-
# Pull
|
|
6093
|
-
|
|
6094
|
-
print(" Pulling configuration (
|
|
6095
|
-
|
|
6096
|
-
if
|
|
6097
|
-
n_settings = len([l for l in
|
|
6098
|
-
print(f" {n_settings}
|
|
6099
|
-
|
|
6100
|
-
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")
|
|
6101
6268
|
os.makedirs(args.blackbox_dir, exist_ok=True)
|
|
6102
|
-
with open(
|
|
6103
|
-
f.write(
|
|
6104
|
-
print(f" Saved: {
|
|
6269
|
+
with open(config_path, "w") as f:
|
|
6270
|
+
f.write(config_raw)
|
|
6271
|
+
print(f" Saved: {config_path}")
|
|
6105
6272
|
else:
|
|
6106
6273
|
print(" no response (FC may not support CLI over MSP)")
|
|
6107
6274
|
|
|
@@ -6143,39 +6310,42 @@ def main():
|
|
|
6143
6310
|
print(f"ERROR: File not found: {logfile}"); sys.exit(1)
|
|
6144
6311
|
|
|
6145
6312
|
# ── Load diff from file (when not using --device) ──
|
|
6146
|
-
if
|
|
6147
|
-
|
|
6148
|
-
if args.
|
|
6149
|
-
# Explicit file path: --
|
|
6150
|
-
|
|
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
|
|
6151
6318
|
else:
|
|
6152
|
-
# Auto-discover
|
|
6319
|
+
# Auto-discover config files in same directory as BBL
|
|
6320
|
+
# Prefer dump (complete) over diff (only changed params)
|
|
6153
6321
|
log_dir = os.path.dirname(os.path.abspath(logfile))
|
|
6154
6322
|
candidates = []
|
|
6155
6323
|
for fname in os.listdir(log_dir):
|
|
6156
|
-
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")):
|
|
6157
6326
|
candidates.append(os.path.join(log_dir, fname))
|
|
6158
|
-
if
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
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):
|
|
6166
6336
|
try:
|
|
6167
|
-
with open(
|
|
6168
|
-
|
|
6169
|
-
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()
|
|
6170
6340
|
if l.strip().startswith("set ")])
|
|
6171
6341
|
if n_settings > 0:
|
|
6172
|
-
print(f" Config: {os.path.basename(
|
|
6342
|
+
print(f" Config: {os.path.basename(config_file)} ({n_settings} settings)")
|
|
6173
6343
|
else:
|
|
6174
|
-
|
|
6344
|
+
config_raw = None # not a valid config file
|
|
6175
6345
|
except Exception:
|
|
6176
|
-
|
|
6346
|
+
config_raw = None
|
|
6177
6347
|
else:
|
|
6178
|
-
print(f" Warning:
|
|
6348
|
+
print(f" Warning: Config file not found: {config_file}")
|
|
6179
6349
|
|
|
6180
6350
|
# ── History mode: show progression and exit ──
|
|
6181
6351
|
if args.history or args.trend:
|
|
@@ -6206,14 +6376,14 @@ def main():
|
|
|
6206
6376
|
if not os.path.isfile(args.compare):
|
|
6207
6377
|
print(f"ERROR: Comparison file not found: {args.compare}")
|
|
6208
6378
|
sys.exit(1)
|
|
6209
|
-
_run_comparison(logfile, args.compare, args,
|
|
6379
|
+
_run_comparison(logfile, args.compare, args, config_raw)
|
|
6210
6380
|
return
|
|
6211
6381
|
|
|
6212
6382
|
# ── Replay mode: interactive HTML time-series ──
|
|
6213
6383
|
if args.replay:
|
|
6214
6384
|
if not logfile:
|
|
6215
6385
|
parser.error("logfile required for --replay")
|
|
6216
|
-
_run_replay(logfile, args,
|
|
6386
|
+
_run_replay(logfile, args, config_raw)
|
|
6217
6387
|
return
|
|
6218
6388
|
|
|
6219
6389
|
# ── Log quality check mode ──
|
|
@@ -6252,15 +6422,15 @@ def main():
|
|
|
6252
6422
|
target = log_files[-1]
|
|
6253
6423
|
if len(log_files) > 1:
|
|
6254
6424
|
print(f"\n Nav mode: analyzing last flight ({len(log_files)} in flash)")
|
|
6255
|
-
_analyze_single_log(target, args,
|
|
6425
|
+
_analyze_single_log(target, args, config_raw)
|
|
6256
6426
|
elif len(log_files) > 1:
|
|
6257
|
-
_process_multi_log(log_files, args,
|
|
6427
|
+
_process_multi_log(log_files, args, config_raw)
|
|
6258
6428
|
else:
|
|
6259
6429
|
# Single flight - full analysis
|
|
6260
|
-
_analyze_single_log(log_files[0], args,
|
|
6430
|
+
_analyze_single_log(log_files[0], args, config_raw)
|
|
6261
6431
|
|
|
6262
6432
|
|
|
6263
|
-
def _process_multi_log(log_files, args,
|
|
6433
|
+
def _process_multi_log(log_files, args, config_raw):
|
|
6264
6434
|
"""Handle multiple flights from a single flash download.
|
|
6265
6435
|
|
|
6266
6436
|
Strategy:
|
|
@@ -6276,7 +6446,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6276
6446
|
print(f" Scanning {len(log_files)} flights...")
|
|
6277
6447
|
summaries = []
|
|
6278
6448
|
for lf in log_files:
|
|
6279
|
-
s = _analyze_single_log(lf, args,
|
|
6449
|
+
s = _analyze_single_log(lf, args, config_raw, summary_only=True)
|
|
6280
6450
|
if s:
|
|
6281
6451
|
summaries.append(s)
|
|
6282
6452
|
|
|
@@ -6289,7 +6459,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6289
6459
|
# header config matches the diff are "current session" (post-change),
|
|
6290
6460
|
# flights that don't match are "old session" (pre-change).
|
|
6291
6461
|
# Without a diff, fall back to comparing consecutive flights.
|
|
6292
|
-
current_fp = _fingerprint_from_diff(
|
|
6462
|
+
current_fp = _fingerprint_from_diff(config_raw)
|
|
6293
6463
|
is_current = [] # True/False per flight
|
|
6294
6464
|
|
|
6295
6465
|
if current_fp:
|
|
@@ -6413,7 +6583,7 @@ def _process_multi_log(log_files, args, diff_raw):
|
|
|
6413
6583
|
|
|
6414
6584
|
# ── Phase 5: Full analysis on the selected flight ──
|
|
6415
6585
|
print(f"{'═' * 70}")
|
|
6416
|
-
_analyze_single_log(log_files[best_idx], args,
|
|
6586
|
+
_analyze_single_log(log_files[best_idx], args, config_raw)
|
|
6417
6587
|
|
|
6418
6588
|
# ── Phase 6: Show cross-session progression ──
|
|
6419
6589
|
if not args.no_db:
|
|
@@ -6467,7 +6637,7 @@ def _config_fingerprint(config):
|
|
|
6467
6637
|
return "|".join(parts) if parts else ""
|
|
6468
6638
|
|
|
6469
6639
|
|
|
6470
|
-
def _fingerprint_from_diff(
|
|
6640
|
+
def _fingerprint_from_diff(config_raw):
|
|
6471
6641
|
"""Build a config fingerprint from CLI 'diff all' output.
|
|
6472
6642
|
|
|
6473
6643
|
This represents the FC's CURRENT config - the ground truth.
|
|
@@ -6475,14 +6645,14 @@ def _fingerprint_from_diff(diff_raw):
|
|
|
6475
6645
|
session; flights that don't match are from before the user applied
|
|
6476
6646
|
changes.
|
|
6477
6647
|
"""
|
|
6478
|
-
if not
|
|
6648
|
+
if not config_raw:
|
|
6479
6649
|
return ""
|
|
6480
6650
|
|
|
6481
6651
|
try:
|
|
6482
6652
|
from inav_toolkit.flight_db import parse_diff_output
|
|
6483
6653
|
except ImportError:
|
|
6484
6654
|
from inav_flight_db import parse_diff_output
|
|
6485
|
-
diff_settings = parse_diff_output(
|
|
6655
|
+
diff_settings = parse_diff_output(config_raw)
|
|
6486
6656
|
|
|
6487
6657
|
# Map CLI names → config keys (same mapping as merge_diff_into_config)
|
|
6488
6658
|
config = {}
|
|
@@ -6498,8 +6668,12 @@ def _fingerprint_from_diff(diff_raw):
|
|
|
6498
6668
|
return _config_fingerprint(config)
|
|
6499
6669
|
|
|
6500
6670
|
|
|
6501
|
-
def _print_config_review(
|
|
6502
|
-
"""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.
|
|
6503
6677
|
|
|
6504
6678
|
Only shows CRITICAL and WARNING findings from categories that the blackbox
|
|
6505
6679
|
analyzer doesn't cover (safety, nav, motor protocol, GPS, battery, RX).
|
|
@@ -6508,6 +6682,9 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
|
|
|
6508
6682
|
"""
|
|
6509
6683
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
6510
6684
|
|
|
6685
|
+
if not config_raw:
|
|
6686
|
+
return
|
|
6687
|
+
|
|
6511
6688
|
try:
|
|
6512
6689
|
try:
|
|
6513
6690
|
from inav_toolkit.param_analyzer import parse_diff_all, run_all_checks, CRITICAL, WARNING
|
|
@@ -6517,7 +6694,7 @@ def _print_config_review(diff_raw, config, frame_inches, plan):
|
|
|
6517
6694
|
return # param analyzer not available
|
|
6518
6695
|
|
|
6519
6696
|
try:
|
|
6520
|
-
parsed = parse_diff_all(
|
|
6697
|
+
parsed = parse_diff_all(config_raw)
|
|
6521
6698
|
except Exception:
|
|
6522
6699
|
return
|
|
6523
6700
|
|
|
@@ -6788,15 +6965,15 @@ footer {{ text-align:center; color:#555; margin-top:30px; padding:10px; border-t
|
|
|
6788
6965
|
|
|
6789
6966
|
# ─── Comparison Mode ─────────────────────────────────────────────────────────
|
|
6790
6967
|
|
|
6791
|
-
def _analyze_for_compare(logfile, args,
|
|
6968
|
+
def _analyze_for_compare(logfile, args, config_raw=None):
|
|
6792
6969
|
"""Run analysis pipeline on a single file and return structured results.
|
|
6793
6970
|
Returns dict with: config, data, noise_results, pid_results, motor_analysis,
|
|
6794
6971
|
dterm_results, plan, noise_fp, hover_osc, profile
|
|
6795
6972
|
"""
|
|
6796
6973
|
raw_params = parse_headers_from_bbl(logfile)
|
|
6797
6974
|
config = extract_fc_config(raw_params)
|
|
6798
|
-
if
|
|
6799
|
-
merge_diff_into_config(config,
|
|
6975
|
+
if config_raw:
|
|
6976
|
+
merge_diff_into_config(config, config_raw)
|
|
6800
6977
|
|
|
6801
6978
|
# Auto-detect frame
|
|
6802
6979
|
craft = config.get("craft_name", "")
|
|
@@ -6824,7 +7001,7 @@ def _analyze_for_compare(logfile, args, diff_raw=None):
|
|
|
6824
7001
|
sr = data["sample_rate"]
|
|
6825
7002
|
|
|
6826
7003
|
# Run all analyses
|
|
6827
|
-
hover_osc = detect_hover_oscillation(data, sr)
|
|
7004
|
+
hover_osc = detect_hover_oscillation(data, sr, profile)
|
|
6828
7005
|
noise_results = [analyze_noise(data, ax, f"gyro_{ax.lower()}", sr) for ax in AXIS_NAMES]
|
|
6829
7006
|
pid_results = [analyze_pid_response(data, i, sr) for i in range(3)]
|
|
6830
7007
|
motor_analysis = analyze_motors(data, sr, config)
|
|
@@ -7081,7 +7258,7 @@ footer{{text-align:center;color:var(--dm);font-size:.7rem;padding:24px 0;border-
|
|
|
7081
7258
|
</div><footer>INAV Blackbox Analyzer v{REPORT_VERSION} - Comparison Report</footer></body></html>"""
|
|
7082
7259
|
|
|
7083
7260
|
|
|
7084
|
-
def _run_comparison(file_a, file_b, args,
|
|
7261
|
+
def _run_comparison(file_a, file_b, args, config_raw):
|
|
7085
7262
|
"""Run comparative analysis on two flight logs."""
|
|
7086
7263
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
7087
7264
|
|
|
@@ -7091,12 +7268,12 @@ def _run_comparison(file_a, file_b, args, diff_raw):
|
|
|
7091
7268
|
print()
|
|
7092
7269
|
|
|
7093
7270
|
print(f" Analyzing flight A...", end=" ", flush=True)
|
|
7094
|
-
res_a = _analyze_for_compare(file_a, args,
|
|
7271
|
+
res_a = _analyze_for_compare(file_a, args, config_raw)
|
|
7095
7272
|
sa = res_a["plan"]["scores"]
|
|
7096
7273
|
print(f"score {sa['overall']:.0f}/100")
|
|
7097
7274
|
|
|
7098
7275
|
print(f" Analyzing flight B...", end=" ", flush=True)
|
|
7099
|
-
res_b = _analyze_for_compare(file_b, args,
|
|
7276
|
+
res_b = _analyze_for_compare(file_b, args, config_raw)
|
|
7100
7277
|
sb = res_b["plan"]["scores"]
|
|
7101
7278
|
print(f"score {sb['overall']:.0f}/100")
|
|
7102
7279
|
|
|
@@ -7606,7 +7783,7 @@ allPlotIds.forEach(srcId => {{
|
|
|
7606
7783
|
</script></body></html>"""
|
|
7607
7784
|
|
|
7608
7785
|
|
|
7609
|
-
def _run_replay(logfile, args,
|
|
7786
|
+
def _run_replay(logfile, args, config_raw):
|
|
7610
7787
|
"""Generate interactive replay HTML for a single flight."""
|
|
7611
7788
|
R, B, C, G, Y, RED, DIM = _colors()
|
|
7612
7789
|
|
|
@@ -7615,8 +7792,8 @@ def _run_replay(logfile, args, diff_raw):
|
|
|
7615
7792
|
|
|
7616
7793
|
raw_params = parse_headers_from_bbl(logfile)
|
|
7617
7794
|
config = extract_fc_config(raw_params)
|
|
7618
|
-
if
|
|
7619
|
-
merge_diff_into_config(config,
|
|
7795
|
+
if config_raw:
|
|
7796
|
+
merge_diff_into_config(config, config_raw)
|
|
7620
7797
|
|
|
7621
7798
|
ext = os.path.splitext(logfile)[1].lower()
|
|
7622
7799
|
is_blackbox = ext in (".bbl", ".bfl", ".bbs")
|
|
@@ -7665,13 +7842,13 @@ def _run_replay(logfile, args, diff_raw):
|
|
|
7665
7842
|
print()
|
|
7666
7843
|
|
|
7667
7844
|
|
|
7668
|
-
def _analyze_single_log(logfile, args,
|
|
7845
|
+
def _analyze_single_log(logfile, args, config_raw=None, summary_only=False):
|
|
7669
7846
|
"""Analyze a single blackbox log file.
|
|
7670
7847
|
|
|
7671
7848
|
Args:
|
|
7672
7849
|
logfile: Path to log file
|
|
7673
7850
|
args: Command line arguments
|
|
7674
|
-
|
|
7851
|
+
config_raw: Optional CLI diff text
|
|
7675
7852
|
summary_only: If True, skip verbose output/reports but still analyze
|
|
7676
7853
|
and store in DB. Returns a summary dict.
|
|
7677
7854
|
|
|
@@ -7710,8 +7887,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
7710
7887
|
config = extract_fc_config(raw_params)
|
|
7711
7888
|
|
|
7712
7889
|
# ── Merge CLI diff if available ──
|
|
7713
|
-
if
|
|
7714
|
-
n_merged = merge_diff_into_config(config,
|
|
7890
|
+
if config_raw:
|
|
7891
|
+
n_merged = merge_diff_into_config(config, config_raw)
|
|
7715
7892
|
mismatches = config.get("_diff_mismatches", [])
|
|
7716
7893
|
if n_merged > 0 or mismatches:
|
|
7717
7894
|
parts = [f"{n_merged} new settings from CLI diff"]
|
|
@@ -7979,8 +8156,9 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
7979
8156
|
# ── NAV-ONLY MODE: skip tuning, run navigation health analysis ──
|
|
7980
8157
|
if nav_mode:
|
|
7981
8158
|
# Quick oscillation check - warn if PID tuning is polluting nav readings
|
|
7982
|
-
hover_osc = detect_hover_oscillation(data, sr)
|
|
7983
|
-
osc_severe = [h for h in hover_osc
|
|
8159
|
+
hover_osc = detect_hover_oscillation(data, sr, profile)
|
|
8160
|
+
osc_severe = [h for h in hover_osc
|
|
8161
|
+
if h["severity"] in ("moderate", "severe") and h.get("cause") != "wind_buffeting"]
|
|
7984
8162
|
tune_warning = False
|
|
7985
8163
|
if osc_severe:
|
|
7986
8164
|
tune_warning = True
|
|
@@ -8011,7 +8189,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8011
8189
|
print(" Enable NAV_POS, NAV_ACC, and Attitude in blackbox settings.")
|
|
8012
8190
|
return None
|
|
8013
8191
|
|
|
8014
|
-
hover_osc = detect_hover_oscillation(data, sr)
|
|
8192
|
+
hover_osc = detect_hover_oscillation(data, sr, profile)
|
|
8015
8193
|
noise_results = [analyze_noise(data, ax, f"gyro_{ax.lower()}", sr) for ax in AXIS_NAMES]
|
|
8016
8194
|
noise_fp = fingerprint_noise(noise_results, config, prop_harmonics)
|
|
8017
8195
|
pid_results = [analyze_pid_response(data, i, sr) for i in range(3)]
|
|
@@ -8057,7 +8235,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8057
8235
|
db = FlightDB(args.db_path)
|
|
8058
8236
|
flight_id, is_new = db.store_flight(
|
|
8059
8237
|
plan, config, data, hover_osc, motor_analysis,
|
|
8060
|
-
pid_results, noise_results, log_file=logfile,
|
|
8238
|
+
pid_results, noise_results, log_file=logfile, config_raw=config_raw)
|
|
8061
8239
|
db.close()
|
|
8062
8240
|
summary["flight_id"] = flight_id
|
|
8063
8241
|
summary["is_new"] = is_new
|
|
@@ -8072,8 +8250,8 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8072
8250
|
# ── Config review from diff (if available) ──
|
|
8073
8251
|
# Runs parameter analyzer checks on the FC's current config.
|
|
8074
8252
|
# Catches safety, nav, motor protocol issues that flight data alone can't detect.
|
|
8075
|
-
if
|
|
8076
|
-
_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)
|
|
8077
8255
|
|
|
8078
8256
|
nav_results = None # nav analysis only runs in --nav mode
|
|
8079
8257
|
if not args.no_html and plan["verdict"] != "GROUND_ONLY":
|
|
@@ -8115,7 +8293,7 @@ def _analyze_single_log(logfile, args, diff_raw=None, summary_only=False):
|
|
|
8115
8293
|
db = FlightDB(args.db_path)
|
|
8116
8294
|
flight_id, is_new = db.store_flight(
|
|
8117
8295
|
plan, config, data, hover_osc, motor_analysis,
|
|
8118
|
-
pid_results, noise_results, log_file=logfile,
|
|
8296
|
+
pid_results, noise_results, log_file=logfile, config_raw=config_raw)
|
|
8119
8297
|
craft = config.get("craft_name", "unknown")
|
|
8120
8298
|
n_flights = db.get_flight_count(craft)
|
|
8121
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
|