px4-configuration 0.1.0__tar.gz → 0.2.1__tar.gz
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-0.1.0 → px4_configuration-0.2.1}/PKG-INFO +12 -10
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/README.md +10 -8
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/main.py +8 -8
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/models.py +2 -2
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/calibration_service.py +12 -4
- px4_configuration-0.2.1/px4_configuration/api/services/health_service.py +224 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/utils/calibration_parser.py +62 -21
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/pyproject.toml +2 -2
- px4_configuration-0.1.0/px4_configuration/api/services/health_service.py +0 -131
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/.gitignore +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/configs/default-rc.params +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/configs/generic-quad-airframe.params +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/configs/mrs-generic.params +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/configs/platforms/x500.params +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/configs/sd-card/extras.txt +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/__init__.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/__init__.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/config.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/__init__.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/param_service.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/sdcard_service.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/shell_service.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/utils/__init__.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/requirements.txt +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/scripts/calibrate.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/scripts/px_uploader.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/scripts/set-params.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/scripts/shell.py +0 -0
- {px4_configuration-0.1.0 → px4_configuration-0.2.1}/scripts/upload-sdcard-config.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: px4-configuration
|
|
3
|
-
Version: 0.1
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -76,7 +76,7 @@ _All scripts use a serial connection to talk to PX4._
|
|
|
76
76
|
|
|
77
77
|
## REST API (FastAPI)
|
|
78
78
|
|
|
79
|
-
The companion computer can expose the same functionality through a REST API located in `px4_configuration
|
|
79
|
+
The companion computer can expose the same functionality through a REST API located in the `px4_configuration.api` package.
|
|
80
80
|
|
|
81
81
|
### Run the API server
|
|
82
82
|
|
|
@@ -93,20 +93,22 @@ uvicorn px4_configuration.api.main:app --host 0.0.0.0 --port 8000
|
|
|
93
93
|
|
|
94
94
|
### Endpoints
|
|
95
95
|
|
|
96
|
-
- `GET /
|
|
97
|
-
- `POST /
|
|
98
|
-
- `POST /
|
|
99
|
-
- `POST /
|
|
100
|
-
- `WS /
|
|
96
|
+
- `GET /health` – health check
|
|
97
|
+
- `POST /calibration/start` – start selected calibrations, returns Server-Sent Events (SSE) with progress
|
|
98
|
+
- `POST /params/upload` – upload a `.params` file (multipart form), SSE progress stream
|
|
99
|
+
- `POST /sdcard/upload` – upload a file (e.g. `extras.txt`) to the SD card, SSE progress stream
|
|
100
|
+
- `WS /shell/connect` – interactive PX4 shell over WebSocket
|
|
101
101
|
|
|
102
102
|
All endpoints accept optional `port` and `baudrate` (defaults `/dev/pixhawk`, `2000000`). SSE responses emit JSON payloads with keys such as `type`, `message`, `progress`, etc.
|
|
103
103
|
|
|
104
104
|
### Calibration Message Structure
|
|
105
105
|
|
|
106
|
-
The `/
|
|
106
|
+
The `/calibration/start` endpoint returns structured JSON messages via Server-Sent Events (SSE). Each calibration type has a specific message format:
|
|
107
107
|
|
|
108
108
|
#### Accelerometer Calibration
|
|
109
109
|
|
|
110
|
+
The `/calibration/start` endpoint returns structured JSON messages via Server-Sent Events (SSE). Each calibration type has a specific message format:
|
|
111
|
+
|
|
110
112
|
```json
|
|
111
113
|
{
|
|
112
114
|
"type": "accelerometer_progress",
|
|
@@ -45,7 +45,7 @@ pip install dist/px4_configuration-0.1.0-py3-none-any.whl
|
|
|
45
45
|
|
|
46
46
|
## Requirements
|
|
47
47
|
|
|
48
|
-
- Python 3.
|
|
48
|
+
- Python 3.10+
|
|
49
49
|
- PX4 flight controller connected over serial (e.g. `/dev/pixhawk`)
|
|
50
50
|
- User added to the `dialout` group for serial access (`sudo usermod -aG dialout $USER`)
|
|
51
51
|
|
|
@@ -61,7 +61,7 @@ _All scripts use a serial connection to talk to PX4._
|
|
|
61
61
|
|
|
62
62
|
## REST API (FastAPI)
|
|
63
63
|
|
|
64
|
-
The companion computer can expose the same functionality through a REST API located in `px4_configuration
|
|
64
|
+
The companion computer can expose the same functionality through a REST API located in the `px4_configuration.api` package.
|
|
65
65
|
|
|
66
66
|
### Run the API server
|
|
67
67
|
|
|
@@ -78,20 +78,22 @@ uvicorn px4_configuration.api.main:app --host 0.0.0.0 --port 8000
|
|
|
78
78
|
|
|
79
79
|
### Endpoints
|
|
80
80
|
|
|
81
|
-
- `GET /
|
|
82
|
-
- `POST /
|
|
83
|
-
- `POST /
|
|
84
|
-
- `POST /
|
|
85
|
-
- `WS /
|
|
81
|
+
- `GET /health` – health check
|
|
82
|
+
- `POST /calibration/start` – start selected calibrations, returns Server-Sent Events (SSE) with progress
|
|
83
|
+
- `POST /params/upload` – upload a `.params` file (multipart form), SSE progress stream
|
|
84
|
+
- `POST /sdcard/upload` – upload a file (e.g. `extras.txt`) to the SD card, SSE progress stream
|
|
85
|
+
- `WS /shell/connect` – interactive PX4 shell over WebSocket
|
|
86
86
|
|
|
87
87
|
All endpoints accept optional `port` and `baudrate` (defaults `/dev/pixhawk`, `2000000`). SSE responses emit JSON payloads with keys such as `type`, `message`, `progress`, etc.
|
|
88
88
|
|
|
89
89
|
### Calibration Message Structure
|
|
90
90
|
|
|
91
|
-
The `/
|
|
91
|
+
The `/calibration/start` endpoint returns structured JSON messages via Server-Sent Events (SSE). Each calibration type has a specific message format:
|
|
92
92
|
|
|
93
93
|
#### Accelerometer Calibration
|
|
94
94
|
|
|
95
|
+
The `/calibration/start` endpoint returns structured JSON messages via Server-Sent Events (SSE). Each calibration type has a specific message format:
|
|
96
|
+
|
|
95
97
|
```json
|
|
96
98
|
{
|
|
97
99
|
"type": "accelerometer_progress",
|
|
@@ -45,7 +45,7 @@ app.add_middleware(
|
|
|
45
45
|
active_shell_connections: dict[str, ShellConnection] = {}
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
@app.get("/
|
|
48
|
+
@app.get("/health", response_model=Px4HealthResponse)
|
|
49
49
|
async def get_px4_health(
|
|
50
50
|
port: Optional[str] = Query(None),
|
|
51
51
|
baudrate: Optional[int] = Query(None),
|
|
@@ -57,7 +57,7 @@ async def get_px4_health(
|
|
|
57
57
|
|
|
58
58
|
return await read_px4_health(port=port, baudrate=baudrate)
|
|
59
59
|
|
|
60
|
-
@app.post("/
|
|
60
|
+
@app.post("/calibration/start")
|
|
61
61
|
async def start_calibration(request: CalibrationRequest):
|
|
62
62
|
port = request.port or settings.default_port
|
|
63
63
|
baudrate = request.baudrate or settings.default_baudrate
|
|
@@ -70,16 +70,16 @@ 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
|
|
|
81
81
|
|
|
82
|
-
@app.post("/
|
|
82
|
+
@app.post("/params/upload")
|
|
83
83
|
async def upload_params(
|
|
84
84
|
file: UploadFile = File(...),
|
|
85
85
|
port: Optional[str] = Query(None, description="Serial port (default: /dev/pixhawk)"),
|
|
@@ -115,7 +115,7 @@ async def upload_params(
|
|
|
115
115
|
return EventSourceResponse(event_generator())
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
@app.post("/
|
|
118
|
+
@app.post("/sdcard/upload")
|
|
119
119
|
async def upload_sdcard_file(
|
|
120
120
|
file: UploadFile = File(...),
|
|
121
121
|
port: Optional[str] = Query(None, description="Serial port (default: /dev/pixhawk)"),
|
|
@@ -151,7 +151,7 @@ async def upload_sdcard_file(
|
|
|
151
151
|
return EventSourceResponse(event_generator())
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
@app.websocket("/
|
|
154
|
+
@app.websocket("/shell/connect")
|
|
155
155
|
async def shell_websocket(
|
|
156
156
|
websocket: WebSocket,
|
|
157
157
|
port: Optional[str] = Query(None, description="Serial port (default: /dev/pixhawk)"),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
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(
|
|
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 {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Service for PX4 health and calibration snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Optional, Dict, Any, Iterable
|
|
7
|
+
|
|
8
|
+
from mavsdk import System
|
|
9
|
+
|
|
10
|
+
from ..config import settings
|
|
11
|
+
from ..models import Px4HealthResponse, CalibrationHealth
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def _get_health_once(drone: System, timeout_s: float = 5.0):
|
|
15
|
+
"""
|
|
16
|
+
Wait for the first telemetry.health() message.
|
|
17
|
+
|
|
18
|
+
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
19
|
+
"""
|
|
20
|
+
async def _first():
|
|
21
|
+
async for health in drone.telemetry.health():
|
|
22
|
+
return health
|
|
23
|
+
|
|
24
|
+
return await asyncio.wait_for(_first(), timeout_s)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _get_gps_info_once(drone: System, timeout_s: float = 5.0):
|
|
28
|
+
"""
|
|
29
|
+
Wait for the first telemetry.gps_info() message.
|
|
30
|
+
|
|
31
|
+
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
32
|
+
"""
|
|
33
|
+
async def _first():
|
|
34
|
+
async for gps_info in drone.telemetry.gps_info():
|
|
35
|
+
return gps_info
|
|
36
|
+
|
|
37
|
+
return await asyncio.wait_for(_first(), timeout_s)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
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
|
+
|
|
76
|
+
async def read_px4_health(
|
|
77
|
+
port: Optional[str] = None,
|
|
78
|
+
baudrate: Optional[int] = None,
|
|
79
|
+
timeout_s: float = 10.0,
|
|
80
|
+
) -> Px4HealthResponse:
|
|
81
|
+
"""
|
|
82
|
+
Connect to PX4 via MAVSDK and return a one-shot health snapshot.
|
|
83
|
+
|
|
84
|
+
- Uses PX4 CAL_* params to derive calibration health.
|
|
85
|
+
- Uses telemetry.health() for armable + positional flags.
|
|
86
|
+
- Uses telemetry.gps_info() for fix type + satellites.
|
|
87
|
+
- Returns Px4HealthResponse; never raises to the API layer.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# Fallback to global settings if not provided explicitly
|
|
91
|
+
port = port or settings.default_port
|
|
92
|
+
baudrate = baudrate or settings.default_baudrate
|
|
93
|
+
serial = f"serial://{port}:{baudrate}"
|
|
94
|
+
|
|
95
|
+
drone = System()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Connect to PX4
|
|
99
|
+
await drone.connect(system_address=serial)
|
|
100
|
+
|
|
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) --------
|
|
135
|
+
try:
|
|
136
|
+
gps_info = await _get_gps_info_once(drone, timeout_s=timeout_s / 2)
|
|
137
|
+
except asyncio.TimeoutError:
|
|
138
|
+
gps_info = None
|
|
139
|
+
|
|
140
|
+
gps_fix_type: Optional[str] = None
|
|
141
|
+
satellites_used: Optional[int] = None
|
|
142
|
+
|
|
143
|
+
if gps_info is not None:
|
|
144
|
+
if hasattr(gps_info.fix_type, "name"):
|
|
145
|
+
gps_fix_type = gps_info.fix_type.name.lower()
|
|
146
|
+
else:
|
|
147
|
+
gps_fix_type = str(gps_info.fix_type).lower()
|
|
148
|
+
|
|
149
|
+
satellites_used = int(getattr(gps_info, "num_satellites", 0))
|
|
150
|
+
|
|
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:
|
|
161
|
+
status = "ok"
|
|
162
|
+
else:
|
|
163
|
+
status = "degraded"
|
|
164
|
+
|
|
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,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Px4HealthResponse(
|
|
203
|
+
status=status,
|
|
204
|
+
connection_port=port,
|
|
205
|
+
connection_baudrate=baudrate,
|
|
206
|
+
calibration=calibration,
|
|
207
|
+
gps_fix_type=gps_fix_type,
|
|
208
|
+
satellites_used=satellites_used,
|
|
209
|
+
armable=armable,
|
|
210
|
+
raw_health=raw_health,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
except (asyncio.TimeoutError, Exception):
|
|
214
|
+
# On any connection / telemetry failure, report "disconnected"
|
|
215
|
+
return Px4HealthResponse(
|
|
216
|
+
status="disconnected",
|
|
217
|
+
connection_port=port,
|
|
218
|
+
connection_baudrate=baudrate,
|
|
219
|
+
calibration=None,
|
|
220
|
+
gps_fix_type=None,
|
|
221
|
+
satellites_used=None,
|
|
222
|
+
armable=False,
|
|
223
|
+
raw_health=None,
|
|
224
|
+
)
|
|
@@ -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
|
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "px4-configuration"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Tools and REST API for configuring PX4 from a companion computer"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "BSD-3-Clause" }
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Alex", email = "bonnefond@fly4future.com" }
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Service for PX4 health and calibration snapshot."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from typing import Optional, Dict, Any
|
|
7
|
-
|
|
8
|
-
from mavsdk import System
|
|
9
|
-
|
|
10
|
-
from ..config import settings
|
|
11
|
-
from ..models import Px4HealthResponse, CalibrationHealth
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def _get_health_once(drone: System, timeout_s: float = 5.0):
|
|
15
|
-
"""
|
|
16
|
-
Wait for the first telemetry.health() message.
|
|
17
|
-
|
|
18
|
-
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
19
|
-
"""
|
|
20
|
-
async def _first():
|
|
21
|
-
async for health in drone.telemetry.health():
|
|
22
|
-
return health
|
|
23
|
-
|
|
24
|
-
return await asyncio.wait_for(_first(), timeout_s)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def _get_gps_info_once(drone: System, timeout_s: float = 5.0):
|
|
28
|
-
"""
|
|
29
|
-
Wait for the first telemetry.gps_info() message.
|
|
30
|
-
|
|
31
|
-
Raises asyncio.TimeoutError if nothing is received within timeout_s.
|
|
32
|
-
"""
|
|
33
|
-
async def _first():
|
|
34
|
-
async for gps_info in drone.telemetry.gps_info():
|
|
35
|
-
return gps_info
|
|
36
|
-
|
|
37
|
-
return await asyncio.wait_for(_first(), timeout_s)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
async def read_px4_health(
|
|
41
|
-
port: Optional[str] = None,
|
|
42
|
-
baudrate: Optional[int] = None,
|
|
43
|
-
timeout_s: float = 10.0,
|
|
44
|
-
) -> Px4HealthResponse:
|
|
45
|
-
"""
|
|
46
|
-
Connect to PX4 via MAVSDK and return a one-shot health snapshot.
|
|
47
|
-
|
|
48
|
-
- Uses telemetry.health() for calibration + armable flags.
|
|
49
|
-
- Uses telemetry.gps_info() for fix type + satellites.
|
|
50
|
-
- Returns Px4HealthResponse; never raises to the API layer.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
# Fallback to global settings if not provided explicitly
|
|
54
|
-
port = port or settings.default_port
|
|
55
|
-
baudrate = baudrate or settings.default_baudrate
|
|
56
|
-
serial = f"serial://{port}:{baudrate}"
|
|
57
|
-
|
|
58
|
-
drone = System()
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
# Connect to PX4
|
|
62
|
-
await drone.connect(system_address=serial)
|
|
63
|
-
|
|
64
|
-
# Get health & GPS within the global timeout
|
|
65
|
-
health = await _get_health_once(drone, timeout_s=timeout_s / 2)
|
|
66
|
-
try:
|
|
67
|
-
gps_info = await _get_gps_info_once(drone, timeout_s=timeout_s / 2)
|
|
68
|
-
except asyncio.TimeoutError:
|
|
69
|
-
gps_info = None
|
|
70
|
-
|
|
71
|
-
# Build calibration health
|
|
72
|
-
calibration = CalibrationHealth(
|
|
73
|
-
gyroscope_ok=health.is_gyrometer_calibration_ok,
|
|
74
|
-
accelerometer_ok=health.is_accelerometer_calibration_ok,
|
|
75
|
-
magnetometer_ok=health.is_magnetometer_calibration_ok,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# GPS fields
|
|
79
|
-
gps_fix_type: Optional[str] = None
|
|
80
|
-
satellites_used: Optional[int] = None
|
|
81
|
-
|
|
82
|
-
if gps_info is not None:
|
|
83
|
-
# gps_info.fix_type is an enum; prefer its name
|
|
84
|
-
if hasattr(gps_info.fix_type, "name"):
|
|
85
|
-
gps_fix_type = gps_info.fix_type.name.lower()
|
|
86
|
-
else:
|
|
87
|
-
gps_fix_type = str(gps_info.fix_type).lower()
|
|
88
|
-
|
|
89
|
-
# MAVSDK docs: num_satellites = number of visible satellites in use
|
|
90
|
-
satellites_used = int(getattr(gps_info, "num_satellites", 0))
|
|
91
|
-
|
|
92
|
-
# High-level status heuristic
|
|
93
|
-
if calibration.requires_calibration:
|
|
94
|
-
status = "degraded"
|
|
95
|
-
else:
|
|
96
|
-
status = "ok"
|
|
97
|
-
|
|
98
|
-
# Raw health flags (optional, but handy for debugging / advanced UI)
|
|
99
|
-
raw_health: Dict[str, Any] = {
|
|
100
|
-
"is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
|
|
101
|
-
"is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
|
|
102
|
-
"is_magnetometer_calibration_ok": health.is_magnetometer_calibration_ok,
|
|
103
|
-
"is_local_position_ok": getattr(health, "is_local_position_ok", None),
|
|
104
|
-
"is_global_position_ok": getattr(health, "is_global_position_ok", None),
|
|
105
|
-
"is_home_position_ok": getattr(health, "is_home_position_ok", None),
|
|
106
|
-
"is_armable": health.is_armable,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return Px4HealthResponse(
|
|
110
|
-
status=status,
|
|
111
|
-
connection_port=port,
|
|
112
|
-
connection_baudrate=baudrate,
|
|
113
|
-
calibration=calibration,
|
|
114
|
-
gps_fix_type=gps_fix_type,
|
|
115
|
-
satellites_used=satellites_used,
|
|
116
|
-
armable=health.is_armable,
|
|
117
|
-
raw_health=raw_health,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
except (asyncio.TimeoutError, Exception):
|
|
121
|
-
# On any connection / telemetry failure, report "disconnected"
|
|
122
|
-
return Px4HealthResponse(
|
|
123
|
-
status="disconnected",
|
|
124
|
-
connection_port=port,
|
|
125
|
-
connection_baudrate=baudrate,
|
|
126
|
-
calibration=None,
|
|
127
|
-
gps_fix_type=None,
|
|
128
|
-
satellites_used=None,
|
|
129
|
-
armable=False,
|
|
130
|
-
raw_health=None,
|
|
131
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/__init__.py
RENAMED
|
File without changes
|
{px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/param_service.py
RENAMED
|
File without changes
|
{px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/sdcard_service.py
RENAMED
|
File without changes
|
{px4_configuration-0.1.0 → px4_configuration-0.2.1}/px4_configuration/api/services/shell_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|