px4-configuration 0.2.0__py3-none-any.whl → 0.2.2__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/api/__init__.py +2 -2
- px4_configuration/api/config.py +12 -2
- px4_configuration/api/main.py +3 -3
- px4_configuration/api/models.py +66 -3
- px4_configuration/api/services/calibration_service.py +193 -93
- px4_configuration/api/services/health_service.py +146 -62
- px4_configuration/api/services/param_service.py +26 -7
- px4_configuration/api/services/sdcard_service.py +26 -7
- px4_configuration/api/services/shell_service.py +41 -15
- px4_configuration/api/utils/calibration_parser.py +62 -21
- {px4_configuration-0.2.0.dist-info → px4_configuration-0.2.2.dist-info}/METADATA +4 -4
- px4_configuration-0.2.2.dist-info/RECORD +17 -0
- {px4_configuration-0.2.0.dist-info → px4_configuration-0.2.2.dist-info}/WHEEL +1 -1
- px4_configuration-0.2.0.dist-info/RECORD +0 -17
- {px4_configuration-0.2.0.dist-info → px4_configuration-0.2.2.dist-info}/entry_points.txt +0 -0
px4_configuration/api/config.py
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def _get_package_version() -> str:
|
|
7
|
+
"""Read version from package metadata (pyproject.toml). Single source of truth."""
|
|
8
|
+
try:
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
return version("px4-configuration")
|
|
12
|
+
except Exception:
|
|
13
|
+
return "0.0.0"
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
class Settings:
|
|
7
17
|
"""Application settings with defaults."""
|
|
8
18
|
|
|
@@ -10,9 +20,9 @@ class Settings:
|
|
|
10
20
|
default_port: str = os.getenv("PX4_DEFAULT_PORT", "/dev/pixhawk")
|
|
11
21
|
default_baudrate: int = int(os.getenv("PX4_DEFAULT_BAUDRATE", "2000000"))
|
|
12
22
|
|
|
13
|
-
# API settings
|
|
23
|
+
# API settings (version from pyproject.toml via package metadata)
|
|
14
24
|
api_title: str = "PX4 Configuration API"
|
|
15
|
-
api_version: str =
|
|
25
|
+
api_version: str = _get_package_version()
|
|
16
26
|
api_description: str = (
|
|
17
27
|
"REST API for configuring PX4 flight controller from companion computer"
|
|
18
28
|
)
|
px4_configuration/api/main.py
CHANGED
|
@@ -70,11 +70,11 @@ async def start_calibration(request: CalibrationRequest):
|
|
|
70
70
|
calibration_type=request.calibration_type, # <-- enum here
|
|
71
71
|
):
|
|
72
72
|
# Convert dict -> SSE frame
|
|
73
|
-
yield
|
|
73
|
+
yield json.dumps(payload)
|
|
74
74
|
except Exception as exc:
|
|
75
|
-
yield
|
|
75
|
+
yield json.dumps({'type': 'error', 'message': str(exc)})
|
|
76
76
|
finally:
|
|
77
|
-
yield
|
|
77
|
+
yield json.dumps({'type': 'complete'})
|
|
78
78
|
|
|
79
79
|
return EventSourceResponse(event_generator())
|
|
80
80
|
|
px4_configuration/api/models.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
4
|
-
from typing import Literal, Optional, Dict, Any
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Literal, Optional, Dict, Any, List
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field, ConfigDict
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class CalibrationType(
|
|
9
|
+
class CalibrationType(Enum):
|
|
10
10
|
"""Supported calibration types."""
|
|
11
11
|
GYROSCOPE = "gyroscope"
|
|
12
12
|
ACCELEROMETER = "accelerometer"
|
|
@@ -59,6 +59,61 @@ class CalibrationHealth(BaseModel):
|
|
|
59
59
|
"""Whether the calibration is required."""
|
|
60
60
|
return not (self.gyroscope_ok and self.accelerometer_ok and self.magnetometer_ok)
|
|
61
61
|
|
|
62
|
+
|
|
63
|
+
class HealthAndArmingCheckProblem(BaseModel):
|
|
64
|
+
"""A single health/arming check problem from PX4 Events."""
|
|
65
|
+
|
|
66
|
+
message: str = Field(..., description="Short, single-line message.")
|
|
67
|
+
description: str = Field(default="", description="Detailed description (optional).")
|
|
68
|
+
health_component: str = Field(default="", description="Associated component, e.g. 'gps'.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class HealthAndArmingCheckMode(BaseModel):
|
|
72
|
+
"""Arming checks for a specific flight mode (from PX4 Events)."""
|
|
73
|
+
|
|
74
|
+
mode_name: str = Field(..., description="Mode name, e.g. 'Position'.")
|
|
75
|
+
can_arm_or_run: bool = Field(
|
|
76
|
+
...,
|
|
77
|
+
description="If disarmed: whether arming is possible. If armed: whether the mode can be selected.",
|
|
78
|
+
)
|
|
79
|
+
problems: List[HealthAndArmingCheckProblem] = Field(
|
|
80
|
+
default_factory=list,
|
|
81
|
+
description="List of reported problems for this mode.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HealthComponentReport(BaseModel):
|
|
86
|
+
"""Health component report (e.g. GPS, Accelerometer) from PX4 Events."""
|
|
87
|
+
|
|
88
|
+
name: str = Field(..., description="Unique component name, e.g. 'gps'.")
|
|
89
|
+
label: str = Field(..., description="Human-readable label, e.g. 'GPS'.")
|
|
90
|
+
is_present: bool = Field(..., description="Whether the component is present.")
|
|
91
|
+
has_error: bool = Field(..., description="Whether the component has errors.")
|
|
92
|
+
has_warning: bool = Field(..., description="Whether the component has warnings.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class HealthAndArmingCheckReport(BaseModel):
|
|
96
|
+
"""
|
|
97
|
+
Health and arming checks report from MAVSDK Events plugin.
|
|
98
|
+
|
|
99
|
+
Provides detailed readiness-to-fly information: current mode arming status,
|
|
100
|
+
per-component health, and all reported problems.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
current_mode_intention: Optional[HealthAndArmingCheckMode] = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="Report for the currently intended flight mode (can_arm_or_run, problems).",
|
|
106
|
+
)
|
|
107
|
+
health_components: List[HealthComponentReport] = Field(
|
|
108
|
+
default_factory=list,
|
|
109
|
+
description="Health components (e.g. gps, accelerometer) with present/error/warning flags.",
|
|
110
|
+
)
|
|
111
|
+
all_problems: List[HealthAndArmingCheckProblem] = Field(
|
|
112
|
+
default_factory=list,
|
|
113
|
+
description="Complete list of all reported problems.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
62
117
|
class Px4HealthResponse(BaseModel):
|
|
63
118
|
"""
|
|
64
119
|
High-level PX4 health snapshot.
|
|
@@ -111,6 +166,14 @@ class Px4HealthResponse(BaseModel):
|
|
|
111
166
|
description="True if PX4 considers the vehicle armable.",
|
|
112
167
|
)
|
|
113
168
|
|
|
169
|
+
arming_checks_report: Optional[HealthAndArmingCheckReport] = Field(
|
|
170
|
+
default=None,
|
|
171
|
+
description=(
|
|
172
|
+
"Detailed health and arming checks from MAVSDK Events plugin "
|
|
173
|
+
"(mode intention, health components, all problems). None if Events plugin unavailable."
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
114
177
|
raw_health: Optional[Dict[str, Any]] = Field(
|
|
115
178
|
default=None,
|
|
116
179
|
description=(
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import AsyncGenerator, Dict, Any, Optional
|
|
6
7
|
|
|
7
8
|
from mavsdk import System
|
|
8
9
|
|
|
10
|
+
try:
|
|
11
|
+
from grpc import RpcError
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Fallback if grpc is not directly available (shouldn't happen with mavsdk)
|
|
14
|
+
RpcError = Exception
|
|
15
|
+
|
|
9
16
|
from ..models import CalibrationType
|
|
10
17
|
from ..utils.calibration_parser import (
|
|
11
18
|
parse_accelerometer_progress,
|
|
@@ -15,6 +22,40 @@ from ..utils.calibration_parser import (
|
|
|
15
22
|
)
|
|
16
23
|
|
|
17
24
|
|
|
25
|
+
def _format_grpc_error(exc: Exception) -> str:
|
|
26
|
+
"""Format gRPC errors into user-friendly messages."""
|
|
27
|
+
error_msg = str(exc)
|
|
28
|
+
|
|
29
|
+
if isinstance(exc, RpcError):
|
|
30
|
+
# Check for COMMAND_DENIED (PX4 rejected the command)
|
|
31
|
+
if "COMMAND_DENIED" in error_msg:
|
|
32
|
+
return (
|
|
33
|
+
"PX4 rejected the calibration command. "
|
|
34
|
+
"This may occur if: (1) a calibration is already in progress, "
|
|
35
|
+
"(2) the vehicle is armed or in flight, (3) the vehicle is not in the correct state. "
|
|
36
|
+
"Please ensure the vehicle is disarmed, on the ground, and no other calibration is running."
|
|
37
|
+
)
|
|
38
|
+
# Check for common gRPC error patterns
|
|
39
|
+
if "Connection reset by peer" in error_msg or "UNAVAILABLE" in error_msg:
|
|
40
|
+
return (
|
|
41
|
+
"Connection to PX4 device was lost. "
|
|
42
|
+
"This may occur if the device disconnected, rebooted, or the connection timed out. "
|
|
43
|
+
"Please check the device connection and try again."
|
|
44
|
+
)
|
|
45
|
+
elif "DEADLINE_EXCEEDED" in error_msg or "timeout" in error_msg.lower():
|
|
46
|
+
return (
|
|
47
|
+
"Connection timeout. The device may be unresponsive. "
|
|
48
|
+
"Please check the device connection and try again."
|
|
49
|
+
)
|
|
50
|
+
elif "PERMISSION_DENIED" in error_msg:
|
|
51
|
+
return "Permission denied. Please check device permissions."
|
|
52
|
+
else:
|
|
53
|
+
return f"gRPC error: {error_msg}"
|
|
54
|
+
|
|
55
|
+
# For non-gRPC exceptions, return the error message as-is
|
|
56
|
+
return error_msg
|
|
57
|
+
|
|
58
|
+
|
|
18
59
|
async def calibrate_sensors(
|
|
19
60
|
port: str,
|
|
20
61
|
baudrate: int,
|
|
@@ -26,111 +67,170 @@ async def calibrate_sensors(
|
|
|
26
67
|
This function is transport-agnostic: it does NOT produce SSE-formatted strings.
|
|
27
68
|
It yields plain Python dicts suitable for JSON serialization. The API layer
|
|
28
69
|
(FastAPI endpoint) is responsible for converting these dicts into SSE frames.
|
|
70
|
+
|
|
71
|
+
The function properly handles connection cleanup and gRPC errors.
|
|
29
72
|
"""
|
|
30
73
|
|
|
31
74
|
serial = f"serial://{port}:{baudrate}"
|
|
32
|
-
drone =
|
|
33
|
-
|
|
34
|
-
# Initial status messages
|
|
35
|
-
yield {
|
|
36
|
-
"type": "status",
|
|
37
|
-
"message": f"Connecting to {port}...",
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
# Connect to the PX4 system
|
|
41
|
-
await drone.connect(system_address=serial)
|
|
42
|
-
|
|
43
|
-
yield {
|
|
44
|
-
"type": "status",
|
|
45
|
-
"message": "Connected to device",
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Dispatch based on calibration type
|
|
49
|
-
if calibration_type is CalibrationType.GYROSCOPE:
|
|
50
|
-
# Gyroscope calibration
|
|
51
|
-
yield {
|
|
52
|
-
"type": "status",
|
|
53
|
-
"message": "Starting gyroscope calibration...",
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async for progress_data in drone.calibration.calibrate_gyro():
|
|
57
|
-
structured_data = parse_gyroscope_progress(progress_data)
|
|
58
|
-
yield structured_data
|
|
59
|
-
|
|
60
|
-
yield {
|
|
61
|
-
"type": "gyroscope_complete",
|
|
62
|
-
"message": "Gyroscope calibration finished",
|
|
63
|
-
"is_complete": True,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
elif calibration_type is CalibrationType.ACCELEROMETER:
|
|
67
|
-
# Accelerometer calibration
|
|
68
|
-
yield {
|
|
69
|
-
"type": "status",
|
|
70
|
-
"message": "Starting accelerometer calibration...",
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async for progress_data in drone.calibration.calibrate_accelerometer():
|
|
74
|
-
structured_data = parse_accelerometer_progress(progress_data)
|
|
75
|
-
yield structured_data
|
|
76
|
-
|
|
77
|
-
yield {
|
|
78
|
-
"type": "accelerometer_complete",
|
|
79
|
-
"message": "Accelerometer calibration finished",
|
|
80
|
-
"is_complete": True,
|
|
81
|
-
}
|
|
75
|
+
drone: Optional[System] = None
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
#
|
|
77
|
+
try:
|
|
78
|
+
# Initial status messages
|
|
85
79
|
yield {
|
|
86
80
|
"type": "status",
|
|
87
|
-
"message": "
|
|
81
|
+
"message": f"Connecting to {port}...",
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
# Create and connect to the PX4 system
|
|
85
|
+
drone = System()
|
|
86
|
+
await drone.connect(system_address=serial)
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
88
|
+
# Wait for connection to be established
|
|
89
|
+
async for state in drone.core.connection_state():
|
|
90
|
+
if state.is_connected:
|
|
91
|
+
break
|
|
99
92
|
|
|
100
|
-
elif calibration_type is CalibrationType.HORIZON:
|
|
101
|
-
# Level horizon calibration
|
|
102
93
|
yield {
|
|
103
94
|
"type": "status",
|
|
104
|
-
"message": "
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async for progress_data in drone.calibration.calibrate_level_horizon():
|
|
108
|
-
structured_data = parse_horizon_progress(progress_data)
|
|
109
|
-
yield structured_data
|
|
110
|
-
|
|
111
|
-
yield {
|
|
112
|
-
"type": "horizon_complete",
|
|
113
|
-
"message": "Level horizon calibration finished",
|
|
114
|
-
"is_complete": True,
|
|
95
|
+
"message": "Connected to device",
|
|
115
96
|
}
|
|
116
97
|
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
# Small delay to ensure connection is stable before starting calibration
|
|
99
|
+
# This helps avoid COMMAND_DENIED errors from rapid-fire requests
|
|
100
|
+
await asyncio.sleep(0.1)
|
|
101
|
+
|
|
102
|
+
# Dispatch based on calibration type
|
|
103
|
+
if calibration_type is CalibrationType.GYROSCOPE:
|
|
104
|
+
# Gyroscope calibration
|
|
105
|
+
yield {
|
|
106
|
+
"type": "status",
|
|
107
|
+
"message": "Starting gyroscope calibration...",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
prev: Dict[str, Any] | None = None
|
|
111
|
+
try:
|
|
112
|
+
async for progress_data in drone.calibration.calibrate_gyro():
|
|
113
|
+
structured_data = parse_gyroscope_progress(progress_data, prev=prev)
|
|
114
|
+
prev = structured_data
|
|
115
|
+
yield structured_data
|
|
116
|
+
except (RpcError, Exception) as exc:
|
|
117
|
+
yield {
|
|
118
|
+
"type": "error",
|
|
119
|
+
"message": f"Gyroscope calibration failed: {_format_grpc_error(exc)}",
|
|
120
|
+
}
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
yield {
|
|
124
|
+
"type": "gyroscope_complete",
|
|
125
|
+
"message": "Gyroscope calibration finished",
|
|
126
|
+
"is_complete": True,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
elif calibration_type is CalibrationType.ACCELEROMETER:
|
|
130
|
+
# Accelerometer calibration
|
|
131
|
+
yield {
|
|
132
|
+
"type": "status",
|
|
133
|
+
"message": "Starting accelerometer calibration...",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
prev = None
|
|
137
|
+
try:
|
|
138
|
+
async for progress_data in drone.calibration.calibrate_accelerometer():
|
|
139
|
+
structured_data = parse_accelerometer_progress(progress_data, prev=prev)
|
|
140
|
+
prev = structured_data
|
|
141
|
+
yield structured_data
|
|
142
|
+
except (RpcError, Exception) as exc:
|
|
143
|
+
yield {
|
|
144
|
+
"type": "error",
|
|
145
|
+
"message": f"Accelerometer calibration failed: {_format_grpc_error(exc)}",
|
|
146
|
+
}
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
yield {
|
|
150
|
+
"type": "accelerometer_complete",
|
|
151
|
+
"message": "Accelerometer calibration finished",
|
|
152
|
+
"is_complete": True,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
elif calibration_type is CalibrationType.MAGNETOMETER:
|
|
156
|
+
# Magnetometer calibration
|
|
157
|
+
yield {
|
|
158
|
+
"type": "status",
|
|
159
|
+
"message": "Starting magnetometer calibration...",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
prev = None
|
|
163
|
+
try:
|
|
164
|
+
async for progress_data in drone.calibration.calibrate_magnetometer():
|
|
165
|
+
structured_data = parse_magnetometer_progress(progress_data, prev=prev)
|
|
166
|
+
prev = structured_data
|
|
167
|
+
yield structured_data
|
|
168
|
+
except (RpcError, Exception) as exc:
|
|
169
|
+
yield {
|
|
170
|
+
"type": "error",
|
|
171
|
+
"message": f"Magnetometer calibration failed: {_format_grpc_error(exc)}",
|
|
172
|
+
}
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
yield {
|
|
176
|
+
"type": "magnetometer_complete",
|
|
177
|
+
"message": "Magnetometer calibration finished",
|
|
178
|
+
"is_complete": True,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
elif calibration_type is CalibrationType.HORIZON:
|
|
182
|
+
# Level horizon calibration
|
|
183
|
+
yield {
|
|
184
|
+
"type": "status",
|
|
185
|
+
"message": "Starting board level horizon calibration...",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
prev = None
|
|
189
|
+
try:
|
|
190
|
+
async for progress_data in drone.calibration.calibrate_level_horizon():
|
|
191
|
+
structured_data = parse_horizon_progress(progress_data, prev=prev)
|
|
192
|
+
prev = structured_data
|
|
193
|
+
yield structured_data
|
|
194
|
+
except (RpcError, Exception) as exc:
|
|
195
|
+
yield {
|
|
196
|
+
"type": "error",
|
|
197
|
+
"message": f"Horizon calibration failed: {_format_grpc_error(exc)}",
|
|
198
|
+
}
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
yield {
|
|
202
|
+
"type": "horizon_complete",
|
|
203
|
+
"message": "Level horizon calibration finished",
|
|
204
|
+
"is_complete": True,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
elif calibration_type is CalibrationType.ESC:
|
|
208
|
+
# ESC calibration not implemented yet
|
|
209
|
+
yield {
|
|
210
|
+
"type": "status",
|
|
211
|
+
"message": "Starting ESC calibration...",
|
|
212
|
+
}
|
|
213
|
+
yield {
|
|
214
|
+
"type": "warning",
|
|
215
|
+
"message": "ESC calibration not yet implemented",
|
|
216
|
+
}
|
|
217
|
+
yield {
|
|
218
|
+
"type": "status",
|
|
219
|
+
"message": "ESC calibration finished",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
except (RpcError, Exception) as exc:
|
|
223
|
+
# Handle connection and gRPC errors
|
|
224
|
+
error_message = _format_grpc_error(exc)
|
|
123
225
|
yield {
|
|
124
|
-
"type": "
|
|
125
|
-
"message": "
|
|
226
|
+
"type": "error",
|
|
227
|
+
"message": f"Calibration failed: {error_message}",
|
|
126
228
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"message": "Calibration sequence completed",
|
|
136
|
-
}
|
|
229
|
+
raise
|
|
230
|
+
|
|
231
|
+
finally:
|
|
232
|
+
# Cleanup: Ensure the System object is properly released
|
|
233
|
+
# MAVSDK System objects are cleaned up when they go out of scope,
|
|
234
|
+
# but explicitly setting to None helps with resource management
|
|
235
|
+
if drone is not None:
|
|
236
|
+
drone = None
|
|
@@ -3,38 +3,105 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
7
|
|
|
8
8
|
from mavsdk import System
|
|
9
9
|
|
|
10
|
+
try:
|
|
11
|
+
from grpc import RpcError
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Fallback if grpc is not directly available (shouldn't happen with mavsdk)
|
|
14
|
+
RpcError = Exception
|
|
15
|
+
|
|
10
16
|
from ..config import settings
|
|
11
|
-
from ..models import
|
|
17
|
+
from ..models import (
|
|
18
|
+
Px4HealthResponse,
|
|
19
|
+
CalibrationHealth,
|
|
20
|
+
HealthAndArmingCheckReport,
|
|
21
|
+
HealthAndArmingCheckMode,
|
|
22
|
+
HealthAndArmingCheckProblem,
|
|
23
|
+
HealthComponentReport,
|
|
24
|
+
)
|
|
12
25
|
|
|
13
26
|
|
|
14
|
-
async def
|
|
27
|
+
async def _get_stable_health(drone: System, duration_s: float = 2.0) -> Optional[Any]:
|
|
15
28
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
29
|
+
Stream health messages for the specified duration and return the latest.
|
|
30
|
+
This skips initial stale messages (all False) and gets accurate values.
|
|
19
31
|
"""
|
|
20
|
-
|
|
32
|
+
latest_health = None
|
|
33
|
+
start_time = asyncio.get_event_loop().time()
|
|
34
|
+
|
|
35
|
+
try:
|
|
21
36
|
async for health in drone.telemetry.health():
|
|
22
|
-
|
|
37
|
+
latest_health = health
|
|
38
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
39
|
+
if elapsed >= duration_s:
|
|
40
|
+
break
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
return latest_health
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _convert_arming_checks_report(report: Any) -> HealthAndArmingCheckReport:
|
|
48
|
+
"""Convert MAVSDK HealthAndArmingCheckReport to our Pydantic model."""
|
|
49
|
+
current = getattr(report, "current_mode_intention", None)
|
|
50
|
+
current_mode = None
|
|
51
|
+
if current is not None:
|
|
52
|
+
problems = [
|
|
53
|
+
HealthAndArmingCheckProblem(
|
|
54
|
+
message=getattr(p, "message", ""),
|
|
55
|
+
description=getattr(p, "description", ""),
|
|
56
|
+
health_component=getattr(p, "health_component", ""),
|
|
57
|
+
)
|
|
58
|
+
for p in (getattr(current, "problems", None) or [])
|
|
59
|
+
]
|
|
60
|
+
current_mode = HealthAndArmingCheckMode(
|
|
61
|
+
mode_name=getattr(current, "mode_name", ""),
|
|
62
|
+
can_arm_or_run=getattr(current, "can_arm_or_run", False),
|
|
63
|
+
problems=problems,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
health_components = [
|
|
67
|
+
HealthComponentReport(
|
|
68
|
+
name=getattr(c, "name", ""),
|
|
69
|
+
label=getattr(c, "label", ""),
|
|
70
|
+
is_present=getattr(c, "is_present", False),
|
|
71
|
+
has_error=getattr(c, "has_error", False),
|
|
72
|
+
has_warning=getattr(c, "has_warning", False),
|
|
73
|
+
)
|
|
74
|
+
for c in (getattr(report, "health_components", None) or [])
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
all_problems = [
|
|
78
|
+
HealthAndArmingCheckProblem(
|
|
79
|
+
message=getattr(p, "message", ""),
|
|
80
|
+
description=getattr(p, "description", ""),
|
|
81
|
+
health_component=getattr(p, "health_component", ""),
|
|
82
|
+
)
|
|
83
|
+
for p in (getattr(report, "all_problems", None) or [])
|
|
84
|
+
]
|
|
23
85
|
|
|
24
|
-
return
|
|
86
|
+
return HealthAndArmingCheckReport(
|
|
87
|
+
current_mode_intention=current_mode,
|
|
88
|
+
health_components=health_components,
|
|
89
|
+
all_problems=all_problems,
|
|
90
|
+
)
|
|
25
91
|
|
|
26
92
|
|
|
27
|
-
async def
|
|
93
|
+
async def _get_arming_checks_report(drone: System) -> Optional[HealthAndArmingCheckReport]:
|
|
28
94
|
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
95
|
+
Fetch health and arming checks report from MAVSDK Events plugin.
|
|
96
|
+
Returns None if Events plugin is not available (e.g. older PX4).
|
|
32
97
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
98
|
+
try:
|
|
99
|
+
report = await drone.events.get_health_and_arming_checks_report()
|
|
100
|
+
if report is None:
|
|
101
|
+
return None
|
|
102
|
+
return _convert_arming_checks_report(report)
|
|
103
|
+
except Exception:
|
|
104
|
+
return None
|
|
38
105
|
|
|
39
106
|
|
|
40
107
|
async def read_px4_health(
|
|
@@ -42,83 +109,95 @@ async def read_px4_health(
|
|
|
42
109
|
baudrate: Optional[int] = None,
|
|
43
110
|
timeout_s: float = 10.0,
|
|
44
111
|
) -> 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
|
-
"""
|
|
112
|
+
"""Connect to PX4 via MAVSDK and return a health snapshot using telemetry health flags."""
|
|
52
113
|
|
|
53
|
-
# Fallback to global settings if not provided explicitly
|
|
54
114
|
port = port or settings.default_port
|
|
55
115
|
baudrate = baudrate or settings.default_baudrate
|
|
56
116
|
serial = f"serial://{port}:{baudrate}"
|
|
57
117
|
|
|
58
|
-
drone =
|
|
118
|
+
drone: Optional[System] = None
|
|
59
119
|
|
|
60
120
|
try:
|
|
61
|
-
|
|
121
|
+
drone = System()
|
|
62
122
|
await drone.connect(system_address=serial)
|
|
63
123
|
|
|
64
|
-
#
|
|
65
|
-
|
|
124
|
+
# Wait for connection with timeout
|
|
125
|
+
connection_established = False
|
|
66
126
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
async for state in drone.core.connection_state():
|
|
128
|
+
if state.is_connected:
|
|
129
|
+
connection_established = True
|
|
130
|
+
break
|
|
131
|
+
# Timeout if not connected within reasonable time
|
|
132
|
+
await asyncio.sleep(0.1)
|
|
133
|
+
except (RpcError, Exception):
|
|
134
|
+
connection_established = False
|
|
135
|
+
|
|
136
|
+
if not connection_established:
|
|
137
|
+
return Px4HealthResponse(
|
|
138
|
+
status="disconnected",
|
|
139
|
+
connection_port=port,
|
|
140
|
+
connection_baudrate=baudrate,
|
|
141
|
+
calibration=None,
|
|
142
|
+
gps_fix_type=None,
|
|
143
|
+
satellites_used=None,
|
|
144
|
+
armable=False,
|
|
145
|
+
arming_checks_report=None,
|
|
146
|
+
raw_health=None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Stream health for 2 seconds to get accurate values (skip initial stale messages)
|
|
150
|
+
health = await asyncio.wait_for(_get_stable_health(drone, duration_s=2.0), timeout_s)
|
|
151
|
+
|
|
152
|
+
if health is None:
|
|
153
|
+
return Px4HealthResponse(
|
|
154
|
+
status="disconnected",
|
|
155
|
+
connection_port=port,
|
|
156
|
+
connection_baudrate=baudrate,
|
|
157
|
+
calibration=None,
|
|
158
|
+
gps_fix_type=None,
|
|
159
|
+
satellites_used=None,
|
|
160
|
+
armable=False,
|
|
161
|
+
arming_checks_report=None,
|
|
162
|
+
raw_health=None,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Use telemetry health flags directly
|
|
72
166
|
calibration = CalibrationHealth(
|
|
73
167
|
gyroscope_ok=health.is_gyrometer_calibration_ok,
|
|
74
168
|
accelerometer_ok=health.is_accelerometer_calibration_ok,
|
|
75
169
|
magnetometer_ok=health.is_magnetometer_calibration_ok,
|
|
76
170
|
)
|
|
77
171
|
|
|
78
|
-
#
|
|
79
|
-
|
|
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()
|
|
172
|
+
# Determine status
|
|
173
|
+
status = "ok" if (health.is_armable and not calibration.requires_calibration) else "degraded"
|
|
88
174
|
|
|
89
|
-
|
|
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)
|
|
175
|
+
# Minimal raw health for debugging
|
|
99
176
|
raw_health: Dict[str, Any] = {
|
|
177
|
+
"raw_health_message": str(health),
|
|
100
178
|
"is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
|
|
101
179
|
"is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
|
|
102
180
|
"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
181
|
"is_armable": health.is_armable,
|
|
107
182
|
}
|
|
108
183
|
|
|
184
|
+
# Fetch detailed health and arming checks from Events plugin (optional)
|
|
185
|
+
arming_checks_report = await _get_arming_checks_report(drone)
|
|
186
|
+
|
|
109
187
|
return Px4HealthResponse(
|
|
110
188
|
status=status,
|
|
111
189
|
connection_port=port,
|
|
112
190
|
connection_baudrate=baudrate,
|
|
113
191
|
calibration=calibration,
|
|
114
|
-
gps_fix_type=
|
|
115
|
-
satellites_used=
|
|
192
|
+
gps_fix_type=None,
|
|
193
|
+
satellites_used=None,
|
|
116
194
|
armable=health.is_armable,
|
|
195
|
+
arming_checks_report=arming_checks_report,
|
|
117
196
|
raw_health=raw_health,
|
|
118
197
|
)
|
|
119
198
|
|
|
120
|
-
except (
|
|
121
|
-
#
|
|
199
|
+
except (RpcError, Exception):
|
|
200
|
+
# Return disconnected status on any error
|
|
122
201
|
return Px4HealthResponse(
|
|
123
202
|
status="disconnected",
|
|
124
203
|
connection_port=port,
|
|
@@ -127,5 +206,10 @@ async def read_px4_health(
|
|
|
127
206
|
gps_fix_type=None,
|
|
128
207
|
satellites_used=None,
|
|
129
208
|
armable=False,
|
|
209
|
+
arming_checks_report=None,
|
|
130
210
|
raw_health=None,
|
|
131
211
|
)
|
|
212
|
+
finally:
|
|
213
|
+
# Cleanup: Ensure the System object is properly released
|
|
214
|
+
if drone is not None:
|
|
215
|
+
drone = None
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""Service for PX4 parameter upload operations."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import AsyncGenerator
|
|
4
|
+
from typing import AsyncGenerator, Optional
|
|
5
5
|
|
|
6
6
|
from mavsdk import System
|
|
7
7
|
|
|
8
|
+
try:
|
|
9
|
+
from grpc import RpcError
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Fallback if grpc is not directly available (shouldn't happen with mavsdk)
|
|
12
|
+
RpcError = Exception
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
async def upload_parameters(
|
|
10
16
|
param_file_path: str,
|
|
@@ -13,7 +19,7 @@ async def upload_parameters(
|
|
|
13
19
|
) -> AsyncGenerator[str, None]:
|
|
14
20
|
"""Upload PX4 parameters from a .params file, streaming progress via SSE."""
|
|
15
21
|
|
|
16
|
-
drone = None
|
|
22
|
+
drone: Optional[System] = None
|
|
17
23
|
try:
|
|
18
24
|
drone = System()
|
|
19
25
|
serial = f"serial://{port}:{baudrate}"
|
|
@@ -22,6 +28,12 @@ async def upload_parameters(
|
|
|
22
28
|
json.dumps({"type": "status", "message": f"Connecting to {port}..."})
|
|
23
29
|
)
|
|
24
30
|
await drone.connect(system_address=serial)
|
|
31
|
+
|
|
32
|
+
# Wait for connection to be established
|
|
33
|
+
async for state in drone.core.connection_state():
|
|
34
|
+
if state.is_connected:
|
|
35
|
+
break
|
|
36
|
+
|
|
25
37
|
yield "data: {}\n\n".format(
|
|
26
38
|
json.dumps({"type": "status", "message": "Connected to device"})
|
|
27
39
|
)
|
|
@@ -133,18 +145,25 @@ async def upload_parameters(
|
|
|
133
145
|
)
|
|
134
146
|
)
|
|
135
147
|
|
|
136
|
-
except Exception as exc: # pragma: no cover - streaming generator
|
|
148
|
+
except (RpcError, Exception) as exc: # pragma: no cover - streaming generator
|
|
149
|
+
error_msg = str(exc)
|
|
150
|
+
if isinstance(exc, RpcError):
|
|
151
|
+
if "Connection reset by peer" in error_msg or "UNAVAILABLE" in error_msg:
|
|
152
|
+
error_msg = (
|
|
153
|
+
"Connection to PX4 device was lost. "
|
|
154
|
+
"This may occur if the device disconnected, rebooted, or the connection timed out."
|
|
155
|
+
)
|
|
137
156
|
yield "data: {}\n\n".format(
|
|
138
157
|
json.dumps(
|
|
139
158
|
{
|
|
140
159
|
"type": "error",
|
|
141
|
-
"message": f"Parameter upload failed: {
|
|
160
|
+
"message": f"Parameter upload failed: {error_msg}",
|
|
142
161
|
}
|
|
143
162
|
)
|
|
144
163
|
)
|
|
145
164
|
raise
|
|
146
165
|
finally:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
# Cleanup: Ensure the System object is properly released
|
|
167
|
+
if drone is not None:
|
|
168
|
+
drone = None
|
|
150
169
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""Service for uploading files to PX4 SD card via FTP."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import AsyncGenerator
|
|
4
|
+
from typing import AsyncGenerator, Optional
|
|
5
5
|
|
|
6
6
|
from mavsdk import System
|
|
7
7
|
|
|
8
|
+
try:
|
|
9
|
+
from grpc import RpcError
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Fallback if grpc is not directly available (shouldn't happen with mavsdk)
|
|
12
|
+
RpcError = Exception
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
async def upload_to_sdcard(
|
|
10
16
|
file_path: str,
|
|
@@ -14,7 +20,7 @@ async def upload_to_sdcard(
|
|
|
14
20
|
) -> AsyncGenerator[str, None]:
|
|
15
21
|
"""Upload a file to the PX4 SD card via MAVSDK FTP and stream progress."""
|
|
16
22
|
|
|
17
|
-
drone = None
|
|
23
|
+
drone: Optional[System] = None
|
|
18
24
|
try:
|
|
19
25
|
drone = System()
|
|
20
26
|
serial = f"serial://{port}:{baudrate}"
|
|
@@ -23,6 +29,12 @@ async def upload_to_sdcard(
|
|
|
23
29
|
json.dumps({"type": "status", "message": f"Connecting to {port}..."})
|
|
24
30
|
)
|
|
25
31
|
await drone.connect(system_address=serial)
|
|
32
|
+
|
|
33
|
+
# Wait for connection to be established
|
|
34
|
+
async for state in drone.core.connection_state():
|
|
35
|
+
if state.is_connected:
|
|
36
|
+
break
|
|
37
|
+
|
|
26
38
|
yield "data: {}\n\n".format(
|
|
27
39
|
json.dumps({"type": "status", "message": "Connected to device"})
|
|
28
40
|
)
|
|
@@ -72,18 +84,25 @@ async def upload_to_sdcard(
|
|
|
72
84
|
json.dumps({"type": "success", "message": "File uploaded successfully"})
|
|
73
85
|
)
|
|
74
86
|
|
|
75
|
-
except Exception as exc: # pragma: no cover - streaming generator
|
|
87
|
+
except (RpcError, Exception) as exc: # pragma: no cover - streaming generator
|
|
88
|
+
error_msg = str(exc)
|
|
89
|
+
if isinstance(exc, RpcError):
|
|
90
|
+
if "Connection reset by peer" in error_msg or "UNAVAILABLE" in error_msg:
|
|
91
|
+
error_msg = (
|
|
92
|
+
"Connection to PX4 device was lost. "
|
|
93
|
+
"This may occur if the device disconnected, rebooted, or the connection timed out."
|
|
94
|
+
)
|
|
76
95
|
yield "data: {}\n\n".format(
|
|
77
96
|
json.dumps(
|
|
78
97
|
{
|
|
79
98
|
"type": "error",
|
|
80
|
-
"message": f"SD card upload failed: {
|
|
99
|
+
"message": f"SD card upload failed: {error_msg}",
|
|
81
100
|
}
|
|
82
101
|
)
|
|
83
102
|
)
|
|
84
103
|
raise
|
|
85
104
|
finally:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
# Cleanup: Ensure the System object is properly released
|
|
106
|
+
if drone is not None:
|
|
107
|
+
drone = None
|
|
89
108
|
|
|
@@ -5,6 +5,12 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
from mavsdk import System
|
|
7
7
|
|
|
8
|
+
try:
|
|
9
|
+
from grpc import RpcError
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Fallback if grpc is not directly available (shouldn't happen with mavsdk)
|
|
12
|
+
RpcError = Exception
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
class ShellConnection:
|
|
10
16
|
"""Manage a persistent MAVSDK shell connection with queued output."""
|
|
@@ -19,16 +25,23 @@ class ShellConnection:
|
|
|
19
25
|
async def connect(self) -> None:
|
|
20
26
|
"""Establish connection to PX4 and start shell monitoring."""
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
try:
|
|
29
|
+
self.drone = System()
|
|
30
|
+
serial = f"serial://{self.port}:{self.baudrate}"
|
|
31
|
+
await self.drone.connect(system_address=serial)
|
|
32
|
+
|
|
33
|
+
# Wait for connection to be established
|
|
34
|
+
async for state in self.drone.core.connection_state():
|
|
35
|
+
if state.is_connected:
|
|
36
|
+
self.connected = True
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
asyncio.create_task(self._observe_shell())
|
|
40
|
+
except (RpcError, Exception) as exc:
|
|
41
|
+
self.connected = False
|
|
42
|
+
if self.drone is not None:
|
|
43
|
+
self.drone = None
|
|
44
|
+
raise RuntimeError(f"Failed to connect to PX4 shell: {exc}") from exc
|
|
32
45
|
|
|
33
46
|
async def _observe_shell(self) -> None:
|
|
34
47
|
"""Monitor shell output and place data into the queue."""
|
|
@@ -38,16 +51,27 @@ class ShellConnection:
|
|
|
38
51
|
|
|
39
52
|
try:
|
|
40
53
|
async for output in self.drone.shell.receive():
|
|
54
|
+
if not self.connected:
|
|
55
|
+
break
|
|
41
56
|
await self.output_queue.put(output)
|
|
42
|
-
except Exception as exc: # pragma: no cover - streaming loop
|
|
43
|
-
|
|
57
|
+
except (RpcError, Exception) as exc: # pragma: no cover - streaming loop
|
|
58
|
+
error_msg = str(exc)
|
|
59
|
+
if isinstance(exc, RpcError):
|
|
60
|
+
if "Connection reset by peer" in error_msg or "UNAVAILABLE" in error_msg:
|
|
61
|
+
error_msg = "Connection to PX4 device was lost"
|
|
62
|
+
await self.output_queue.put(f"Error receiving shell output: {error_msg}")
|
|
63
|
+
self.connected = False
|
|
44
64
|
|
|
45
65
|
async def send_command(self, command: str) -> None:
|
|
46
66
|
"""Send a command to the PX4 shell."""
|
|
47
67
|
|
|
48
68
|
if not self.connected or not self.drone:
|
|
49
69
|
raise RuntimeError("Not connected to PX4")
|
|
50
|
-
|
|
70
|
+
try:
|
|
71
|
+
await self.drone.shell.send(command)
|
|
72
|
+
except (RpcError, Exception) as exc:
|
|
73
|
+
self.connected = False
|
|
74
|
+
raise RuntimeError(f"Failed to send command: {exc}") from exc
|
|
51
75
|
|
|
52
76
|
async def receive_output(self) -> Optional[str]:
|
|
53
77
|
"""Receive shell output (non-blocking)."""
|
|
@@ -58,8 +82,10 @@ class ShellConnection:
|
|
|
58
82
|
return None
|
|
59
83
|
|
|
60
84
|
async def disconnect(self) -> None:
|
|
61
|
-
"""Close the connection
|
|
85
|
+
"""Close the connection and cleanup resources."""
|
|
62
86
|
|
|
63
87
|
self.connected = False
|
|
64
|
-
#
|
|
88
|
+
# Cleanup: Ensure the System object is properly released
|
|
89
|
+
if self.drone is not None:
|
|
90
|
+
self.drone = None
|
|
65
91
|
|
|
@@ -5,7 +5,10 @@ from typing import Optional, Dict, Any, List
|
|
|
5
5
|
import math
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def parse_accelerometer_progress(
|
|
8
|
+
def parse_accelerometer_progress(
|
|
9
|
+
progress_data,
|
|
10
|
+
prev: Optional[Dict[str, Any]] = None,
|
|
11
|
+
) -> Dict[str, Any]:
|
|
9
12
|
"""
|
|
10
13
|
Parse accelerometer calibration progress data into structured format.
|
|
11
14
|
|
|
@@ -33,10 +36,11 @@ def parse_accelerometer_progress(progress_data) -> Dict[str, Any]:
|
|
|
33
36
|
result["progress"] = float(progress_value)
|
|
34
37
|
|
|
35
38
|
# Extract status text if available
|
|
39
|
+
pending_match = None
|
|
36
40
|
if hasattr(progress_data, 'has_status_text') and progress_data.has_status_text:
|
|
37
41
|
status_text = progress_data.status_text
|
|
38
42
|
result["status_text"] = status_text
|
|
39
|
-
|
|
43
|
+
|
|
40
44
|
# Parse pending orientations (e.g., "pending: back front left right up down")
|
|
41
45
|
pending_match = re.search(r'pending:\s*([a-z\s]+)', status_text, re.IGNORECASE)
|
|
42
46
|
if pending_match:
|
|
@@ -65,31 +69,44 @@ def parse_accelerometer_progress(progress_data) -> Dict[str, Any]:
|
|
|
65
69
|
except ValueError:
|
|
66
70
|
pass
|
|
67
71
|
|
|
68
|
-
# Determine current message/instruction
|
|
69
|
-
|
|
72
|
+
# Determine current message/instruction (normalize to simple labels)
|
|
73
|
+
low = status_text.lower()
|
|
74
|
+
if "progress" in low and "<" in status_text and ">" in status_text:
|
|
70
75
|
# This seems to be a completion indicator (e.g., "progress <102>")
|
|
71
76
|
result["is_complete"] = True
|
|
72
77
|
result["current_message"] = "complete"
|
|
73
|
-
elif
|
|
78
|
+
elif pending_match:
|
|
79
|
+
result["current_message"] = "pending"
|
|
80
|
+
elif "hold vehicle still" in low:
|
|
74
81
|
result["current_message"] = "hold_still"
|
|
75
|
-
elif "detected rest position" in
|
|
82
|
+
elif "detected rest position" in low:
|
|
76
83
|
result["current_message"] = "rest_detected"
|
|
77
|
-
elif "measuring" in
|
|
84
|
+
elif "measuring" in low:
|
|
78
85
|
result["current_message"] = "measuring"
|
|
79
|
-
elif "done, rotate" in
|
|
86
|
+
elif "done, rotate" in low:
|
|
80
87
|
result["current_message"] = "rotate"
|
|
81
|
-
elif "already completed" in
|
|
88
|
+
elif "already completed" in low:
|
|
82
89
|
result["current_message"] = "already_completed"
|
|
83
|
-
elif "orientation detected" in
|
|
90
|
+
elif "orientation detected" in low:
|
|
84
91
|
result["current_message"] = "orientation_detected"
|
|
85
92
|
else:
|
|
86
93
|
# Default: use the status text as message
|
|
87
94
|
result["current_message"] = status_text
|
|
95
|
+
|
|
96
|
+
# Carry forward state from previous snapshot where appropriate
|
|
97
|
+
if prev:
|
|
98
|
+
if not result["pending_orientations"]:
|
|
99
|
+
result["pending_orientations"] = prev.get("pending_orientations", [])
|
|
100
|
+
if result["current_orientation"] is None:
|
|
101
|
+
result["current_orientation"] = prev.get("current_orientation")
|
|
88
102
|
|
|
89
103
|
return result
|
|
90
104
|
|
|
91
105
|
|
|
92
|
-
def parse_gyroscope_progress(
|
|
106
|
+
def parse_gyroscope_progress(
|
|
107
|
+
progress_data,
|
|
108
|
+
prev: Optional[Dict[str, Any]] = None,
|
|
109
|
+
) -> Dict[str, Any]:
|
|
93
110
|
"""
|
|
94
111
|
Parse gyroscope calibration progress data into structured format.
|
|
95
112
|
|
|
@@ -126,11 +143,19 @@ def parse_gyroscope_progress(progress_data) -> Dict[str, Any]:
|
|
|
126
143
|
result["status_text"] = status_text
|
|
127
144
|
if status_text and not result["current_message"]:
|
|
128
145
|
result["current_message"] = status_text
|
|
146
|
+
|
|
147
|
+
# Carry forward simple state where helpful
|
|
148
|
+
if prev:
|
|
149
|
+
if result["progress"] is None:
|
|
150
|
+
result["progress"] = prev.get("progress")
|
|
129
151
|
|
|
130
152
|
return result
|
|
131
153
|
|
|
132
154
|
|
|
133
|
-
def parse_magnetometer_progress(
|
|
155
|
+
def parse_magnetometer_progress(
|
|
156
|
+
progress_data,
|
|
157
|
+
prev: Optional[Dict[str, Any]] = None,
|
|
158
|
+
) -> Dict[str, Any]:
|
|
134
159
|
"""
|
|
135
160
|
Parse magnetometer calibration progress data into structured format.
|
|
136
161
|
|
|
@@ -185,29 +210,40 @@ def parse_magnetometer_progress(progress_data) -> Dict[str, Any]:
|
|
|
185
210
|
result["current_orientation"] = match.group(1).lower()
|
|
186
211
|
break
|
|
187
212
|
|
|
188
|
-
# Determine current message/instruction
|
|
189
|
-
|
|
213
|
+
# Determine current message/instruction (normalize)
|
|
214
|
+
low = status_text.lower()
|
|
215
|
+
if "rotate vehicle" in status_text:
|
|
190
216
|
result["current_message"] = "rotate"
|
|
191
|
-
elif "hold vehicle still" in
|
|
217
|
+
elif "hold vehicle still" in low:
|
|
192
218
|
result["current_message"] = "hold_still"
|
|
193
|
-
elif "detected rest position" in
|
|
219
|
+
elif "detected rest position" in low:
|
|
194
220
|
result["current_message"] = "rest_detected"
|
|
195
|
-
elif "detected motion" in
|
|
221
|
+
elif "detected motion" in low:
|
|
196
222
|
result["current_message"] = "motion_detected"
|
|
197
|
-
elif "done, rotate" in
|
|
223
|
+
elif "done, rotate" in low:
|
|
198
224
|
result["current_message"] = "rotate"
|
|
199
|
-
elif "already completed" in
|
|
225
|
+
elif "already completed" in low:
|
|
200
226
|
result["current_message"] = "already_completed"
|
|
201
|
-
elif "orientation detected" in
|
|
227
|
+
elif "orientation detected" in low:
|
|
202
228
|
result["current_message"] = "orientation_detected"
|
|
203
229
|
else:
|
|
204
230
|
# Default: use the status text as message
|
|
205
231
|
result["current_message"] = status_text
|
|
232
|
+
|
|
233
|
+
# Carry forward orientation / pending where appropriate
|
|
234
|
+
if prev:
|
|
235
|
+
if not result["pending_orientations"]:
|
|
236
|
+
result["pending_orientations"] = prev.get("pending_orientations", [])
|
|
237
|
+
if result["current_orientation"] is None:
|
|
238
|
+
result["current_orientation"] = prev.get("current_orientation")
|
|
206
239
|
|
|
207
240
|
return result
|
|
208
241
|
|
|
209
242
|
|
|
210
|
-
def parse_horizon_progress(
|
|
243
|
+
def parse_horizon_progress(
|
|
244
|
+
progress_data,
|
|
245
|
+
prev: Optional[Dict[str, Any]] = None,
|
|
246
|
+
) -> Dict[str, Any]:
|
|
211
247
|
"""
|
|
212
248
|
Parse level horizon calibration progress data into structured format.
|
|
213
249
|
|
|
@@ -243,6 +279,11 @@ def parse_horizon_progress(progress_data) -> Dict[str, Any]:
|
|
|
243
279
|
result["status_text"] = status_text
|
|
244
280
|
if status_text and not result["current_message"]:
|
|
245
281
|
result["current_message"] = status_text
|
|
282
|
+
|
|
283
|
+
# Carry forward simple state where helpful
|
|
284
|
+
if prev:
|
|
285
|
+
if result["progress"] is None:
|
|
286
|
+
result["progress"] = prev.get("progress")
|
|
246
287
|
|
|
247
288
|
return result
|
|
248
289
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: px4-configuration
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Tools and REST API for configuring PX4 from a companion computer
|
|
5
5
|
Author-email: Alex <bonnefond@fly4future.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
8
|
Requires-Dist: fastapi>=0.104.0
|
|
9
|
-
Requires-Dist: mavsdk==3.
|
|
9
|
+
Requires-Dist: mavsdk==3.10.2
|
|
10
10
|
Requires-Dist: python-multipart>=0.0.6
|
|
11
11
|
Requires-Dist: sse-starlette>=1.6.5
|
|
12
12
|
Requires-Dist: tqdm==4.67.1
|
|
@@ -60,7 +60,7 @@ pip install dist/px4_configuration-0.1.0-py3-none-any.whl
|
|
|
60
60
|
|
|
61
61
|
## Requirements
|
|
62
62
|
|
|
63
|
-
- Python 3.
|
|
63
|
+
- Python 3.10+
|
|
64
64
|
- PX4 flight controller connected over serial (e.g. `/dev/pixhawk`)
|
|
65
65
|
- User added to the `dialout` group for serial access (`sudo usermod -aG dialout $USER`)
|
|
66
66
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
px4_configuration/__init__.py,sha256=U0jqmbB8mDtIQ_ZPVjfHzoF7b9EM1TFJQRWEJ49Vk1I,334
|
|
2
|
+
px4_configuration/api/__init__.py,sha256=vQzGhWTEdxVccRcgICUNqCWF-1Ts7t_dGryDTnsGKCs,157
|
|
3
|
+
px4_configuration/api/config.py,sha256=onlTwFoMEcbNDRWK77xb_hMZqft_gAEgC8ZR1pPaYLE,920
|
|
4
|
+
px4_configuration/api/main.py,sha256=aeGpssDFcg9xwps3sdRM0tH2p2yYeqkGn9y7QLHw3I4,8278
|
|
5
|
+
px4_configuration/api/models.py,sha256=ltb6N-8g6prR-vNqj_OCh7U0AqqxNDGHZm6ksgWo3DA,6081
|
|
6
|
+
px4_configuration/api/services/__init__.py,sha256=U91RCGMiGcIQqtbnzscfNbWpFw65ZNz0DKabCPtTGMw,163
|
|
7
|
+
px4_configuration/api/services/calibration_service.py,sha256=W5ZpkokPMOjfK3Sf64JAFKiRdrc0QivCYIGfBedLNBc,8451
|
|
8
|
+
px4_configuration/api/services/health_service.py,sha256=WcZR3aioEFVQ1PwrHZYlR3bCKX3-2HdCBA6uPBUbavM,7299
|
|
9
|
+
px4_configuration/api/services/param_service.py,sha256=AUDNckhF05hCK7zOF8l9yI4erD76VWqpFZuglN-ejnQ,5977
|
|
10
|
+
px4_configuration/api/services/sdcard_service.py,sha256=UKZNR6n5c0Xrlpv2TKM2HmG6Rujwr3svZqJ6m050kMQ,3419
|
|
11
|
+
px4_configuration/api/services/shell_service.py,sha256=Df9Zprdu4s1tq2i0-uS83kR76We2f601ULZcLhmmQ9I,3167
|
|
12
|
+
px4_configuration/api/utils/__init__.py,sha256=KR-_U6nAXr8V1uxsB1hCFXPxKYYEwbf096kvhhUJfFk,54
|
|
13
|
+
px4_configuration/api/utils/calibration_parser.py,sha256=rlW4D-ogc0z8o80jBkazZ3oqucAvSMGp_vKfpp5bLp0,10990
|
|
14
|
+
px4_configuration-0.2.2.dist-info/METADATA,sha256=JPSgYEMPqn1HBaK2ts3U6PUbpaslJlbAxTTY9ZavqFM,7109
|
|
15
|
+
px4_configuration-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
px4_configuration-0.2.2.dist-info/entry_points.txt,sha256=mBRa68p2qL9HPLE9nCruYZDtTmuDOhFIbkY_nwX-WTc,67
|
|
17
|
+
px4_configuration-0.2.2.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
px4_configuration/__init__.py,sha256=U0jqmbB8mDtIQ_ZPVjfHzoF7b9EM1TFJQRWEJ49Vk1I,334
|
|
2
|
-
px4_configuration/api/__init__.py,sha256=o0txTrY6RjO1ehOJGtMerrVMm0QR1E4PiutPhoKtmZo,137
|
|
3
|
-
px4_configuration/api/config.py,sha256=imb6qv75ye_DE4t7kbf7xbxWPvy3Yota95keVjg1c5A,562
|
|
4
|
-
px4_configuration/api/main.py,sha256=xiOOVfWQB6pRhzA2rDJzFDrfGTnqAyQeIcQ3Hlzplyk,8323
|
|
5
|
-
px4_configuration/api/models.py,sha256=Wi0eWsfqd4noxSPgIN4yUnKGdoncBwkmlSvCs6HAsbE,3552
|
|
6
|
-
px4_configuration/api/services/__init__.py,sha256=U91RCGMiGcIQqtbnzscfNbWpFw65ZNz0DKabCPtTGMw,163
|
|
7
|
-
px4_configuration/api/services/calibration_service.py,sha256=yLlSd3ePe4zOrb4hdXdiwVRiZ5u1f-ACO1-eiyocFSU,4076
|
|
8
|
-
px4_configuration/api/services/health_service.py,sha256=3rNGRfy0GvwnQ_OMD4jU5qm-O2_YDOnfVg_AGS3Fcl4,4457
|
|
9
|
-
px4_configuration/api/services/param_service.py,sha256=95Ee5v3rEGjXiW2LJURTUHXqtxBl2JomV_tMcLwGjac,5201
|
|
10
|
-
px4_configuration/api/services/sdcard_service.py,sha256=6bnMTcaHddo8ZOsrKtl39V518MBO_Z-2fXFT4PTLT5M,2643
|
|
11
|
-
px4_configuration/api/services/shell_service.py,sha256=A_28vCxWyc6e-Usf3QLHqCy5Auf7tyGwqySQ5E5LO1s,2096
|
|
12
|
-
px4_configuration/api/utils/__init__.py,sha256=KR-_U6nAXr8V1uxsB1hCFXPxKYYEwbf096kvhhUJfFk,54
|
|
13
|
-
px4_configuration/api/utils/calibration_parser.py,sha256=ARn702mhz16gJV3gw0-HvFQmUSNCMbohwxTAtcs06GQ,9821
|
|
14
|
-
px4_configuration-0.2.0.dist-info/METADATA,sha256=MaxU3zpqE8nUdJgb892fbuMvrnqL1irMfyAs43fyYus,7106
|
|
15
|
-
px4_configuration-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
px4_configuration-0.2.0.dist-info/entry_points.txt,sha256=mBRa68p2qL9HPLE9nCruYZDtTmuDOhFIbkY_nwX-WTc,67
|
|
17
|
-
px4_configuration-0.2.0.dist-info/RECORD,,
|
|
File without changes
|