inav-toolkit 2.15.0__py3-none-any.whl
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/__init__.py +3 -0
- inav_toolkit/autotune.py +504 -0
- inav_toolkit/blackbox_analyzer.py +8017 -0
- inav_toolkit/flight_db.py +467 -0
- inav_toolkit/msp.py +1115 -0
- inav_toolkit/param_analyzer.py +2715 -0
- inav_toolkit/vtol_configurator.py +856 -0
- inav_toolkit/wizard.py +1095 -0
- inav_toolkit-2.15.0.dist-info/METADATA +375 -0
- inav_toolkit-2.15.0.dist-info/RECORD +14 -0
- inav_toolkit-2.15.0.dist-info/WHEEL +5 -0
- inav_toolkit-2.15.0.dist-info/entry_points.txt +5 -0
- inav_toolkit-2.15.0.dist-info/licenses/LICENSE +674 -0
- inav_toolkit-2.15.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""INAV Flight History Database.
|
|
3
|
+
|
|
4
|
+
Stores flight analysis results in SQLite for progression tracking.
|
|
5
|
+
Each analyzed flight is recorded with scores, per-axis measurements,
|
|
6
|
+
motor data, configuration snapshot, and recommended actions.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from inav_toolkit.flight_db import FlightDB
|
|
10
|
+
|
|
11
|
+
db = FlightDB("./inav_flights.db")
|
|
12
|
+
flight_id = db.store_flight(plan, config, data, hover_osc, motor_analysis,
|
|
13
|
+
pid_results, noise_results, diff_raw=None)
|
|
14
|
+
|
|
15
|
+
# Get progression for a craft
|
|
16
|
+
history = db.get_craft_history("NAZGUL 10", limit=10)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sqlite3
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
VERSION = "2.15.0"
|
|
25
|
+
|
|
26
|
+
SCHEMA_VERSION = 1
|
|
27
|
+
|
|
28
|
+
SCHEMA = """
|
|
29
|
+
CREATE TABLE IF NOT EXISTS schema_info (
|
|
30
|
+
key TEXT PRIMARY KEY,
|
|
31
|
+
value TEXT
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS flights (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
timestamp TEXT NOT NULL,
|
|
37
|
+
craft TEXT NOT NULL,
|
|
38
|
+
firmware TEXT,
|
|
39
|
+
board TEXT,
|
|
40
|
+
duration_s REAL,
|
|
41
|
+
sample_rate REAL,
|
|
42
|
+
total_frames INTEGER,
|
|
43
|
+
log_file TEXT,
|
|
44
|
+
-- Scores
|
|
45
|
+
overall_score REAL,
|
|
46
|
+
noise_score REAL,
|
|
47
|
+
pid_score REAL,
|
|
48
|
+
pid_measurable INTEGER,
|
|
49
|
+
motor_score REAL,
|
|
50
|
+
osc_score REAL,
|
|
51
|
+
verdict TEXT,
|
|
52
|
+
verdict_text TEXT,
|
|
53
|
+
-- Raw diff output
|
|
54
|
+
diff_raw TEXT
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS flight_axes (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
flight_id INTEGER NOT NULL,
|
|
60
|
+
axis TEXT NOT NULL,
|
|
61
|
+
-- Hover oscillation
|
|
62
|
+
hover_severity TEXT,
|
|
63
|
+
hover_rms REAL,
|
|
64
|
+
hover_p2p REAL,
|
|
65
|
+
hover_freq_hz REAL,
|
|
66
|
+
hover_cause TEXT,
|
|
67
|
+
hover_seconds REAL,
|
|
68
|
+
-- PID response
|
|
69
|
+
overshoot_pct REAL,
|
|
70
|
+
delay_ms REAL,
|
|
71
|
+
tracking_error REAL,
|
|
72
|
+
n_steps INTEGER,
|
|
73
|
+
-- Config at time of flight
|
|
74
|
+
p_value INTEGER,
|
|
75
|
+
i_value INTEGER,
|
|
76
|
+
d_value INTEGER,
|
|
77
|
+
ff_value INTEGER,
|
|
78
|
+
FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS flight_motors (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
flight_id INTEGER NOT NULL,
|
|
84
|
+
motor_num INTEGER NOT NULL,
|
|
85
|
+
avg_pct REAL,
|
|
86
|
+
saturation_pct REAL,
|
|
87
|
+
FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS flight_config (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
flight_id INTEGER NOT NULL,
|
|
93
|
+
param TEXT NOT NULL,
|
|
94
|
+
value TEXT,
|
|
95
|
+
FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS flight_actions (
|
|
99
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
flight_id INTEGER NOT NULL,
|
|
101
|
+
priority INTEGER,
|
|
102
|
+
urgency TEXT,
|
|
103
|
+
category TEXT,
|
|
104
|
+
action TEXT,
|
|
105
|
+
reason TEXT,
|
|
106
|
+
deferred INTEGER DEFAULT 0,
|
|
107
|
+
FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_flights_craft ON flights(craft);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_flights_timestamp ON flights(timestamp);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_flight_axes_flight ON flight_axes(flight_id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_flight_motors_flight ON flight_motors(flight_id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_flight_config_flight ON flight_config(flight_id);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_flight_actions_flight ON flight_actions(flight_id);
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FlightDB:
|
|
120
|
+
"""SQLite database for storing and querying flight analysis history."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, db_path="./inav_flights.db"):
|
|
123
|
+
self.db_path = db_path
|
|
124
|
+
self._conn = None
|
|
125
|
+
|
|
126
|
+
def _connect(self):
|
|
127
|
+
if self._conn is None:
|
|
128
|
+
self._conn = sqlite3.connect(self.db_path)
|
|
129
|
+
self._conn.row_factory = sqlite3.Row
|
|
130
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
131
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
132
|
+
self._init_schema()
|
|
133
|
+
return self._conn
|
|
134
|
+
|
|
135
|
+
def _init_schema(self):
|
|
136
|
+
conn = self._conn
|
|
137
|
+
conn.executescript(SCHEMA)
|
|
138
|
+
# Check/set schema version
|
|
139
|
+
cur = conn.execute(
|
|
140
|
+
"SELECT value FROM schema_info WHERE key='schema_version'")
|
|
141
|
+
row = cur.fetchone()
|
|
142
|
+
if row is None:
|
|
143
|
+
conn.execute(
|
|
144
|
+
"INSERT INTO schema_info (key, value) VALUES ('schema_version', ?)",
|
|
145
|
+
(str(SCHEMA_VERSION),))
|
|
146
|
+
conn.commit()
|
|
147
|
+
|
|
148
|
+
def close(self):
|
|
149
|
+
if self._conn:
|
|
150
|
+
self._conn.close()
|
|
151
|
+
self._conn = None
|
|
152
|
+
|
|
153
|
+
def store_flight(self, plan, config, data, hover_osc=None,
|
|
154
|
+
motor_analysis=None, pid_results=None,
|
|
155
|
+
noise_results=None, log_file=None, diff_raw=None):
|
|
156
|
+
"""Store a complete flight analysis.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
plan: Analysis plan dict (from generate_action_plan)
|
|
160
|
+
config: Parsed config dict
|
|
161
|
+
data: Parsed flight data dict
|
|
162
|
+
hover_osc: Hover oscillation results list
|
|
163
|
+
motor_analysis: Motor analysis dict
|
|
164
|
+
pid_results: PID analysis results list
|
|
165
|
+
noise_results: Noise analysis results list
|
|
166
|
+
log_file: Path to the log file
|
|
167
|
+
diff_raw: Raw CLI diff output string
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
flight_id (int), or existing flight_id if duplicate detected
|
|
171
|
+
"""
|
|
172
|
+
conn = self._connect()
|
|
173
|
+
scores = plan.get("scores", {})
|
|
174
|
+
|
|
175
|
+
# Extract metadata
|
|
176
|
+
craft = config.get("craft_name", "unknown")
|
|
177
|
+
firmware = config.get("firmware_version", "")
|
|
178
|
+
board = config.get("board", "")
|
|
179
|
+
duration = float(data["time_s"][-1]) if "time_s" in data and len(data["time_s"]) > 0 else 0
|
|
180
|
+
sr = data.get("sample_rate", 0)
|
|
181
|
+
total_frames = len(data.get("time_s", []))
|
|
182
|
+
|
|
183
|
+
# ── Deduplication ──
|
|
184
|
+
# Content-based: match on craft + total_frames + duration + firmware.
|
|
185
|
+
# This catches re-downloads of the same flash (different filenames)
|
|
186
|
+
# as well as re-analysis of the same file.
|
|
187
|
+
existing = conn.execute("""
|
|
188
|
+
SELECT id FROM flights
|
|
189
|
+
WHERE craft = ? AND total_frames = ?
|
|
190
|
+
AND ABS(duration_s - ?) < 0.1
|
|
191
|
+
AND firmware = ?
|
|
192
|
+
""", (craft, total_frames, duration, firmware)).fetchone()
|
|
193
|
+
if existing:
|
|
194
|
+
return existing[0], False # (flight_id, is_new=False)
|
|
195
|
+
|
|
196
|
+
# Insert flight record
|
|
197
|
+
cur = conn.execute("""
|
|
198
|
+
INSERT INTO flights (timestamp, craft, firmware, board, duration_s,
|
|
199
|
+
sample_rate, total_frames, log_file,
|
|
200
|
+
overall_score, noise_score, pid_score, pid_measurable,
|
|
201
|
+
motor_score, osc_score, verdict, verdict_text, diff_raw)
|
|
202
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
203
|
+
""", (
|
|
204
|
+
datetime.now().isoformat(),
|
|
205
|
+
craft, firmware, board, duration, sr, total_frames, log_file,
|
|
206
|
+
scores.get("overall"),
|
|
207
|
+
scores.get("noise"),
|
|
208
|
+
scores.get("pid"),
|
|
209
|
+
1 if scores.get("pid_measurable", False) else 0,
|
|
210
|
+
scores.get("motor"),
|
|
211
|
+
scores.get("gyro_oscillation"),
|
|
212
|
+
plan.get("verdict"),
|
|
213
|
+
plan.get("verdict_text"),
|
|
214
|
+
diff_raw,
|
|
215
|
+
))
|
|
216
|
+
flight_id = cur.lastrowid
|
|
217
|
+
|
|
218
|
+
# Store per-axis data
|
|
219
|
+
axis_names = ["Roll", "Pitch", "Yaw"]
|
|
220
|
+
for i, axis in enumerate(axis_names):
|
|
221
|
+
axis_l = axis.lower()
|
|
222
|
+
# Hover oscillation
|
|
223
|
+
h_sev = h_rms = h_p2p = h_freq = h_cause = h_sec = None
|
|
224
|
+
if hover_osc:
|
|
225
|
+
for osc in hover_osc:
|
|
226
|
+
if osc["axis"] == axis:
|
|
227
|
+
h_sev = osc["severity"]
|
|
228
|
+
h_rms = osc["gyro_rms"]
|
|
229
|
+
h_p2p = osc["gyro_p2p"]
|
|
230
|
+
h_freq = osc.get("dominant_freq_hz")
|
|
231
|
+
h_cause = osc.get("cause")
|
|
232
|
+
h_sec = osc.get("hover_seconds")
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
# PID response
|
|
236
|
+
os_pct = dl_ms = trk_err = n_steps = None
|
|
237
|
+
if pid_results and i < len(pid_results) and pid_results[i]:
|
|
238
|
+
pid = pid_results[i]
|
|
239
|
+
os_pct = pid.get("avg_overshoot_pct")
|
|
240
|
+
dl_ms = pid.get("tracking_delay_ms")
|
|
241
|
+
trk_err = pid.get("rms_error")
|
|
242
|
+
n_steps = pid.get("n_steps")
|
|
243
|
+
|
|
244
|
+
# Config values
|
|
245
|
+
p_val = config.get(f"{axis_l}_p")
|
|
246
|
+
i_val = config.get(f"{axis_l}_i")
|
|
247
|
+
d_val = config.get(f"{axis_l}_d")
|
|
248
|
+
ff_val = config.get(f"{axis_l}_ff")
|
|
249
|
+
|
|
250
|
+
conn.execute("""
|
|
251
|
+
INSERT INTO flight_axes (flight_id, axis,
|
|
252
|
+
hover_severity, hover_rms, hover_p2p, hover_freq_hz,
|
|
253
|
+
hover_cause, hover_seconds,
|
|
254
|
+
overshoot_pct, delay_ms, tracking_error, n_steps,
|
|
255
|
+
p_value, i_value, d_value, ff_value)
|
|
256
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
257
|
+
""", (
|
|
258
|
+
flight_id, axis,
|
|
259
|
+
h_sev, h_rms, h_p2p, h_freq, h_cause, h_sec,
|
|
260
|
+
os_pct, dl_ms, trk_err, n_steps,
|
|
261
|
+
p_val, i_val, d_val, ff_val,
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
# Store motor data
|
|
265
|
+
if motor_analysis and "motors" in motor_analysis:
|
|
266
|
+
for m in motor_analysis["motors"]:
|
|
267
|
+
conn.execute("""
|
|
268
|
+
INSERT INTO flight_motors (flight_id, motor_num, avg_pct, saturation_pct)
|
|
269
|
+
VALUES (?, ?, ?, ?)
|
|
270
|
+
""", (flight_id, m["motor"], m["avg_pct"], m["saturation_pct"]))
|
|
271
|
+
|
|
272
|
+
# Store config snapshot (meaningful params only)
|
|
273
|
+
config_params = [
|
|
274
|
+
"gyro_lpf_hz", "dterm_lpf_hz", "dyn_notch_min_hz", "dyn_notch_q",
|
|
275
|
+
"rpm_filter_enabled", "motor_protocol", "looptime",
|
|
276
|
+
"roll_p", "roll_i", "roll_d", "roll_ff",
|
|
277
|
+
"pitch_p", "pitch_i", "pitch_d", "pitch_ff",
|
|
278
|
+
"yaw_p", "yaw_i", "yaw_d", "yaw_ff",
|
|
279
|
+
"craft_name", "firmware_version", "board",
|
|
280
|
+
]
|
|
281
|
+
for param in config_params:
|
|
282
|
+
val = config.get(param)
|
|
283
|
+
if val is not None:
|
|
284
|
+
conn.execute("""
|
|
285
|
+
INSERT INTO flight_config (flight_id, param, value)
|
|
286
|
+
VALUES (?, ?, ?)
|
|
287
|
+
""", (flight_id, param, str(val)))
|
|
288
|
+
|
|
289
|
+
# Store diff config if available
|
|
290
|
+
if diff_raw:
|
|
291
|
+
diff_config = parse_diff_output(diff_raw)
|
|
292
|
+
for param, val in diff_config.items():
|
|
293
|
+
# Avoid duplicates with blackbox config
|
|
294
|
+
if param not in config_params:
|
|
295
|
+
conn.execute("""
|
|
296
|
+
INSERT INTO flight_config (flight_id, param, value)
|
|
297
|
+
VALUES (?, ?, ?)
|
|
298
|
+
""", (flight_id, param, str(val)))
|
|
299
|
+
|
|
300
|
+
# Store actions
|
|
301
|
+
for a in plan.get("actions", []):
|
|
302
|
+
conn.execute("""
|
|
303
|
+
INSERT INTO flight_actions (flight_id, priority, urgency,
|
|
304
|
+
category, action, reason, deferred)
|
|
305
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
306
|
+
""", (
|
|
307
|
+
flight_id,
|
|
308
|
+
a.get("priority"),
|
|
309
|
+
a.get("urgency"),
|
|
310
|
+
a.get("category"),
|
|
311
|
+
a.get("action"),
|
|
312
|
+
a.get("reason"),
|
|
313
|
+
1 if a.get("deferred") else 0,
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
conn.commit()
|
|
317
|
+
return flight_id, True # (flight_id, is_new=True)
|
|
318
|
+
|
|
319
|
+
def get_craft_history(self, craft, limit=20):
|
|
320
|
+
"""Get flight history for a specific craft, newest first.
|
|
321
|
+
|
|
322
|
+
Returns list of dicts with scores and key metrics.
|
|
323
|
+
"""
|
|
324
|
+
conn = self._connect()
|
|
325
|
+
rows = conn.execute("""
|
|
326
|
+
SELECT f.*,
|
|
327
|
+
GROUP_CONCAT(DISTINCT fa.axis || ':' || COALESCE(fa.hover_severity, 'none'))
|
|
328
|
+
AS osc_summary
|
|
329
|
+
FROM flights f
|
|
330
|
+
LEFT JOIN flight_axes fa ON fa.flight_id = f.id
|
|
331
|
+
WHERE f.craft = ?
|
|
332
|
+
GROUP BY f.id
|
|
333
|
+
ORDER BY f.timestamp DESC
|
|
334
|
+
LIMIT ?
|
|
335
|
+
""", (craft, limit)).fetchall()
|
|
336
|
+
|
|
337
|
+
history = []
|
|
338
|
+
for row in rows:
|
|
339
|
+
flight = dict(row)
|
|
340
|
+
# Get axis details
|
|
341
|
+
axes = conn.execute("""
|
|
342
|
+
SELECT * FROM flight_axes WHERE flight_id = ?
|
|
343
|
+
""", (row["id"],)).fetchall()
|
|
344
|
+
flight["axes"] = [dict(a) for a in axes]
|
|
345
|
+
|
|
346
|
+
# Get motor data
|
|
347
|
+
motors = conn.execute("""
|
|
348
|
+
SELECT * FROM flight_motors WHERE flight_id = ?
|
|
349
|
+
""", (row["id"],)).fetchall()
|
|
350
|
+
flight["motors"] = [dict(m) for m in motors]
|
|
351
|
+
|
|
352
|
+
history.append(flight)
|
|
353
|
+
|
|
354
|
+
return history
|
|
355
|
+
|
|
356
|
+
def get_progression(self, craft, limit=10):
|
|
357
|
+
"""Get a progression summary for a craft.
|
|
358
|
+
|
|
359
|
+
Returns a dict with:
|
|
360
|
+
flights: list of simplified flight records (oldest first)
|
|
361
|
+
trend: "improving", "stable", "degrading", or "insufficient"
|
|
362
|
+
changes: list of notable changes between flights
|
|
363
|
+
|
|
364
|
+
Ground-only flights (armed but never flew) are excluded from
|
|
365
|
+
progression since they have no meaningful scores.
|
|
366
|
+
"""
|
|
367
|
+
history = self.get_craft_history(craft, limit)
|
|
368
|
+
if not history:
|
|
369
|
+
return {"flights": [], "trend": "insufficient", "changes": []}
|
|
370
|
+
|
|
371
|
+
# Reverse to chronological order, filter out ground-only flights
|
|
372
|
+
flights = [f for f in reversed(history)
|
|
373
|
+
if f.get("verdict") != "GROUND_ONLY"
|
|
374
|
+
and f.get("overall_score") is not None]
|
|
375
|
+
|
|
376
|
+
if not flights:
|
|
377
|
+
return {"flights": [], "trend": "insufficient", "changes": []}
|
|
378
|
+
|
|
379
|
+
simplified = []
|
|
380
|
+
for f in flights:
|
|
381
|
+
simplified.append({
|
|
382
|
+
"id": f["id"],
|
|
383
|
+
"timestamp": f["timestamp"],
|
|
384
|
+
"score": f["overall_score"],
|
|
385
|
+
"noise": f["noise_score"],
|
|
386
|
+
"pid": f["pid_score"],
|
|
387
|
+
"motor": f["motor_score"],
|
|
388
|
+
"osc": f["osc_score"],
|
|
389
|
+
"verdict": f["verdict"],
|
|
390
|
+
"duration": f["duration_s"],
|
|
391
|
+
"axes": f["axes"],
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
# Determine trend
|
|
395
|
+
changes = []
|
|
396
|
+
if len(simplified) >= 2:
|
|
397
|
+
prev = simplified[-2]
|
|
398
|
+
curr = simplified[-1]
|
|
399
|
+
delta = (curr["score"] or 0) - (prev["score"] or 0)
|
|
400
|
+
|
|
401
|
+
if delta > 10:
|
|
402
|
+
trend = "improving"
|
|
403
|
+
elif delta < -10:
|
|
404
|
+
trend = "degrading"
|
|
405
|
+
else:
|
|
406
|
+
trend = "stable"
|
|
407
|
+
|
|
408
|
+
# Note specific changes
|
|
409
|
+
if prev["score"] and curr["score"]:
|
|
410
|
+
changes.append(
|
|
411
|
+
f"Score: {prev['score']:.0f} → {curr['score']:.0f} "
|
|
412
|
+
f"({'↑' if delta > 0 else '↓'}{abs(delta):.0f})")
|
|
413
|
+
|
|
414
|
+
# Compare axes
|
|
415
|
+
for i, axis in enumerate(["Roll", "Pitch", "Yaw"]):
|
|
416
|
+
prev_ax = next((a for a in prev["axes"] if a["axis"] == axis), None)
|
|
417
|
+
curr_ax = next((a for a in curr["axes"] if a["axis"] == axis), None)
|
|
418
|
+
if prev_ax and curr_ax:
|
|
419
|
+
if prev_ax["hover_rms"] and curr_ax["hover_rms"]:
|
|
420
|
+
rms_delta = curr_ax["hover_rms"] - prev_ax["hover_rms"]
|
|
421
|
+
if abs(rms_delta) > 2:
|
|
422
|
+
changes.append(
|
|
423
|
+
f"{axis} hover: {prev_ax['hover_rms']:.1f} → "
|
|
424
|
+
f"{curr_ax['hover_rms']:.1f}°/s "
|
|
425
|
+
f"({'↑worse' if rms_delta > 0 else '↓better'})")
|
|
426
|
+
if prev_ax["p_value"] and curr_ax["p_value"]:
|
|
427
|
+
if prev_ax["p_value"] != curr_ax["p_value"]:
|
|
428
|
+
changes.append(
|
|
429
|
+
f"{axis} P: {prev_ax['p_value']} → {curr_ax['p_value']}")
|
|
430
|
+
else:
|
|
431
|
+
trend = "insufficient"
|
|
432
|
+
|
|
433
|
+
return {"flights": simplified, "trend": trend, "changes": changes}
|
|
434
|
+
|
|
435
|
+
def get_flight_count(self, craft=None):
|
|
436
|
+
"""Get total number of stored flights, optionally filtered by craft."""
|
|
437
|
+
conn = self._connect()
|
|
438
|
+
if craft:
|
|
439
|
+
row = conn.execute(
|
|
440
|
+
"SELECT COUNT(*) FROM flights WHERE craft = ?", (craft,)).fetchone()
|
|
441
|
+
else:
|
|
442
|
+
row = conn.execute("SELECT COUNT(*) FROM flights").fetchone()
|
|
443
|
+
return row[0]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def parse_diff_output(diff_text):
|
|
447
|
+
"""Parse INAV CLI 'diff all' output into key-value dict.
|
|
448
|
+
|
|
449
|
+
Handles lines like:
|
|
450
|
+
set gyro_main_lpf_hz = 40
|
|
451
|
+
set mc_p_roll = 32
|
|
452
|
+
# master
|
|
453
|
+
# profile
|
|
454
|
+
"""
|
|
455
|
+
config = {}
|
|
456
|
+
if not diff_text:
|
|
457
|
+
return config
|
|
458
|
+
for line in diff_text.splitlines():
|
|
459
|
+
line = line.strip()
|
|
460
|
+
if line.startswith("set ") and " = " in line:
|
|
461
|
+
# "set param_name = value"
|
|
462
|
+
parts = line[4:].split(" = ", 1)
|
|
463
|
+
if len(parts) == 2:
|
|
464
|
+
param = parts[0].strip()
|
|
465
|
+
value = parts[1].strip()
|
|
466
|
+
config[param] = value
|
|
467
|
+
return config
|