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.
@@ -1,5 +1,7 @@
1
1
  """PX4 Configuration REST API package."""
2
2
 
3
+ from px4_configuration import __version__
4
+
3
5
  __all__ = [
4
6
  "config",
5
7
  "models",
@@ -7,5 +9,3 @@ __all__ = [
7
9
  "main",
8
10
  ]
9
11
 
10
- __version__ = "1.0.0"
11
-
@@ -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 = "1.0.0"
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
  )
@@ -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 f"data: {json.dumps(payload)}\n\n"
73
+ yield json.dumps(payload)
74
74
  except Exception as exc:
75
- yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
75
+ yield json.dumps({'type': 'error', 'message': str(exc)})
76
76
  finally:
77
- yield f"data: {json.dumps({'type': 'complete'})}\n\n"
77
+ yield json.dumps({'type': 'complete'})
78
78
 
79
79
  return EventSourceResponse(event_generator())
80
80
 
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import StrEnum
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(StrEnum):
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
- from typing import AsyncGenerator, Dict, Any
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 = System()
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
- elif calibration_type is CalibrationType.MAGNETOMETER:
84
- # Magnetometer calibration
77
+ try:
78
+ # Initial status messages
85
79
  yield {
86
80
  "type": "status",
87
- "message": "Starting magnetometer calibration...",
81
+ "message": f"Connecting to {port}...",
88
82
  }
89
83
 
90
- async for progress_data in drone.calibration.calibrate_magnetometer():
91
- structured_data = parse_magnetometer_progress(progress_data)
92
- yield structured_data
84
+ # Create and connect to the PX4 system
85
+ drone = System()
86
+ await drone.connect(system_address=serial)
93
87
 
94
- yield {
95
- "type": "magnetometer_complete",
96
- "message": "Magnetometer calibration finished",
97
- "is_complete": True,
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": "Starting board level horizon calibration...",
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
- elif calibration_type is CalibrationType.ESC:
118
- # ESC calibration not implemented yet
119
- yield {
120
- "type": "status",
121
- "message": "Starting ESC calibration...",
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": "warning",
125
- "message": "ESC calibration not yet implemented",
226
+ "type": "error",
227
+ "message": f"Calibration failed: {error_message}",
126
228
  }
127
- yield {
128
- "type": "status",
129
- "message": "ESC calibration finished",
130
- }
131
-
132
- # Final success message for the whole calibration run
133
- yield {
134
- "type": "success",
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 Optional, Dict, Any
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 Px4HealthResponse, CalibrationHealth
17
+ from ..models import (
18
+ Px4HealthResponse,
19
+ CalibrationHealth,
20
+ HealthAndArmingCheckReport,
21
+ HealthAndArmingCheckMode,
22
+ HealthAndArmingCheckProblem,
23
+ HealthComponentReport,
24
+ )
12
25
 
13
26
 
14
- async def _get_health_once(drone: System, timeout_s: float = 5.0):
27
+ async def _get_stable_health(drone: System, duration_s: float = 2.0) -> Optional[Any]:
15
28
  """
16
- Wait for the first telemetry.health() message.
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
- async def _first():
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
- return health
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 await asyncio.wait_for(_first(), timeout_s)
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 _get_gps_info_once(drone: System, timeout_s: float = 5.0):
93
+ async def _get_arming_checks_report(drone: System) -> Optional[HealthAndArmingCheckReport]:
28
94
  """
29
- Wait for the first telemetry.gps_info() message.
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
- 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)
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 = System()
118
+ drone: Optional[System] = None
59
119
 
60
120
  try:
61
- # Connect to PX4
121
+ drone = System()
62
122
  await drone.connect(system_address=serial)
63
123
 
64
- # Get health & GPS within the global timeout
65
- health = await _get_health_once(drone, timeout_s=timeout_s / 2)
124
+ # Wait for connection with timeout
125
+ connection_established = False
66
126
  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
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
- # 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()
172
+ # Determine status
173
+ status = "ok" if (health.is_armable and not calibration.requires_calibration) else "degraded"
88
174
 
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)
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=gps_fix_type,
115
- satellites_used=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 (asyncio.TimeoutError, Exception):
121
- # On any connection / telemetry failure, report "disconnected"
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: {exc}",
160
+ "message": f"Parameter upload failed: {error_msg}",
142
161
  }
143
162
  )
144
163
  )
145
164
  raise
146
165
  finally:
147
- if drone:
148
- # No explicit teardown required
149
- pass
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: {exc}",
99
+ "message": f"SD card upload failed: {error_msg}",
81
100
  }
82
101
  )
83
102
  )
84
103
  raise
85
104
  finally:
86
- if drone:
87
- # No explicit teardown required
88
- pass
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
- 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())
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
- await self.output_queue.put(f"Error receiving shell output: {exc}")
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
- await self.drone.shell.send(command)
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 if required."""
85
+ """Close the connection and cleanup resources."""
62
86
 
63
87
  self.connected = False
64
- # MAVSDK handles cleanup on garbage collection; nothing else required here.
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(progress_data) -> Dict[str, Any]:
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
- if "progress" in status_text.lower() and "<" in status_text and ">" in status_text:
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 "hold vehicle still" in status_text.lower():
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 status_text.lower():
82
+ elif "detected rest position" in low:
76
83
  result["current_message"] = "rest_detected"
77
- elif "measuring" in status_text.lower():
84
+ elif "measuring" in low:
78
85
  result["current_message"] = "measuring"
79
- elif "done, rotate" in status_text.lower():
86
+ elif "done, rotate" in low:
80
87
  result["current_message"] = "rotate"
81
- elif "already completed" in status_text.lower():
88
+ elif "already completed" in low:
82
89
  result["current_message"] = "already_completed"
83
- elif "orientation detected" in status_text.lower():
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(progress_data) -> Dict[str, Any]:
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(progress_data) -> Dict[str, Any]:
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
- if "Rotate vehicle" in status_text:
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 status_text.lower():
217
+ elif "hold vehicle still" in low:
192
218
  result["current_message"] = "hold_still"
193
- elif "detected rest position" in status_text.lower():
219
+ elif "detected rest position" in low:
194
220
  result["current_message"] = "rest_detected"
195
- elif "detected motion" in status_text.lower():
221
+ elif "detected motion" in low:
196
222
  result["current_message"] = "motion_detected"
197
- elif "done, rotate" in status_text.lower():
223
+ elif "done, rotate" in low:
198
224
  result["current_message"] = "rotate"
199
- elif "already completed" in status_text.lower():
225
+ elif "already completed" in low:
200
226
  result["current_message"] = "already_completed"
201
- elif "orientation detected" in status_text.lower():
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(progress_data) -> Dict[str, Any]:
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.0
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.8
7
+ Requires-Python: >=3.10
8
8
  Requires-Dist: fastapi>=0.104.0
9
- Requires-Dist: mavsdk==3.0.1
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.8+
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,