px4-configuration 0.2.0__py3-none-any.whl → 0.2.1__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.
@@ -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
3
+ from enum import Enum
4
4
  from typing import Literal, Optional, Dict, Any
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"
@@ -53,8 +53,10 @@ async def calibrate_sensors(
53
53
  "message": "Starting gyroscope calibration...",
54
54
  }
55
55
 
56
+ prev: Dict[str, Any] | None = None
56
57
  async for progress_data in drone.calibration.calibrate_gyro():
57
- structured_data = parse_gyroscope_progress(progress_data)
58
+ structured_data = parse_gyroscope_progress(progress_data, prev=prev)
59
+ prev = structured_data
58
60
  yield structured_data
59
61
 
60
62
  yield {
@@ -70,8 +72,10 @@ async def calibrate_sensors(
70
72
  "message": "Starting accelerometer calibration...",
71
73
  }
72
74
 
75
+ prev = None
73
76
  async for progress_data in drone.calibration.calibrate_accelerometer():
74
- structured_data = parse_accelerometer_progress(progress_data)
77
+ structured_data = parse_accelerometer_progress(progress_data, prev=prev)
78
+ prev = structured_data
75
79
  yield structured_data
76
80
 
77
81
  yield {
@@ -87,8 +91,10 @@ async def calibrate_sensors(
87
91
  "message": "Starting magnetometer calibration...",
88
92
  }
89
93
 
94
+ prev = None
90
95
  async for progress_data in drone.calibration.calibrate_magnetometer():
91
- structured_data = parse_magnetometer_progress(progress_data)
96
+ structured_data = parse_magnetometer_progress(progress_data, prev=prev)
97
+ prev = structured_data
92
98
  yield structured_data
93
99
 
94
100
  yield {
@@ -104,8 +110,10 @@ async def calibrate_sensors(
104
110
  "message": "Starting board level horizon calibration...",
105
111
  }
106
112
 
113
+ prev = None
107
114
  async for progress_data in drone.calibration.calibrate_level_horizon():
108
- structured_data = parse_horizon_progress(progress_data)
115
+ structured_data = parse_horizon_progress(progress_data, prev=prev)
116
+ prev = structured_data
109
117
  yield structured_data
110
118
 
111
119
  yield {
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from typing import Optional, Dict, Any
6
+ from typing import Optional, Dict, Any, Iterable
7
7
 
8
8
  from mavsdk import System
9
9
 
@@ -37,6 +37,42 @@ async def _get_gps_info_once(drone: System, timeout_s: float = 5.0):
37
37
  return await asyncio.wait_for(_first(), timeout_s)
38
38
 
39
39
 
40
+ SENSOR_INDEXES: Iterable[int] = (0, 1, 2)
41
+
42
+
43
+ async def _is_family_calibrated(
44
+ drone: System,
45
+ family: str,
46
+ indexes: Iterable[int] = SENSOR_INDEXES,
47
+ ) -> bool:
48
+ """
49
+ Param-based heuristic:
50
+
51
+ A sensor family (gyro/accel/mag) is considered calibrated if ANY
52
+ CAL_FAMILYn_ID != 0 for the probed indexes and the param read succeeds.
53
+ """
54
+ prefix_map = {
55
+ "gyro": "CAL_GYRO",
56
+ "accel": "CAL_ACC",
57
+ "mag": "CAL_MAG",
58
+ }
59
+ prefix = prefix_map.get(family)
60
+ if not prefix:
61
+ return False
62
+
63
+ for i in indexes:
64
+ name = f"{prefix}{i}_ID"
65
+ try:
66
+ sensor_id = await drone.param.get_param_int(name)
67
+ except Exception:
68
+ # Ignore this index if param read fails
69
+ continue
70
+ if sensor_id != 0:
71
+ return True
72
+
73
+ return False
74
+
75
+
40
76
  async def read_px4_health(
41
77
  port: Optional[str] = None,
42
78
  baudrate: Optional[int] = None,
@@ -45,7 +81,8 @@ async def read_px4_health(
45
81
  """
46
82
  Connect to PX4 via MAVSDK and return a one-shot health snapshot.
47
83
 
48
- - Uses telemetry.health() for calibration + armable flags.
84
+ - Uses PX4 CAL_* params to derive calibration health.
85
+ - Uses telemetry.health() for armable + positional flags.
49
86
  - Uses telemetry.gps_info() for fix type + satellites.
50
87
  - Returns Px4HealthResponse; never raises to the API layer.
51
88
  """
@@ -61,49 +98,105 @@ async def read_px4_health(
61
98
  # Connect to PX4
62
99
  await drone.connect(system_address=serial)
63
100
 
64
- # Get health & GPS within the global timeout
65
- health = await _get_health_once(drone, timeout_s=timeout_s / 2)
101
+ # Optional: verify we actually got a connection state
102
+ async for state in drone.core.connection_state():
103
+ if not state.is_connected:
104
+ # Treat as disconnected; don't bother with the rest
105
+ return Px4HealthResponse(
106
+ status="disconnected",
107
+ connection_port=port,
108
+ connection_baudrate=baudrate,
109
+ calibration=None,
110
+ gps_fix_type=None,
111
+ satellites_used=None,
112
+ armable=False,
113
+ raw_health=None,
114
+ )
115
+ break
116
+
117
+ # -------- PARAM-BASED CALIBRATION (source of truth) --------
118
+ gyroscope_ok = await _is_family_calibrated(drone, "gyro")
119
+ accelerometer_ok = await _is_family_calibrated(drone, "accel")
120
+ magnetometer_ok = await _is_family_calibrated(drone, "mag")
121
+
122
+ calibration = CalibrationHealth(
123
+ gyroscope_ok=gyroscope_ok,
124
+ accelerometer_ok=accelerometer_ok,
125
+ magnetometer_ok=magnetometer_ok,
126
+ )
127
+
128
+ # -------- TELEMETRY HEALTH (for armable + debug) --------
129
+ try:
130
+ health = await _get_health_once(drone, timeout_s=timeout_s / 2)
131
+ except asyncio.TimeoutError:
132
+ health = None
133
+
134
+ # -------- GPS INFO (unchanged) --------
66
135
  try:
67
136
  gps_info = await _get_gps_info_once(drone, timeout_s=timeout_s / 2)
68
137
  except asyncio.TimeoutError:
69
138
  gps_info = None
70
139
 
71
- # Build calibration health
72
- calibration = CalibrationHealth(
73
- gyroscope_ok=health.is_gyrometer_calibration_ok,
74
- accelerometer_ok=health.is_accelerometer_calibration_ok,
75
- magnetometer_ok=health.is_magnetometer_calibration_ok,
76
- )
77
-
78
- # GPS fields
79
140
  gps_fix_type: Optional[str] = None
80
141
  satellites_used: Optional[int] = None
81
142
 
82
143
  if gps_info is not None:
83
- # gps_info.fix_type is an enum; prefer its name
84
144
  if hasattr(gps_info.fix_type, "name"):
85
145
  gps_fix_type = gps_info.fix_type.name.lower()
86
146
  else:
87
147
  gps_fix_type = str(gps_info.fix_type).lower()
88
148
 
89
- # MAVSDK docs: num_satellites = number of visible satellites in use
90
149
  satellites_used = int(getattr(gps_info, "num_satellites", 0))
91
150
 
92
- # High-level status heuristic
93
- if calibration.requires_calibration:
94
- status = "degraded"
95
- else:
151
+ # -------- STATUS + ARMABLE (respect your Literal) --------
152
+ armable: Optional[bool] = None
153
+ if health is not None:
154
+ armable = health.is_armable
155
+
156
+ # Map everything into your fixed status vocabulary:
157
+ # - disconnected: handled in exception / early return
158
+ # - ok: armable and all calibrated
159
+ # - degraded: anything else (not armable or missing calibration)
160
+ if armable and not calibration.requires_calibration:
96
161
  status = "ok"
162
+ else:
163
+ status = "degraded"
97
164
 
98
- # Raw health flags (optional, but handy for debugging / advanced UI)
99
- raw_health: Dict[str, Any] = {
100
- "is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
101
- "is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
102
- "is_magnetometer_calibration_ok": health.is_magnetometer_calibration_ok,
103
- "is_local_position_ok": getattr(health, "is_local_position_ok", None),
104
- "is_global_position_ok": getattr(health, "is_global_position_ok", None),
105
- "is_home_position_ok": getattr(health, "is_home_position_ok", None),
106
- "is_armable": health.is_armable,
165
+ # -------- RAW HEALTH FOR DEBUG/UI --------
166
+ raw_health: Dict[str, Any] = {}
167
+
168
+ # Telemetry-derived bits (if available)
169
+ if health is not None:
170
+ raw_health.update(
171
+ {
172
+ "is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
173
+ "is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
174
+ "is_magnetometer_calibration_ok": health.is_magnetometer_calibration_ok,
175
+ "is_local_position_ok": getattr(health, "is_local_position_ok", None),
176
+ "is_global_position_ok": getattr(health, "is_global_position_ok", None),
177
+ "is_home_position_ok": getattr(health, "is_home_position_ok", None),
178
+ "is_armable": health.is_armable,
179
+ }
180
+ )
181
+ else:
182
+ raw_health.update(
183
+ {
184
+ "is_gyrometer_calibration_ok": None,
185
+ "is_accelerometer_calibration_ok": None,
186
+ "is_magnetometer_calibration_ok": None,
187
+ "is_local_position_ok": None,
188
+ "is_global_position_ok": None,
189
+ "is_home_position_ok": None,
190
+ "is_armable": None,
191
+ }
192
+ )
193
+
194
+ # Param-derived calibration snapshot as debug info (so you can see IDs in Swagger/UI later if you want)
195
+ raw_health["calibration_params"] = {
196
+ "source": "CAL_*_ID heuristic",
197
+ "gyroscope_ok": gyroscope_ok,
198
+ "accelerometer_ok": accelerometer_ok,
199
+ "magnetometer_ok": magnetometer_ok,
107
200
  }
108
201
 
109
202
  return Px4HealthResponse(
@@ -113,7 +206,7 @@ async def read_px4_health(
113
206
  calibration=calibration,
114
207
  gps_fix_type=gps_fix_type,
115
208
  satellites_used=satellites_used,
116
- armable=health.is_armable,
209
+ armable=armable,
117
210
  raw_health=raw_health,
118
211
  )
119
212
 
@@ -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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: px4-configuration
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
9
  Requires-Dist: mavsdk==3.0.1
10
10
  Requires-Dist: python-multipart>=0.0.6
@@ -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
 
@@ -1,17 +1,17 @@
1
1
  px4_configuration/__init__.py,sha256=U0jqmbB8mDtIQ_ZPVjfHzoF7b9EM1TFJQRWEJ49Vk1I,334
2
2
  px4_configuration/api/__init__.py,sha256=o0txTrY6RjO1ehOJGtMerrVMm0QR1E4PiutPhoKtmZo,137
3
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
4
+ px4_configuration/api/main.py,sha256=aeGpssDFcg9xwps3sdRM0tH2p2yYeqkGn9y7QLHw3I4,8278
5
+ px4_configuration/api/models.py,sha256=UeP_FOiimmBxJmeDZ6EGHTodRfQiBNOSt_hSfiVsglI,3546
6
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
7
+ px4_configuration/api/services/calibration_service.py,sha256=qA5NQx267djW853FKWrfGb6wTsCdqGGpTb3QN6j9zA4,4363
8
+ px4_configuration/api/services/health_service.py,sha256=T1pSMQLxpC41puxQIuTsFj5FVy5NAV0dtTkEoHfHvXE,7539
9
9
  px4_configuration/api/services/param_service.py,sha256=95Ee5v3rEGjXiW2LJURTUHXqtxBl2JomV_tMcLwGjac,5201
10
10
  px4_configuration/api/services/sdcard_service.py,sha256=6bnMTcaHddo8ZOsrKtl39V518MBO_Z-2fXFT4PTLT5M,2643
11
11
  px4_configuration/api/services/shell_service.py,sha256=A_28vCxWyc6e-Usf3QLHqCy5Auf7tyGwqySQ5E5LO1s,2096
12
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,,
13
+ px4_configuration/api/utils/calibration_parser.py,sha256=rlW4D-ogc0z8o80jBkazZ3oqucAvSMGp_vKfpp5bLp0,10990
14
+ px4_configuration-0.2.1.dist-info/METADATA,sha256=URkhx2PatvDNwcOVKWCyhqv60E6qDyAwnqGj2IaUQ3c,7108
15
+ px4_configuration-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ px4_configuration-0.2.1.dist-info/entry_points.txt,sha256=mBRa68p2qL9HPLE9nCruYZDtTmuDOhFIbkY_nwX-WTc,67
17
+ px4_configuration-0.2.1.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