px4-configuration 0.1.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.
- px4_configuration/__init__.py +15 -0
- px4_configuration/api/__init__.py +11 -0
- px4_configuration/api/config.py +22 -0
- px4_configuration/api/main.py +262 -0
- px4_configuration/api/models.py +120 -0
- px4_configuration/api/services/__init__.py +9 -0
- px4_configuration/api/services/calibration_service.py +136 -0
- px4_configuration/api/services/health_service.py +131 -0
- px4_configuration/api/services/param_service.py +150 -0
- px4_configuration/api/services/sdcard_service.py +89 -0
- px4_configuration/api/services/shell_service.py +65 -0
- px4_configuration/api/utils/__init__.py +1 -0
- px4_configuration/api/utils/calibration_parser.py +248 -0
- px4_configuration-0.1.0.dist-info/METADATA +252 -0
- px4_configuration-0.1.0.dist-info/RECORD +17 -0
- px4_configuration-0.1.0.dist-info/WHEEL +4 -0
- px4_configuration-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Service for PX4 health and calibration snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
from mavsdk import System
|
|
9
|
+
|
|
10
|
+
from ..config import settings
|
|
11
|
+
from ..models import Px4HealthResponse, CalibrationHealth
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def _get_health_once(drone: System, timeout_s: float = 5.0):
|
|
15
|
+
"""
|
|
16
|
+
Wait for the first telemetry.health() message.
|
|
17
|
+
|
|
18
|
+
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
19
|
+
"""
|
|
20
|
+
async def _first():
|
|
21
|
+
async for health in drone.telemetry.health():
|
|
22
|
+
return health
|
|
23
|
+
|
|
24
|
+
return await asyncio.wait_for(_first(), timeout_s)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _get_gps_info_once(drone: System, timeout_s: float = 5.0):
|
|
28
|
+
"""
|
|
29
|
+
Wait for the first telemetry.gps_info() message.
|
|
30
|
+
|
|
31
|
+
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
32
|
+
"""
|
|
33
|
+
async def _first():
|
|
34
|
+
async for gps_info in drone.telemetry.gps_info():
|
|
35
|
+
return gps_info
|
|
36
|
+
|
|
37
|
+
return await asyncio.wait_for(_first(), timeout_s)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def read_px4_health(
|
|
41
|
+
port: Optional[str] = None,
|
|
42
|
+
baudrate: Optional[int] = None,
|
|
43
|
+
timeout_s: float = 10.0,
|
|
44
|
+
) -> Px4HealthResponse:
|
|
45
|
+
"""
|
|
46
|
+
Connect to PX4 via MAVSDK and return a one-shot health snapshot.
|
|
47
|
+
|
|
48
|
+
- Uses telemetry.health() for calibration + armable flags.
|
|
49
|
+
- Uses telemetry.gps_info() for fix type + satellites.
|
|
50
|
+
- Returns Px4HealthResponse; never raises to the API layer.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Fallback to global settings if not provided explicitly
|
|
54
|
+
port = port or settings.default_port
|
|
55
|
+
baudrate = baudrate or settings.default_baudrate
|
|
56
|
+
serial = f"serial://{port}:{baudrate}"
|
|
57
|
+
|
|
58
|
+
drone = System()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Connect to PX4
|
|
62
|
+
await drone.connect(system_address=serial)
|
|
63
|
+
|
|
64
|
+
# Get health & GPS within the global timeout
|
|
65
|
+
health = await _get_health_once(drone, timeout_s=timeout_s / 2)
|
|
66
|
+
try:
|
|
67
|
+
gps_info = await _get_gps_info_once(drone, timeout_s=timeout_s / 2)
|
|
68
|
+
except asyncio.TimeoutError:
|
|
69
|
+
gps_info = None
|
|
70
|
+
|
|
71
|
+
# Build calibration health
|
|
72
|
+
calibration = CalibrationHealth(
|
|
73
|
+
gyroscope_ok=health.is_gyrometer_calibration_ok,
|
|
74
|
+
accelerometer_ok=health.is_accelerometer_calibration_ok,
|
|
75
|
+
magnetometer_ok=health.is_magnetometer_calibration_ok,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# GPS fields
|
|
79
|
+
gps_fix_type: Optional[str] = None
|
|
80
|
+
satellites_used: Optional[int] = None
|
|
81
|
+
|
|
82
|
+
if gps_info is not None:
|
|
83
|
+
# gps_info.fix_type is an enum; prefer its name
|
|
84
|
+
if hasattr(gps_info.fix_type, "name"):
|
|
85
|
+
gps_fix_type = gps_info.fix_type.name.lower()
|
|
86
|
+
else:
|
|
87
|
+
gps_fix_type = str(gps_info.fix_type).lower()
|
|
88
|
+
|
|
89
|
+
# MAVSDK docs: num_satellites = number of visible satellites in use
|
|
90
|
+
satellites_used = int(getattr(gps_info, "num_satellites", 0))
|
|
91
|
+
|
|
92
|
+
# High-level status heuristic
|
|
93
|
+
if calibration.requires_calibration:
|
|
94
|
+
status = "degraded"
|
|
95
|
+
else:
|
|
96
|
+
status = "ok"
|
|
97
|
+
|
|
98
|
+
# Raw health flags (optional, but handy for debugging / advanced UI)
|
|
99
|
+
raw_health: Dict[str, Any] = {
|
|
100
|
+
"is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
|
|
101
|
+
"is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
|
|
102
|
+
"is_magnetometer_calibration_ok": health.is_magnetometer_calibration_ok,
|
|
103
|
+
"is_local_position_ok": getattr(health, "is_local_position_ok", None),
|
|
104
|
+
"is_global_position_ok": getattr(health, "is_global_position_ok", None),
|
|
105
|
+
"is_home_position_ok": getattr(health, "is_home_position_ok", None),
|
|
106
|
+
"is_armable": health.is_armable,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Px4HealthResponse(
|
|
110
|
+
status=status,
|
|
111
|
+
connection_port=port,
|
|
112
|
+
connection_baudrate=baudrate,
|
|
113
|
+
calibration=calibration,
|
|
114
|
+
gps_fix_type=gps_fix_type,
|
|
115
|
+
satellites_used=satellites_used,
|
|
116
|
+
armable=health.is_armable,
|
|
117
|
+
raw_health=raw_health,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except (asyncio.TimeoutError, Exception):
|
|
121
|
+
# On any connection / telemetry failure, report "disconnected"
|
|
122
|
+
return Px4HealthResponse(
|
|
123
|
+
status="disconnected",
|
|
124
|
+
connection_port=port,
|
|
125
|
+
connection_baudrate=baudrate,
|
|
126
|
+
calibration=None,
|
|
127
|
+
gps_fix_type=None,
|
|
128
|
+
satellites_used=None,
|
|
129
|
+
armable=False,
|
|
130
|
+
raw_health=None,
|
|
131
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Service for PX4 parameter upload operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import AsyncGenerator
|
|
5
|
+
|
|
6
|
+
from mavsdk import System
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def upload_parameters(
|
|
10
|
+
param_file_path: str,
|
|
11
|
+
port: str,
|
|
12
|
+
baudrate: int,
|
|
13
|
+
) -> AsyncGenerator[str, None]:
|
|
14
|
+
"""Upload PX4 parameters from a .params file, streaming progress via SSE."""
|
|
15
|
+
|
|
16
|
+
drone = None
|
|
17
|
+
try:
|
|
18
|
+
drone = System()
|
|
19
|
+
serial = f"serial://{port}:{baudrate}"
|
|
20
|
+
|
|
21
|
+
yield "data: {}\n\n".format(
|
|
22
|
+
json.dumps({"type": "status", "message": f"Connecting to {port}..."})
|
|
23
|
+
)
|
|
24
|
+
await drone.connect(system_address=serial)
|
|
25
|
+
yield "data: {}\n\n".format(
|
|
26
|
+
json.dumps({"type": "status", "message": "Connected to device"})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
param_plugin = drone.param
|
|
30
|
+
yield "data: {}\n\n".format(
|
|
31
|
+
json.dumps({"type": "status", "message": "Fetching current parameters..."})
|
|
32
|
+
)
|
|
33
|
+
params = await param_plugin.get_all_params()
|
|
34
|
+
float_params = params.float_params
|
|
35
|
+
int_params = params.int_params
|
|
36
|
+
custom_params = params.custom_params
|
|
37
|
+
int_param_names = [p.name for p in int_params]
|
|
38
|
+
float_param_names = [p.name for p in float_params]
|
|
39
|
+
custom_param_names = [p.name for p in custom_params]
|
|
40
|
+
|
|
41
|
+
yield "data: {}\n\n".format(
|
|
42
|
+
json.dumps({"type": "status", "message": "Checking if vehicle is landed..."})
|
|
43
|
+
)
|
|
44
|
+
async for is_in_air in drone.telemetry.in_air():
|
|
45
|
+
if is_in_air:
|
|
46
|
+
yield "data: {}\n\n".format(
|
|
47
|
+
json.dumps(
|
|
48
|
+
{
|
|
49
|
+
"type": "warning",
|
|
50
|
+
"message": "Waiting until vehicle is landed...",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
yield "data: {}\n\n".format(
|
|
58
|
+
json.dumps(
|
|
59
|
+
{
|
|
60
|
+
"type": "status",
|
|
61
|
+
"message": "Uploading parameters... Please do not arm the vehicle!",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with open(param_file_path, "r", encoding="utf-8") as param_file:
|
|
67
|
+
total_lines = sum(
|
|
68
|
+
1 for line in param_file if line.strip() and not line.startswith("#")
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
uploaded_count = 0
|
|
72
|
+
with open(param_file_path, "r", encoding="utf-8") as param_file:
|
|
73
|
+
for line in param_file:
|
|
74
|
+
if line.startswith("#") or not line.strip():
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
columns = line.strip().split(",")
|
|
78
|
+
if len(columns) < 2:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
name = columns[0].strip()
|
|
82
|
+
value = columns[1].strip()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if name in int_param_names:
|
|
86
|
+
await drone.param.set_param_int(name, int(value))
|
|
87
|
+
elif name in float_param_names:
|
|
88
|
+
await drone.param.set_param_float(name, float(value))
|
|
89
|
+
elif name in custom_param_names:
|
|
90
|
+
await drone.param.set_param_custom(name, value)
|
|
91
|
+
else:
|
|
92
|
+
yield "data: {}\n\n".format(
|
|
93
|
+
json.dumps(
|
|
94
|
+
{
|
|
95
|
+
"type": "warning",
|
|
96
|
+
"message": f"Parameter {name} not found, skipping",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
uploaded_count += 1
|
|
103
|
+
yield "data: {}\n\n".format(
|
|
104
|
+
json.dumps(
|
|
105
|
+
{
|
|
106
|
+
"type": "progress",
|
|
107
|
+
"message": f"Uploaded {name} = {value}",
|
|
108
|
+
"progress": uploaded_count,
|
|
109
|
+
"total": total_lines,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
except Exception as exc: # pragma: no cover - streaming generator
|
|
114
|
+
yield "data: {}\n\n".format(
|
|
115
|
+
json.dumps(
|
|
116
|
+
{
|
|
117
|
+
"type": "error",
|
|
118
|
+
"message": f"Failed to set {name}: {exc}",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
yield "data: {}\n\n".format(
|
|
124
|
+
json.dumps({"type": "status", "message": "Parameters uploaded, rebooting..."})
|
|
125
|
+
)
|
|
126
|
+
await drone.action.reboot()
|
|
127
|
+
yield "data: {}\n\n".format(
|
|
128
|
+
json.dumps(
|
|
129
|
+
{
|
|
130
|
+
"type": "success",
|
|
131
|
+
"message": "Parameters uploaded and vehicle rebooted",
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except Exception as exc: # pragma: no cover - streaming generator
|
|
137
|
+
yield "data: {}\n\n".format(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"type": "error",
|
|
141
|
+
"message": f"Parameter upload failed: {exc}",
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
raise
|
|
146
|
+
finally:
|
|
147
|
+
if drone:
|
|
148
|
+
# No explicit teardown required
|
|
149
|
+
pass
|
|
150
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Service for uploading files to PX4 SD card via FTP."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import AsyncGenerator
|
|
5
|
+
|
|
6
|
+
from mavsdk import System
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def upload_to_sdcard(
|
|
10
|
+
file_path: str,
|
|
11
|
+
port: str,
|
|
12
|
+
baudrate: int,
|
|
13
|
+
remote_path: str = "/fs/microsd/etc",
|
|
14
|
+
) -> AsyncGenerator[str, None]:
|
|
15
|
+
"""Upload a file to the PX4 SD card via MAVSDK FTP and stream progress."""
|
|
16
|
+
|
|
17
|
+
drone = None
|
|
18
|
+
try:
|
|
19
|
+
drone = System()
|
|
20
|
+
serial = f"serial://{port}:{baudrate}"
|
|
21
|
+
|
|
22
|
+
yield "data: {}\n\n".format(
|
|
23
|
+
json.dumps({"type": "status", "message": f"Connecting to {port}..."})
|
|
24
|
+
)
|
|
25
|
+
await drone.connect(system_address=serial)
|
|
26
|
+
yield "data: {}\n\n".format(
|
|
27
|
+
json.dumps({"type": "status", "message": "Connected to device"})
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
yield "data: {}\n\n".format(
|
|
31
|
+
json.dumps(
|
|
32
|
+
{
|
|
33
|
+
"type": "status",
|
|
34
|
+
"message": f"Listing directory {remote_path}...",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
directory_list = await drone.ftp.list_directory(remote_path)
|
|
40
|
+
yield "data: {}\n\n".format(
|
|
41
|
+
json.dumps(
|
|
42
|
+
{
|
|
43
|
+
"type": "status",
|
|
44
|
+
"message": f"Directory listing: {directory_list}",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
except Exception as exc: # pragma: no cover
|
|
49
|
+
yield "data: {}\n\n".format(
|
|
50
|
+
json.dumps(
|
|
51
|
+
{
|
|
52
|
+
"type": "warning",
|
|
53
|
+
"message": f"Could not list directory: {exc}",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
yield "data: {}\n\n".format(
|
|
59
|
+
json.dumps(
|
|
60
|
+
{
|
|
61
|
+
"type": "status",
|
|
62
|
+
"message": f"Uploading {file_path} to {remote_path}...",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
async for progress_data in drone.ftp.upload(file_path, remote_path):
|
|
67
|
+
yield "data: {}\n\n".format(
|
|
68
|
+
json.dumps({"type": "progress", "data": str(progress_data)})
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
yield "data: {}\n\n".format(
|
|
72
|
+
json.dumps({"type": "success", "message": "File uploaded successfully"})
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
except Exception as exc: # pragma: no cover - streaming generator
|
|
76
|
+
yield "data: {}\n\n".format(
|
|
77
|
+
json.dumps(
|
|
78
|
+
{
|
|
79
|
+
"type": "error",
|
|
80
|
+
"message": f"SD card upload failed: {exc}",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
raise
|
|
85
|
+
finally:
|
|
86
|
+
if drone:
|
|
87
|
+
# No explicit teardown required
|
|
88
|
+
pass
|
|
89
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Service for PX4 shell connection management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from mavsdk import System
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ShellConnection:
|
|
10
|
+
"""Manage a persistent MAVSDK shell connection with queued output."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, port: str, baudrate: int) -> None:
|
|
13
|
+
self.port = port
|
|
14
|
+
self.baudrate = baudrate
|
|
15
|
+
self.drone: Optional[System] = None
|
|
16
|
+
self.output_queue: "asyncio.Queue[str]" = asyncio.Queue()
|
|
17
|
+
self.connected = False
|
|
18
|
+
|
|
19
|
+
async def connect(self) -> None:
|
|
20
|
+
"""Establish connection to PX4 and start shell monitoring."""
|
|
21
|
+
|
|
22
|
+
self.drone = System()
|
|
23
|
+
serial = f"serial://{self.port}:{self.baudrate}"
|
|
24
|
+
await self.drone.connect(system_address=serial)
|
|
25
|
+
|
|
26
|
+
async for state in self.drone.core.connection_state():
|
|
27
|
+
if state.is_connected:
|
|
28
|
+
self.connected = True
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
asyncio.create_task(self._observe_shell())
|
|
32
|
+
|
|
33
|
+
async def _observe_shell(self) -> None:
|
|
34
|
+
"""Monitor shell output and place data into the queue."""
|
|
35
|
+
|
|
36
|
+
if not self.drone:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
async for output in self.drone.shell.receive():
|
|
41
|
+
await self.output_queue.put(output)
|
|
42
|
+
except Exception as exc: # pragma: no cover - streaming loop
|
|
43
|
+
await self.output_queue.put(f"Error receiving shell output: {exc}")
|
|
44
|
+
|
|
45
|
+
async def send_command(self, command: str) -> None:
|
|
46
|
+
"""Send a command to the PX4 shell."""
|
|
47
|
+
|
|
48
|
+
if not self.connected or not self.drone:
|
|
49
|
+
raise RuntimeError("Not connected to PX4")
|
|
50
|
+
await self.drone.shell.send(command)
|
|
51
|
+
|
|
52
|
+
async def receive_output(self) -> Optional[str]:
|
|
53
|
+
"""Receive shell output (non-blocking)."""
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
return await asyncio.wait_for(self.output_queue.get(), timeout=0.1)
|
|
57
|
+
except asyncio.TimeoutError:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
async def disconnect(self) -> None:
|
|
61
|
+
"""Close the connection if required."""
|
|
62
|
+
|
|
63
|
+
self.connected = False
|
|
64
|
+
# MAVSDK handles cleanup on garbage collection; nothing else required here.
|
|
65
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility functions for the PX4 configuration API."""
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Parser for extracting structured data from MAVSDK calibration progress updates."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_accelerometer_progress(progress_data) -> Dict[str, Any]:
|
|
9
|
+
"""
|
|
10
|
+
Parse accelerometer calibration progress data into structured format.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
progress_data: ProgressData object from mavsdk
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Dictionary with structured calibration information
|
|
17
|
+
"""
|
|
18
|
+
result: Dict[str, Any] = {
|
|
19
|
+
"type": "accelerometer_progress",
|
|
20
|
+
"progress": None,
|
|
21
|
+
"status_text": None,
|
|
22
|
+
"pending_orientations": [],
|
|
23
|
+
"current_orientation": None,
|
|
24
|
+
"current_message": None,
|
|
25
|
+
"result": None,
|
|
26
|
+
"is_complete": False,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Extract progress if available
|
|
30
|
+
if hasattr(progress_data, 'has_progress') and progress_data.has_progress:
|
|
31
|
+
progress_value = progress_data.progress
|
|
32
|
+
if not (math.isnan(progress_value) if isinstance(progress_value, float) else False):
|
|
33
|
+
result["progress"] = float(progress_value)
|
|
34
|
+
|
|
35
|
+
# Extract status text if available
|
|
36
|
+
if hasattr(progress_data, 'has_status_text') and progress_data.has_status_text:
|
|
37
|
+
status_text = progress_data.status_text
|
|
38
|
+
result["status_text"] = status_text
|
|
39
|
+
|
|
40
|
+
# Parse pending orientations (e.g., "pending: back front left right up down")
|
|
41
|
+
pending_match = re.search(r'pending:\s*([a-z\s]+)', status_text, re.IGNORECASE)
|
|
42
|
+
if pending_match:
|
|
43
|
+
orientations = [o.strip() for o in pending_match.group(1).split() if o.strip()]
|
|
44
|
+
result["pending_orientations"] = orientations
|
|
45
|
+
|
|
46
|
+
# Parse current orientation being measured
|
|
47
|
+
orientation_patterns = [
|
|
48
|
+
r'(\w+)\s+orientation detected',
|
|
49
|
+
r'(\w+)\s+side result:',
|
|
50
|
+
r'(\w+)\s+side done',
|
|
51
|
+
r'Hold still, measuring (\w+) side',
|
|
52
|
+
]
|
|
53
|
+
for pattern in orientation_patterns:
|
|
54
|
+
match = re.search(pattern, status_text, re.IGNORECASE)
|
|
55
|
+
if match:
|
|
56
|
+
result["current_orientation"] = match.group(1).lower()
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
# Extract result values (e.g., "down side result: [0.020 0.819 -9.750]")
|
|
60
|
+
result_match = re.search(r'result:\s*\[([\d\.\-\s]+)\]', status_text)
|
|
61
|
+
if result_match:
|
|
62
|
+
try:
|
|
63
|
+
values = [float(v) for v in result_match.group(1).split()]
|
|
64
|
+
result["result"] = values
|
|
65
|
+
except ValueError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Determine current message/instruction
|
|
69
|
+
if "progress" in status_text.lower() and "<" in status_text and ">" in status_text:
|
|
70
|
+
# This seems to be a completion indicator (e.g., "progress <102>")
|
|
71
|
+
result["is_complete"] = True
|
|
72
|
+
result["current_message"] = "complete"
|
|
73
|
+
elif "hold vehicle still" in status_text.lower():
|
|
74
|
+
result["current_message"] = "hold_still"
|
|
75
|
+
elif "detected rest position" in status_text.lower():
|
|
76
|
+
result["current_message"] = "rest_detected"
|
|
77
|
+
elif "measuring" in status_text.lower():
|
|
78
|
+
result["current_message"] = "measuring"
|
|
79
|
+
elif "done, rotate" in status_text.lower():
|
|
80
|
+
result["current_message"] = "rotate"
|
|
81
|
+
elif "already completed" in status_text.lower():
|
|
82
|
+
result["current_message"] = "already_completed"
|
|
83
|
+
elif "orientation detected" in status_text.lower():
|
|
84
|
+
result["current_message"] = "orientation_detected"
|
|
85
|
+
else:
|
|
86
|
+
# Default: use the status text as message
|
|
87
|
+
result["current_message"] = status_text
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_gyroscope_progress(progress_data) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Parse gyroscope calibration progress data into structured format.
|
|
95
|
+
|
|
96
|
+
Gyroscope calibration only provides progress values (0.0 to 1.0) without status text.
|
|
97
|
+
The vehicle should be kept still during calibration.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
progress_data: ProgressData object from mavsdk
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary with structured calibration information
|
|
104
|
+
"""
|
|
105
|
+
result: Dict[str, Any] = {
|
|
106
|
+
"type": "gyroscope_progress",
|
|
107
|
+
"progress": None,
|
|
108
|
+
"status_text": None,
|
|
109
|
+
"current_message": None,
|
|
110
|
+
"is_complete": False,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if hasattr(progress_data, 'has_progress') and progress_data.has_progress:
|
|
114
|
+
progress_value = progress_data.progress
|
|
115
|
+
if not (math.isnan(progress_value) if isinstance(progress_value, float) else False):
|
|
116
|
+
result["progress"] = float(progress_value)
|
|
117
|
+
# Mark as complete when progress reaches 1.0
|
|
118
|
+
if result["progress"] >= 1.0:
|
|
119
|
+
result["is_complete"] = True
|
|
120
|
+
result["current_message"] = "complete"
|
|
121
|
+
else:
|
|
122
|
+
result["current_message"] = "calibrating"
|
|
123
|
+
|
|
124
|
+
if hasattr(progress_data, 'has_status_text') and progress_data.has_status_text:
|
|
125
|
+
status_text = progress_data.status_text
|
|
126
|
+
result["status_text"] = status_text
|
|
127
|
+
if status_text and not result["current_message"]:
|
|
128
|
+
result["current_message"] = status_text
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_magnetometer_progress(progress_data) -> Dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Parse magnetometer calibration progress data into structured format.
|
|
136
|
+
|
|
137
|
+
Magnetometer calibration involves rotating the vehicle through different orientations
|
|
138
|
+
while progress increments from 0.0 to 1.0.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
progress_data: ProgressData object from mavsdk
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary with structured calibration information
|
|
145
|
+
"""
|
|
146
|
+
result: Dict[str, Any] = {
|
|
147
|
+
"type": "magnetometer_progress",
|
|
148
|
+
"progress": None,
|
|
149
|
+
"status_text": None,
|
|
150
|
+
"pending_orientations": [],
|
|
151
|
+
"current_orientation": None,
|
|
152
|
+
"current_message": None,
|
|
153
|
+
"is_complete": False,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Extract progress if available
|
|
157
|
+
if hasattr(progress_data, 'has_progress') and progress_data.has_progress:
|
|
158
|
+
progress_value = progress_data.progress
|
|
159
|
+
if not (math.isnan(progress_value) if isinstance(progress_value, float) else False):
|
|
160
|
+
result["progress"] = float(progress_value)
|
|
161
|
+
# Mark as complete when progress reaches 1.0
|
|
162
|
+
if result["progress"] >= 1.0:
|
|
163
|
+
result["is_complete"] = True
|
|
164
|
+
|
|
165
|
+
# Extract status text if available
|
|
166
|
+
if hasattr(progress_data, 'has_status_text') and progress_data.has_status_text:
|
|
167
|
+
status_text = progress_data.status_text
|
|
168
|
+
result["status_text"] = status_text
|
|
169
|
+
|
|
170
|
+
# Parse pending orientations (e.g., "pending: back front left right up down")
|
|
171
|
+
pending_match = re.search(r'pending:\s*([a-z\s]+)', status_text, re.IGNORECASE)
|
|
172
|
+
if pending_match:
|
|
173
|
+
orientations = [o.strip() for o in pending_match.group(1).split() if o.strip()]
|
|
174
|
+
result["pending_orientations"] = orientations
|
|
175
|
+
|
|
176
|
+
# Parse current orientation being measured
|
|
177
|
+
orientation_patterns = [
|
|
178
|
+
r'(\w+)\s+orientation detected',
|
|
179
|
+
r'(\w+)\s+side done',
|
|
180
|
+
r'(\w+)\s+side already completed',
|
|
181
|
+
]
|
|
182
|
+
for pattern in orientation_patterns:
|
|
183
|
+
match = re.search(pattern, status_text, re.IGNORECASE)
|
|
184
|
+
if match:
|
|
185
|
+
result["current_orientation"] = match.group(1).lower()
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
# Determine current message/instruction
|
|
189
|
+
if "Rotate vehicle" in status_text:
|
|
190
|
+
result["current_message"] = "rotate"
|
|
191
|
+
elif "hold vehicle still" in status_text.lower():
|
|
192
|
+
result["current_message"] = "hold_still"
|
|
193
|
+
elif "detected rest position" in status_text.lower():
|
|
194
|
+
result["current_message"] = "rest_detected"
|
|
195
|
+
elif "detected motion" in status_text.lower():
|
|
196
|
+
result["current_message"] = "motion_detected"
|
|
197
|
+
elif "done, rotate" in status_text.lower():
|
|
198
|
+
result["current_message"] = "rotate"
|
|
199
|
+
elif "already completed" in status_text.lower():
|
|
200
|
+
result["current_message"] = "already_completed"
|
|
201
|
+
elif "orientation detected" in status_text.lower():
|
|
202
|
+
result["current_message"] = "orientation_detected"
|
|
203
|
+
else:
|
|
204
|
+
# Default: use the status text as message
|
|
205
|
+
result["current_message"] = status_text
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_horizon_progress(progress_data) -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Parse level horizon calibration progress data into structured format.
|
|
213
|
+
|
|
214
|
+
Horizon calibration only provides progress values (0.0 to 1.0) without status text.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
progress_data: ProgressData object from mavsdk
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dictionary with structured calibration information
|
|
221
|
+
"""
|
|
222
|
+
result: Dict[str, Any] = {
|
|
223
|
+
"type": "horizon_progress",
|
|
224
|
+
"progress": None,
|
|
225
|
+
"status_text": None,
|
|
226
|
+
"current_message": None,
|
|
227
|
+
"is_complete": False,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if hasattr(progress_data, 'has_progress') and progress_data.has_progress:
|
|
231
|
+
progress_value = progress_data.progress
|
|
232
|
+
if not (math.isnan(progress_value) if isinstance(progress_value, float) else False):
|
|
233
|
+
result["progress"] = float(progress_value)
|
|
234
|
+
# Mark as complete when progress reaches 1.0
|
|
235
|
+
if result["progress"] >= 1.0:
|
|
236
|
+
result["is_complete"] = True
|
|
237
|
+
result["current_message"] = "complete"
|
|
238
|
+
else:
|
|
239
|
+
result["current_message"] = "calibrating"
|
|
240
|
+
|
|
241
|
+
if hasattr(progress_data, 'has_status_text') and progress_data.has_status_text:
|
|
242
|
+
status_text = progress_data.status_text
|
|
243
|
+
result["status_text"] = status_text
|
|
244
|
+
if status_text and not result["current_message"]:
|
|
245
|
+
result["current_message"] = status_text
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
|