petal-user-journey-coordinator 0.1.5__tar.gz → 0.1.6__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.
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/PKG-INFO +1 -1
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/pyproject.toml +2 -2
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/data_model.py +104 -24
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/plugin.py +582 -93
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/README.md +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/__init__.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/controllers.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/__init__.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/dummy_trajectory_test.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/fix_expected_results.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/generate_trajectory_test_data.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/generated_trajectory_test_data.json +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_generated_trajectories.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_petal_user_journey_coordinator.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_plotting.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_trajectory_simple.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_trajectory_verification.py +0 -0
- {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/trajectory_test_data.json +0 -0
{petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "petal-user-journey-coordinator"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.6"
|
|
4
4
|
description = "A petal for the DroneLeaf ecosystem"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Khalil Al Handawi", email = "khalil.alhandawi@droneleaf.io" },
|
|
@@ -41,6 +41,6 @@ test = [
|
|
|
41
41
|
"pytest-asyncio>=1.0.0",
|
|
42
42
|
"anyio>=4.9.0",
|
|
43
43
|
"pytest-cov>=6.2.1",
|
|
44
|
-
"petal-app-manager
|
|
44
|
+
"petal-app-manager>=0.1.50",
|
|
45
45
|
"leaf-pymavlink>=0.1.11",
|
|
46
46
|
]
|
|
@@ -1,57 +1,59 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator, ValidationError
|
|
2
|
-
from typing import Dict, Any, List, Union, Optional, Callable
|
|
2
|
+
from typing import Dict, Any, List, Literal, Union, Optional, Callable
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
class ParameterBaseModel(BaseModel):
|
|
7
6
|
"""Base fields for flight records"""
|
|
8
7
|
parameter_name: str = Field(..., description="Parameter name")
|
|
9
8
|
parameter_value: Union[str,int,float] = Field(..., description="Value of the parameter")
|
|
9
|
+
parameter_type: Optional[Literal['UINT8','INT8','UINT16','INT16','UINT32','INT32','UINT64','INT64','REAL32','REAL64']] = Field(default=None, description="Type of the parameter")
|
|
10
10
|
|
|
11
11
|
model_config = {
|
|
12
12
|
"json_schema_extra": {
|
|
13
13
|
"example": {
|
|
14
14
|
"parameter_name": "CA_ROTOR_COUNT",
|
|
15
|
-
"parameter_value": 4
|
|
15
|
+
"parameter_value": 4,
|
|
16
|
+
"parameter_type": "UINT8"
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class
|
|
22
|
-
"""
|
|
23
|
-
|
|
22
|
+
class ParameterResult(BaseModel):
|
|
23
|
+
"""Individual parameter result"""
|
|
24
|
+
name: str = Field(..., description="Name of the parameter")
|
|
25
|
+
value: Optional[Union[str,int,float]] = Field(None, description="Decoded value of the parameter")
|
|
26
|
+
raw: Optional[Union[str,int,float]] = Field(None, description="Raw value of the parameter")
|
|
27
|
+
type: Optional[int] = Field(None, description="Type of the parameter")
|
|
28
|
+
count: Optional[int] = Field(None, description="Total number of parameters")
|
|
29
|
+
index: Optional[int] = Field(None, description="Index of the parameter")
|
|
30
|
+
error: Optional[str] = Field(default=None, description="Error message if setting the parameter failed")
|
|
31
|
+
success: Optional[bool] = Field(default=None, description="Whether setting the parameter was successful")
|
|
24
32
|
|
|
25
33
|
model_config = {
|
|
26
34
|
"json_schema_extra": {
|
|
27
35
|
"example": {
|
|
28
|
-
"
|
|
36
|
+
"name": "CA_ROTOR_COUNT",
|
|
37
|
+
"value": 4,
|
|
38
|
+
"raw": 4.0,
|
|
39
|
+
"type": 6,
|
|
40
|
+
"count": 1053,
|
|
41
|
+
"index": 65535,
|
|
42
|
+
"error": None,
|
|
43
|
+
"success": True
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
|
|
34
|
-
class
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
messageId: str = Field(..., description="Unique message ID")
|
|
38
|
-
deviceId: str = Field(..., description="Device ID")
|
|
39
|
-
command: str = Field(..., description="Command to execute")
|
|
40
|
-
timestamp: str = Field(..., description="Timestamp of the message")
|
|
41
|
-
payload: Dict[str, Any] = Field(..., description="Message payload")
|
|
49
|
+
class ParameterRequestModel(BaseModel):
|
|
50
|
+
"""Base fields for flight records"""
|
|
51
|
+
parameter_name: str = Field(..., description="Parameter name")
|
|
42
52
|
|
|
43
53
|
model_config = {
|
|
44
54
|
"json_schema_extra": {
|
|
45
55
|
"example": {
|
|
46
|
-
"
|
|
47
|
-
"messageId": "kkkss8fepn-1756665973142-bptyoj06z",
|
|
48
|
-
"deviceId": "Instance-a92c5505-ccdb-4ac7-b0fe-74f4fa5fc5b9",
|
|
49
|
-
"command": "Update",
|
|
50
|
-
"payload": {
|
|
51
|
-
"source": "web-client",
|
|
52
|
-
"app": "leaf-fc"
|
|
53
|
-
},
|
|
54
|
-
"timestamp": "2025-08-31T18:46:13.142Z"
|
|
56
|
+
"parameter_name": "CA_ROTOR_COUNT"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -554,3 +556,81 @@ class SetStaticIpAddressResponse(BaseModel):
|
|
|
554
556
|
}
|
|
555
557
|
}
|
|
556
558
|
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class BulkParameterSetRequest(BaseModel):
|
|
562
|
+
"""Request model for bulk parameter setting"""
|
|
563
|
+
parameters: List[ParameterBaseModel] = Field(..., description="List of parameters to set")
|
|
564
|
+
|
|
565
|
+
model_config = {
|
|
566
|
+
"json_schema_extra": {
|
|
567
|
+
"example": {
|
|
568
|
+
"parameters": [
|
|
569
|
+
{
|
|
570
|
+
"parameter_name": "CA_ROTOR_COUNT",
|
|
571
|
+
"parameter_value": 4,
|
|
572
|
+
"parameter_type": "UINT8"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"parameter_name": "VTO_LOITER_ALT",
|
|
576
|
+
"parameter_value": 80.0,
|
|
577
|
+
"parameter_type": "REAL32"
|
|
578
|
+
}
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class BulkParameterGetRequest(BaseModel):
|
|
586
|
+
"""Request model for bulk parameter getting"""
|
|
587
|
+
parameter_names: List[str] = Field(..., description="List of parameter names to get")
|
|
588
|
+
|
|
589
|
+
model_config = {
|
|
590
|
+
"json_schema_extra": {
|
|
591
|
+
"example": {
|
|
592
|
+
"parameter_names": [
|
|
593
|
+
"CA_ROTOR_COUNT",
|
|
594
|
+
"VTO_LOITER_ALT"
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class BulkParameterResponse(BaseModel):
|
|
602
|
+
"""Response model for bulk parameter setting"""
|
|
603
|
+
success: bool = Field(..., description="Whether all parameters were set successfully")
|
|
604
|
+
results: Dict[str, ParameterResult] = Field(..., description="Results for each parameter set attempt")
|
|
605
|
+
timestamp: str = Field(..., description="Timestamp of the operation")
|
|
606
|
+
|
|
607
|
+
model_config = {
|
|
608
|
+
"json_schema_extra": {
|
|
609
|
+
"example": {
|
|
610
|
+
"success": True,
|
|
611
|
+
"results": {
|
|
612
|
+
"CA_ROTOR_COUNT": {
|
|
613
|
+
"name": "CA_ROTOR_COUNT",
|
|
614
|
+
"value": 4,
|
|
615
|
+
"raw": 4.0,
|
|
616
|
+
"type": 6,
|
|
617
|
+
"count": 1053,
|
|
618
|
+
"index": 65535,
|
|
619
|
+
"error": None,
|
|
620
|
+
"success": True
|
|
621
|
+
},
|
|
622
|
+
"VTO_LOITER_ALT": {
|
|
623
|
+
"name": "VTO_LOITER_ALT",
|
|
624
|
+
"value": 80.0,
|
|
625
|
+
"raw": 80.0,
|
|
626
|
+
"type": 9,
|
|
627
|
+
"count": 1053,
|
|
628
|
+
"index": 1047,
|
|
629
|
+
"error": None,
|
|
630
|
+
"success": True
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
"timestamp": "2023-01-01T00:00:00Z"
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
import math
|
|
7
7
|
import numpy as np
|
|
8
8
|
from typing import Dict, Any, List, Union, Optional, Callable
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
10
|
import threading
|
|
11
11
|
from enum import Enum
|
|
12
12
|
import time
|
|
@@ -21,6 +21,7 @@ from petal_app_manager.proxies import (
|
|
|
21
21
|
RedisProxy
|
|
22
22
|
)
|
|
23
23
|
from petal_app_manager import Config
|
|
24
|
+
from petal_app_manager.models import MQTTMessage
|
|
24
25
|
|
|
25
26
|
import json, math
|
|
26
27
|
from pymavlink import mavutil
|
|
@@ -66,7 +67,7 @@ from .controllers import (
|
|
|
66
67
|
|
|
67
68
|
from .data_model import (
|
|
68
69
|
# generic payloads
|
|
69
|
-
|
|
70
|
+
BulkParameterSetRequest,
|
|
70
71
|
SubscribePayload,
|
|
71
72
|
UnsubscribePayload,
|
|
72
73
|
|
|
@@ -89,9 +90,15 @@ from .data_model import (
|
|
|
89
90
|
MavlinkParametersResponseModel,
|
|
90
91
|
RotorCountParameter,
|
|
91
92
|
DistanceModulePayload,
|
|
92
|
-
OpticalFlowModulePayload
|
|
93
|
+
OpticalFlowModulePayload,
|
|
94
|
+
BulkParameterSetRequest,
|
|
95
|
+
BulkParameterGetRequest,
|
|
96
|
+
BulkParameterResponse
|
|
93
97
|
)
|
|
94
98
|
|
|
99
|
+
from petal_app_manager.models.mavlink import (
|
|
100
|
+
RebootAutopilotResponse
|
|
101
|
+
)
|
|
95
102
|
|
|
96
103
|
class OperationMode(Enum):
|
|
97
104
|
"""Enumeration of operation modes"""
|
|
@@ -267,7 +274,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
267
274
|
self._active_handlers[stream_name] = {
|
|
268
275
|
"stream_id": stream_id,
|
|
269
276
|
"rate_hz": rate_hz,
|
|
270
|
-
"started_at": datetime.now().isoformat(),
|
|
277
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
271
278
|
"controller": self._pubsub_controllers.get(stream_name)
|
|
272
279
|
}
|
|
273
280
|
logger.info(f"Tracking subscription for {stream_name} (ID: {stream_id}, Rate: {rate_hz} Hz)")
|
|
@@ -302,7 +309,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
302
309
|
"status": "success",
|
|
303
310
|
"message": "No active handlers to unregister",
|
|
304
311
|
"unsubscribed_streams": [],
|
|
305
|
-
"timestamp": datetime.now().isoformat()
|
|
312
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
unsubscribed_streams = []
|
|
@@ -336,7 +343,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
336
343
|
"status": "success" if not failed_streams else "partial_success",
|
|
337
344
|
"message": f"Unsubscribed from {len(unsubscribed_streams)} streams",
|
|
338
345
|
"unsubscribed_streams": unsubscribed_streams,
|
|
339
|
-
"timestamp": datetime.now().isoformat()
|
|
346
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
340
347
|
}
|
|
341
348
|
|
|
342
349
|
if failed_streams:
|
|
@@ -427,6 +434,8 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
427
434
|
"petal-user-journey-coordinator/distance_spatial_offset": self._distance_spatial_offset_message_handler,
|
|
428
435
|
"petal-user-journey-coordinator/optical_flow_spatial_offset": self._optical_flow_spatial_offset_message_handler,
|
|
429
436
|
"petal-user-journey-coordinator/esc_update_calibration_limits": self._esc_update_calibration_limits_message_handler,
|
|
437
|
+
"petal-user-journey-coordinator/bulk_set_parameters": self._bulk_set_parameter_message_handler,
|
|
438
|
+
"petal-user-journey-coordinator/bulk_get_parameters": self._bulk_get_parameter_message_handler,
|
|
430
439
|
|
|
431
440
|
# Pub/Sub stream commands
|
|
432
441
|
"petal-user-journey-coordinator/subscribe_rc_value_stream": self._subscribe_rc_value_stream_handler,
|
|
@@ -448,6 +457,9 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
448
457
|
# WiFi OptiTrack connectivity commands
|
|
449
458
|
"petal-user-journey-coordinator/connect_to_wifi_and_verify_optitrack": self._connect_to_wifi_and_verify_optitrack_handler,
|
|
450
459
|
"petal-user-journey-coordinator/set_static_ip_address": self._set_static_ip_address_handler,
|
|
460
|
+
|
|
461
|
+
# Reboot command
|
|
462
|
+
"petal-user-journey-coordinator/reboot_autopilot": self._reboot_px4_message_handler
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
async def _master_command_handler(self, topic: str, message: Dict[str, Any]):
|
|
@@ -518,13 +530,9 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
518
530
|
except Exception as e:
|
|
519
531
|
logger.error(f"Failed to send error response: {e}")
|
|
520
532
|
|
|
533
|
+
@http_action(method="POST", path="/test/esc-calibration")
|
|
521
534
|
async def _test_esc_calibration_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
522
535
|
"""Test handler for ESC calibration with enhanced workflow."""
|
|
523
|
-
# allow only one call
|
|
524
|
-
if getattr(self, "_test_esc_calibration_called", False):
|
|
525
|
-
logger.warning("ESC calibration test has already been called.")
|
|
526
|
-
return
|
|
527
|
-
self._test_esc_calibration_called = True
|
|
528
536
|
|
|
529
537
|
# Test Step 1: Initialize and configure ESC calibration
|
|
530
538
|
test_payload = {
|
|
@@ -583,6 +591,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
583
591
|
|
|
584
592
|
logger.info("✅ ESC calibration test sequence completed!")
|
|
585
593
|
|
|
594
|
+
@http_action(method="POST", path="/test/geometry")
|
|
586
595
|
async def _test_geometry_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
587
596
|
# intercept payload
|
|
588
597
|
test_payload = {
|
|
@@ -592,6 +601,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
592
601
|
# Use the dynamically created handler directly
|
|
593
602
|
await self._rotor_count_message_handler(topic, message)
|
|
594
603
|
|
|
604
|
+
@http_action(method="POST", path="/test/dist-module")
|
|
595
605
|
async def _test_dist_module_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
596
606
|
"""Test handler for distance module configuration."""
|
|
597
607
|
# Test with LiDAR Lite v3
|
|
@@ -601,6 +611,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
601
611
|
message["payload"] = test_payload
|
|
602
612
|
await self._dist_module_message_handler(topic, message)
|
|
603
613
|
|
|
614
|
+
@http_action(method="POST", path="/test/oflow-module")
|
|
604
615
|
async def _test_oflow_module_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
605
616
|
"""Test handler for optical flow module configuration."""
|
|
606
617
|
# Test with ARK Flow
|
|
@@ -610,6 +621,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
610
621
|
message["payload"] = test_payload
|
|
611
622
|
await self._oflow_module_message_handler(topic, message)
|
|
612
623
|
|
|
624
|
+
@http_action(method="POST", path="/test/subscribe-rc-value-stream")
|
|
613
625
|
async def _test_subscribe_rc_value_stream_handler(self, topic: str, message: Dict[str, Any]):
|
|
614
626
|
# intercept payload
|
|
615
627
|
test_payload = {
|
|
@@ -630,6 +642,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
630
642
|
|
|
631
643
|
await self._unsubscribe_rc_value_stream_handler(topic, message)
|
|
632
644
|
|
|
645
|
+
@http_action(method="POST", path="/test/subscribe-real-time-pose")
|
|
633
646
|
async def _test_subscribe_real_time_pose_handler(self, topic: str, message: Dict[str, Any]):
|
|
634
647
|
# intercept payload
|
|
635
648
|
test_payload = {
|
|
@@ -650,6 +663,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
650
663
|
|
|
651
664
|
await self._unsubscribe_real_time_pose_handler(topic, message)
|
|
652
665
|
|
|
666
|
+
@http_action(method="POST", path="/test/kill-switch-stream")
|
|
653
667
|
async def _test_kill_switch_stream_handler(self, topic: str, message: Dict[str, Any]):
|
|
654
668
|
"""Test handler for kill switch stream."""
|
|
655
669
|
logger.info("Running kill switch stream test")
|
|
@@ -675,6 +689,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
675
689
|
|
|
676
690
|
logger.info("Kill switch stream test completed")
|
|
677
691
|
|
|
692
|
+
@http_action(method="POST", path="/test/mfs-a-stream")
|
|
678
693
|
async def _test_mfs_a_stream_handler(self, topic: str, message: Dict[str, Any]):
|
|
679
694
|
"""Test handler for Multi-functional Switch A stream."""
|
|
680
695
|
logger.info("Running Multi-functional Switch A stream test")
|
|
@@ -700,6 +715,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
700
715
|
|
|
701
716
|
logger.info("Multi-functional Switch A stream test completed")
|
|
702
717
|
|
|
718
|
+
@http_action(method="POST", path="/test/mfs-b-stream")
|
|
703
719
|
async def _test_mfs_b_stream_handler(self, topic: str, message: Dict[str, Any]):
|
|
704
720
|
"""Test handler for Multi-functional Switch B stream."""
|
|
705
721
|
logger.info("Running Multi-functional Switch B stream test")
|
|
@@ -725,14 +741,10 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
725
741
|
|
|
726
742
|
logger.info("Multi-functional Switch B stream test completed")
|
|
727
743
|
|
|
744
|
+
@http_action(method="POST", path="/test/verify-pos-yaw-directions")
|
|
728
745
|
async def _test_verify_pos_yaw_directions_handler(self, topic: str, message: Dict[str, Any]):
|
|
729
746
|
"""Test handler for trajectory verification with the new command structure."""
|
|
730
747
|
|
|
731
|
-
if getattr(self, "_test_verification_trajectory", False):
|
|
732
|
-
logger.warning("Trajectory verification test has already been called.")
|
|
733
|
-
return
|
|
734
|
-
self._test_verification_trajectory = True
|
|
735
|
-
|
|
736
748
|
logger.info("Running trajectory verification test with new command structure")
|
|
737
749
|
|
|
738
750
|
# Configure trajectory collection rate (optional - demonstrates the feature)
|
|
@@ -745,7 +757,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
745
757
|
"messageId": f"test-pose-subscribe-{datetime.now().timestamp()}",
|
|
746
758
|
"deviceId": message.get("deviceId", "test-device"),
|
|
747
759
|
"command": "petal-user-journey-coordinator/subscribe_pose_value_stream",
|
|
748
|
-
"timestamp": datetime.now().isoformat(),
|
|
760
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
749
761
|
"payload": {
|
|
750
762
|
"subscribed_stream_id": "real_time_pose",
|
|
751
763
|
"data_rate_hz": 10.0
|
|
@@ -762,7 +774,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
762
774
|
"messageId": f"test-verify-start-{datetime.now().timestamp()}",
|
|
763
775
|
"deviceId": message.get("deviceId", "test-device"),
|
|
764
776
|
"command": "petal-user-journey-coordinator/verify_pos_yaw_directions",
|
|
765
|
-
"timestamp": datetime.now().isoformat(),
|
|
777
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
766
778
|
"payload": {
|
|
767
779
|
"start": True
|
|
768
780
|
}
|
|
@@ -779,7 +791,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
779
791
|
"messageId": f"test-verify-complete-{datetime.now().timestamp()}",
|
|
780
792
|
"deviceId": message.get("deviceId", "test-device"),
|
|
781
793
|
"command": "petal-user-journey-coordinator/verify_pos_yaw_directions_complete",
|
|
782
|
-
"timestamp": datetime.now().isoformat(),
|
|
794
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
783
795
|
"payload": {}
|
|
784
796
|
}
|
|
785
797
|
await self._master_command_handler(topic, verify_complete_message)
|
|
@@ -790,7 +802,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
790
802
|
"messageId": f"test-pose-unsubscribe-{datetime.now().timestamp()}",
|
|
791
803
|
"deviceId": message.get("deviceId", "test-device"),
|
|
792
804
|
"command": "petal-user-journey-coordinator/unsubscribe_pose_value_stream",
|
|
793
|
-
"timestamp": datetime.now().isoformat(),
|
|
805
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
794
806
|
"payload": {
|
|
795
807
|
"unsubscribed_stream_id": "real_time_pose"
|
|
796
808
|
}
|
|
@@ -799,14 +811,9 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
799
811
|
|
|
800
812
|
logger.info("Trajectory verification test completed")
|
|
801
813
|
|
|
814
|
+
@http_action(method="POST", path="/test/connect-to-wifi-and-verify-optitrack")
|
|
802
815
|
async def _test_connect_to_wifi_and_verify_optitrack_handler(self, topic: str, message: Dict[str, Any]):
|
|
803
|
-
"""Test handler for WiFi and OptiTrack connectivity verification."""
|
|
804
|
-
# allow only one call
|
|
805
|
-
if getattr(self, "_test_wifi", False):
|
|
806
|
-
logger.warning("Wifi test has already been called.")
|
|
807
|
-
return
|
|
808
|
-
self._test_wifi = True
|
|
809
|
-
|
|
816
|
+
"""Test handler for WiFi and OptiTrack connectivity verification."""
|
|
810
817
|
logger.info("Running WiFi and OptiTrack connectivity verification test")
|
|
811
818
|
|
|
812
819
|
# Create test message with proper command structure
|
|
@@ -815,7 +822,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
815
822
|
"messageId": f"test-wifi-optitrack-{datetime.now().timestamp()}",
|
|
816
823
|
"deviceId": message.get("deviceId", "test-device"),
|
|
817
824
|
"command": "petal-user-journey-coordinator/connect_to_wifi_and_verify_optitrack",
|
|
818
|
-
"timestamp": datetime.now().isoformat(),
|
|
825
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
819
826
|
"payload": {
|
|
820
827
|
"positioning_system_network_wifi_ssid": "Rob-Lab-C00060",
|
|
821
828
|
"positioning_system_network_wifi_pass": "kuri@1234!!",
|
|
@@ -830,14 +837,9 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
830
837
|
await self._master_command_handler(topic, test_message)
|
|
831
838
|
logger.info("WiFi and OptiTrack connectivity test completed")
|
|
832
839
|
|
|
840
|
+
@http_action(method="POST", path="/test/set-static-ip-address")
|
|
833
841
|
async def _test_set_static_ip_address_handler(self, topic: str, message: Dict[str, Any]):
|
|
834
842
|
"""Test handler for static IP address configuration."""
|
|
835
|
-
# allow only one call
|
|
836
|
-
if getattr(self, "_test_static_ip", False):
|
|
837
|
-
logger.warning("Static IP test has already been called.")
|
|
838
|
-
return
|
|
839
|
-
self._test_static_ip = True
|
|
840
|
-
|
|
841
843
|
logger.info("Running static IP address configuration test")
|
|
842
844
|
|
|
843
845
|
# Create test message with proper command structure
|
|
@@ -846,7 +848,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
846
848
|
"messageId": f"test-static-ip-{datetime.now().timestamp()}",
|
|
847
849
|
"deviceId": message.get("deviceId", "test-device"),
|
|
848
850
|
"command": "petal-user-journey-coordinator/set_static_ip_address",
|
|
849
|
-
"timestamp": datetime.now().isoformat(),
|
|
851
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
850
852
|
"payload": {
|
|
851
853
|
"positioning_system_network_wifi_subnet": "255.255.255.0",
|
|
852
854
|
"positioning_system_network_server_ip_address": "10.0.0.27"
|
|
@@ -857,37 +859,12 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
857
859
|
await self._master_command_handler(topic, test_message)
|
|
858
860
|
logger.info("Static IP address configuration test completed")
|
|
859
861
|
|
|
860
|
-
|
|
861
|
-
"""
|
|
862
|
-
Helper method to create properly formatted command messages for testing.
|
|
863
|
-
|
|
864
|
-
Args:
|
|
865
|
-
command: The command to execute (e.g., "petal-user-journey-coordinator/verify_pos_yaw_directions")
|
|
866
|
-
payload: The payload data for the command
|
|
867
|
-
|
|
868
|
-
Returns:
|
|
869
|
-
Properly formatted MQTT message dict
|
|
870
|
-
"""
|
|
871
|
-
return {
|
|
872
|
-
"waitResponse": True,
|
|
873
|
-
"messageId": f"test-{command.replace('/', '-')}-{datetime.now().timestamp()}",
|
|
874
|
-
"deviceId": "test-device",
|
|
875
|
-
"command": command,
|
|
876
|
-
"timestamp": datetime.now().isoformat(),
|
|
877
|
-
"payload": payload
|
|
878
|
-
}
|
|
879
|
-
|
|
862
|
+
@http_action(method="POST", path="/test/unregister-all-handlers")
|
|
880
863
|
async def _test_unregister_all_handlers(self, topic: str, message: Dict[str, Any]):
|
|
881
864
|
"""
|
|
882
865
|
Test handler that subscribes to multiple streams and then tests unsubscribe all functionality.
|
|
883
866
|
This demonstrates the complete workflow of subscription tracking and bulk unsubscribe.
|
|
884
867
|
"""
|
|
885
|
-
# Allow only one call
|
|
886
|
-
if getattr(self, "_test_unsubscribe_all", False):
|
|
887
|
-
logger.warning("Unsubscribe all test has already been called.")
|
|
888
|
-
return
|
|
889
|
-
self._test_unsubscribe_all = True
|
|
890
|
-
|
|
891
868
|
logger.info("Running unsubscribe all functionality test")
|
|
892
869
|
|
|
893
870
|
# List of streams to subscribe to for testing
|
|
@@ -917,7 +894,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
917
894
|
"messageId": f"test-subscribe-{subscription['stream_id']}-{datetime.now().timestamp()}",
|
|
918
895
|
"deviceId": message.get("deviceId", "test-device"),
|
|
919
896
|
"command": subscription["command"],
|
|
920
|
-
"timestamp": datetime.now().isoformat(),
|
|
897
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
921
898
|
"payload": {
|
|
922
899
|
"subscribed_stream_id": subscription["stream_id"],
|
|
923
900
|
"data_rate_hz": subscription["data_rate_hz"]
|
|
@@ -949,7 +926,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
949
926
|
"messageId": f"test-unsubscribe-all-{datetime.now().timestamp()}",
|
|
950
927
|
"deviceId": message.get("deviceId", "test-device"),
|
|
951
928
|
"command": "petal-user-journey-coordinator/unsubscribeall",
|
|
952
|
-
"timestamp": datetime.now().isoformat(),
|
|
929
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
953
930
|
"payload": {}
|
|
954
931
|
}
|
|
955
932
|
|
|
@@ -1622,6 +1599,387 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
1622
1599
|
response_data={"status": "error", "message": str(e)}
|
|
1623
1600
|
)
|
|
1624
1601
|
|
|
1602
|
+
@http_action(method="POST", path="/mqtt/bulk_set_parameters")
|
|
1603
|
+
async def _bulk_set_parameter_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
1604
|
+
"""
|
|
1605
|
+
Handle bulk set parameter MQTT messages.
|
|
1606
|
+
|
|
1607
|
+
This handler processes requests to set multiple parameters in bulk,
|
|
1608
|
+
ensuring no active operations are in progress before applying changes.
|
|
1609
|
+
"""
|
|
1610
|
+
try:
|
|
1611
|
+
# Parse base MQTT message
|
|
1612
|
+
mqtt_msg = MQTTMessage(**message)
|
|
1613
|
+
message_id = mqtt_msg.messageId
|
|
1614
|
+
|
|
1615
|
+
# Check if controller is in emergency mode
|
|
1616
|
+
for controller in self._active_controllers.values():
|
|
1617
|
+
if controller is not None and controller.is_active:
|
|
1618
|
+
error_msg = "Bulk parameter configuration blocked - Active operation in progress"
|
|
1619
|
+
logger.warning(f"[{message_id}] {error_msg}")
|
|
1620
|
+
if mqtt_msg.waitResponse:
|
|
1621
|
+
await self._mqtt_proxy.send_command_response(
|
|
1622
|
+
message_id=message_id,
|
|
1623
|
+
response_data={
|
|
1624
|
+
"status": "error",
|
|
1625
|
+
"message": error_msg,
|
|
1626
|
+
"error_code": "OPERATION_ACTIVE"
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
return
|
|
1630
|
+
|
|
1631
|
+
# Process payload using the mavlink proxy bulk set parameter helper
|
|
1632
|
+
payload = BulkParameterSetRequest(**mqtt_msg.payload)
|
|
1633
|
+
parameters = payload.parameters
|
|
1634
|
+
|
|
1635
|
+
if not parameters:
|
|
1636
|
+
error_msg = "No parameters provided for bulk set"
|
|
1637
|
+
logger.error(f"[{message_id}] {error_msg}")
|
|
1638
|
+
if mqtt_msg.waitResponse:
|
|
1639
|
+
await self._mqtt_proxy.send_command_response(
|
|
1640
|
+
message_id=message_id,
|
|
1641
|
+
response_data={
|
|
1642
|
+
"status": "error",
|
|
1643
|
+
"message": error_msg,
|
|
1644
|
+
"error_code": "NO_PARAMETERS_PROVIDED"
|
|
1645
|
+
}
|
|
1646
|
+
)
|
|
1647
|
+
return
|
|
1648
|
+
|
|
1649
|
+
logger.info(f"Setting bulk PX4 parameters for {self.name}")
|
|
1650
|
+
|
|
1651
|
+
results = {}
|
|
1652
|
+
set_param_dict = {}
|
|
1653
|
+
for parameter in parameters:
|
|
1654
|
+
set_param_dict[parameter.parameter_name] = (
|
|
1655
|
+
parameter.parameter_value,
|
|
1656
|
+
parameter.parameter_type
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1659
|
+
confirmed = await self._mavlink_proxy.set_params_bulk_lossy(
|
|
1660
|
+
set_param_dict,
|
|
1661
|
+
max_in_flight=6,
|
|
1662
|
+
resend_interval=0.8,
|
|
1663
|
+
max_retries=5,
|
|
1664
|
+
timeout_total=10.0,
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
if not confirmed:
|
|
1668
|
+
error_msg = "No parameters were confirmed after bulk set"
|
|
1669
|
+
logger.error(f"[{message_id}] {error_msg}")
|
|
1670
|
+
if mqtt_msg.waitResponse:
|
|
1671
|
+
await self._mqtt_proxy.send_command_response(
|
|
1672
|
+
message_id=message_id,
|
|
1673
|
+
response_data={
|
|
1674
|
+
"status": "error",
|
|
1675
|
+
"message": error_msg,
|
|
1676
|
+
"error_code": "NO_PARAMETERS_CONFIRMED"
|
|
1677
|
+
}
|
|
1678
|
+
)
|
|
1679
|
+
return
|
|
1680
|
+
|
|
1681
|
+
# check that all requested parameters were confirmed
|
|
1682
|
+
success = True
|
|
1683
|
+
for parameter in parameters:
|
|
1684
|
+
pname = parameter.parameter_name
|
|
1685
|
+
if pname in confirmed:
|
|
1686
|
+
results[pname] = confirmed[pname]
|
|
1687
|
+
# check that the set value matches the requested value
|
|
1688
|
+
confirmed_value = results[pname].get("value")
|
|
1689
|
+
if confirmed_value == parameter.parameter_value:
|
|
1690
|
+
results[pname]["success"] = True
|
|
1691
|
+
else:
|
|
1692
|
+
results[pname]["success"] = False
|
|
1693
|
+
results[pname]["error"] = f"Parameter value mismatch: requested {parameter.parameter_value}, got {confirmed_value}"
|
|
1694
|
+
success = False
|
|
1695
|
+
else:
|
|
1696
|
+
results[pname] = {
|
|
1697
|
+
"name": pname,
|
|
1698
|
+
"error": "Parameter value could not be retrieved after set",
|
|
1699
|
+
"success": False
|
|
1700
|
+
}
|
|
1701
|
+
success = False
|
|
1702
|
+
|
|
1703
|
+
payload = {
|
|
1704
|
+
"results": results,
|
|
1705
|
+
"success": success,
|
|
1706
|
+
# timestamp must be string
|
|
1707
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
payload = BulkParameterResponse(**payload)
|
|
1711
|
+
sanitized_response = _json_safe(payload.model_dump())
|
|
1712
|
+
|
|
1713
|
+
logger.info(f"[{message_id}] Successfully processed bulk parameter configuration")
|
|
1714
|
+
|
|
1715
|
+
# Send response if requested
|
|
1716
|
+
if mqtt_msg.waitResponse:
|
|
1717
|
+
if not success:
|
|
1718
|
+
logger.warning(f"[{message_id}] Some parameters could not be set successfully")
|
|
1719
|
+
await self._mqtt_proxy.send_command_response(
|
|
1720
|
+
message_id=message_id,
|
|
1721
|
+
response_data={
|
|
1722
|
+
"status": "error",
|
|
1723
|
+
"message": f"Bulk parameter set completed - some parameters failed",
|
|
1724
|
+
"data": sanitized_response
|
|
1725
|
+
}
|
|
1726
|
+
)
|
|
1727
|
+
else:
|
|
1728
|
+
await self._mqtt_proxy.send_command_response(
|
|
1729
|
+
message_id=message_id,
|
|
1730
|
+
response_data={
|
|
1731
|
+
"status": "success",
|
|
1732
|
+
"message": f"Bulk parameter set completed - {len(results)} parameters processed",
|
|
1733
|
+
"data": sanitized_response
|
|
1734
|
+
}
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
except ValidationError as ve:
|
|
1738
|
+
error_msg = f"Invalid bulk parameter payload: {ve}"
|
|
1739
|
+
logger.error(f"Bulk parameter config validation error: {error_msg}")
|
|
1740
|
+
if mqtt_msg.waitResponse:
|
|
1741
|
+
await self._mqtt_proxy.send_command_response(
|
|
1742
|
+
message_id=message_id,
|
|
1743
|
+
response_data={
|
|
1744
|
+
"status": "error",
|
|
1745
|
+
"message": error_msg,
|
|
1746
|
+
"error_code": "VALIDATION_ERROR"
|
|
1747
|
+
}
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
except Exception as e:
|
|
1751
|
+
error_msg = f"Bulk parameter handler error: {str(e)}"
|
|
1752
|
+
logger.error(f"Unexpected bulk parameter error: {error_msg}")
|
|
1753
|
+
if mqtt_msg.waitResponse:
|
|
1754
|
+
await self._mqtt_proxy.send_command_response(
|
|
1755
|
+
message_id=message_id,
|
|
1756
|
+
response_data={
|
|
1757
|
+
"status": "error",
|
|
1758
|
+
"message": error_msg,
|
|
1759
|
+
"error_code": "HANDLER_ERROR"
|
|
1760
|
+
}
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
@http_action(method="POST", path="/mqtt/bulk_get_parameters")
|
|
1764
|
+
async def _bulk_get_parameter_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
1765
|
+
"""
|
|
1766
|
+
Handle bulk get parameter MQTT messages.
|
|
1767
|
+
|
|
1768
|
+
This handler processes requests to get multiple parameters in bulk,
|
|
1769
|
+
ensuring no active operations are in progress before retrieving values.
|
|
1770
|
+
"""
|
|
1771
|
+
try:
|
|
1772
|
+
# Parse base MQTT message
|
|
1773
|
+
mqtt_msg = MQTTMessage(**message)
|
|
1774
|
+
message_id = mqtt_msg.messageId
|
|
1775
|
+
|
|
1776
|
+
# Check if controller is in emergency mode
|
|
1777
|
+
for controller in self._active_controllers.values():
|
|
1778
|
+
if controller is not None and controller.is_active:
|
|
1779
|
+
error_msg = "Bulk parameter retrieval blocked - Active operation in progress"
|
|
1780
|
+
logger.warning(f"[{message_id}] {error_msg}")
|
|
1781
|
+
if mqtt_msg.waitResponse:
|
|
1782
|
+
await self._mqtt_proxy.send_command_response(
|
|
1783
|
+
message_id=message_id,
|
|
1784
|
+
response_data={
|
|
1785
|
+
"status": "error",
|
|
1786
|
+
"message": error_msg,
|
|
1787
|
+
"error_code": "OPERATION_ACTIVE"
|
|
1788
|
+
}
|
|
1789
|
+
)
|
|
1790
|
+
return
|
|
1791
|
+
|
|
1792
|
+
# Process payload using the mavlink proxy bulk get parameter helper
|
|
1793
|
+
payload = BulkParameterGetRequest(**mqtt_msg.payload)
|
|
1794
|
+
parameter_names = payload.parameter_names
|
|
1795
|
+
|
|
1796
|
+
if not parameter_names:
|
|
1797
|
+
error_msg = "No parameter names provided for bulk get"
|
|
1798
|
+
logger.error(f"[{message_id}] {error_msg}")
|
|
1799
|
+
if mqtt_msg.waitResponse:
|
|
1800
|
+
await self._mqtt_proxy.send_command_response(
|
|
1801
|
+
message_id=message_id,
|
|
1802
|
+
response_data={
|
|
1803
|
+
"status": "error",
|
|
1804
|
+
"message": error_msg,
|
|
1805
|
+
"error_code": "NO_PARAMETER_NAMES_PROVIDED"
|
|
1806
|
+
}
|
|
1807
|
+
)
|
|
1808
|
+
return
|
|
1809
|
+
|
|
1810
|
+
logger.info(f"Getting bulk PX4 parameters for {self.name}")
|
|
1811
|
+
|
|
1812
|
+
parameters = await self._mavlink_proxy.get_params_bulk_lossy(
|
|
1813
|
+
names=parameter_names,
|
|
1814
|
+
max_in_flight=6,
|
|
1815
|
+
resend_interval=0.8,
|
|
1816
|
+
max_retries=5,
|
|
1817
|
+
timeout_total=10.0,
|
|
1818
|
+
inter_send_delay=0.05,
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
if not parameters:
|
|
1822
|
+
error_msg = "No parameters were confirmed after bulk get"
|
|
1823
|
+
logger.error(f"[{message_id}] {error_msg}")
|
|
1824
|
+
if mqtt_msg.waitResponse:
|
|
1825
|
+
await self._mqtt_proxy.send_command_response(
|
|
1826
|
+
message_id=message_id,
|
|
1827
|
+
response_data={
|
|
1828
|
+
"status": "error",
|
|
1829
|
+
"message": error_msg,
|
|
1830
|
+
"error_code": "NO_PARAMETERS_CONFIRMED"
|
|
1831
|
+
}
|
|
1832
|
+
)
|
|
1833
|
+
|
|
1834
|
+
success = True
|
|
1835
|
+
for parameter in parameter_names:
|
|
1836
|
+
if parameter not in parameters:
|
|
1837
|
+
parameters[parameter] = {
|
|
1838
|
+
"name": parameter,
|
|
1839
|
+
"error": "Parameter value could not be retrieved",
|
|
1840
|
+
"success": False
|
|
1841
|
+
}
|
|
1842
|
+
success = False
|
|
1843
|
+
else:
|
|
1844
|
+
parameters[parameter]["success"] = True
|
|
1845
|
+
|
|
1846
|
+
payload = {
|
|
1847
|
+
"results": parameters,
|
|
1848
|
+
"success": success,
|
|
1849
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
payload = BulkParameterResponse(**payload)
|
|
1853
|
+
sanitized_response = _json_safe(payload.model_dump())
|
|
1854
|
+
|
|
1855
|
+
logger.info(f"[{message_id}] Successfully processed bulk parameter retrieval")
|
|
1856
|
+
|
|
1857
|
+
# Send response if requested
|
|
1858
|
+
if mqtt_msg.waitResponse:
|
|
1859
|
+
if not success:
|
|
1860
|
+
logger.warning(f"[{message_id}] Some parameters could not be retrieved successfully")
|
|
1861
|
+
await self._mqtt_proxy.send_command_response(
|
|
1862
|
+
message_id=message_id,
|
|
1863
|
+
response_data={
|
|
1864
|
+
"status": "error",
|
|
1865
|
+
"message": f"Bulk parameter get completed - some parameters failed",
|
|
1866
|
+
"data": sanitized_response
|
|
1867
|
+
}
|
|
1868
|
+
)
|
|
1869
|
+
else:
|
|
1870
|
+
await self._mqtt_proxy.send_command_response(
|
|
1871
|
+
message_id=message_id,
|
|
1872
|
+
response_data={
|
|
1873
|
+
"status": "success",
|
|
1874
|
+
"message": f"Bulk parameter get completed - {len(parameters)} parameters processed",
|
|
1875
|
+
"data": sanitized_response
|
|
1876
|
+
}
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
except ValidationError as ve:
|
|
1880
|
+
error_msg = f"Invalid bulk parameter get payload: {ve}"
|
|
1881
|
+
logger.error(f"Bulk parameter get validation error: {error_msg}")
|
|
1882
|
+
if mqtt_msg.waitResponse:
|
|
1883
|
+
await self._mqtt_proxy.send_command_response(
|
|
1884
|
+
message_id=message_id,
|
|
1885
|
+
response_data={
|
|
1886
|
+
"status": "error",
|
|
1887
|
+
"message": error_msg,
|
|
1888
|
+
"error_code": "VALIDATION_ERROR"
|
|
1889
|
+
}
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
except Exception as e:
|
|
1893
|
+
error_msg = f"Bulk parameter get handler error: {str(e)}"
|
|
1894
|
+
logger.error(f"Unexpected bulk parameter get error: {error_msg}")
|
|
1895
|
+
if mqtt_msg.waitResponse:
|
|
1896
|
+
await self._mqtt_proxy.send_command_response(
|
|
1897
|
+
message_id=message_id,
|
|
1898
|
+
response_data={
|
|
1899
|
+
"status": "error",
|
|
1900
|
+
"message": error_msg,
|
|
1901
|
+
"error_code": "HANDLER_ERROR"
|
|
1902
|
+
}
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
@http_action(method="POST", path="/mqtt/reboot_px4")
|
|
1906
|
+
async def _reboot_px4_message_handler(self, topic: str, message: Dict[str, Any]):
|
|
1907
|
+
"""Handle reboot PX4 MQTT messages."""
|
|
1908
|
+
try:
|
|
1909
|
+
# Parse base MQTT message
|
|
1910
|
+
mqtt_msg = MQTTMessage(**message)
|
|
1911
|
+
message_id = mqtt_msg.messageId
|
|
1912
|
+
|
|
1913
|
+
# Check if controller is in emergency mode
|
|
1914
|
+
for controller in self._active_controllers.values():
|
|
1915
|
+
if controller is not None and controller.is_active:
|
|
1916
|
+
error_msg = "PX4 reboot blocked - Active operation in progress"
|
|
1917
|
+
logger.warning(f"[{message_id}] {error_msg}")
|
|
1918
|
+
if mqtt_msg.waitResponse:
|
|
1919
|
+
await self._mqtt_proxy.send_command_response(
|
|
1920
|
+
message_id=message_id,
|
|
1921
|
+
response_data={
|
|
1922
|
+
"status": "error",
|
|
1923
|
+
"message": error_msg,
|
|
1924
|
+
"error_code": "OPERATION_ACTIVE"
|
|
1925
|
+
}
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
return
|
|
1929
|
+
|
|
1930
|
+
logger.info(f"Restarting PX4 for {self.name} petal")
|
|
1931
|
+
reboot_response = await self._mavlink_proxy.reboot_autopilot(
|
|
1932
|
+
reboot_onboard_computer=False,
|
|
1933
|
+
timeout=5.0
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
if not reboot_response.success:
|
|
1937
|
+
error_msg = "PX4 reboot command failed or timed out"
|
|
1938
|
+
logger.error(f"[{message_id}] {error_msg}")
|
|
1939
|
+
if mqtt_msg.waitResponse:
|
|
1940
|
+
await self._mqtt_proxy.send_command_response(
|
|
1941
|
+
message_id=message_id,
|
|
1942
|
+
response_data={
|
|
1943
|
+
"status": "error",
|
|
1944
|
+
"message": error_msg,
|
|
1945
|
+
"error_code": "REBOOT_FAILED",
|
|
1946
|
+
"data": _json_safe(reboot_response.model_dump())
|
|
1947
|
+
}
|
|
1948
|
+
)
|
|
1949
|
+
return
|
|
1950
|
+
|
|
1951
|
+
logger.info(f"[{message_id}] PX4 reboot command successful")
|
|
1952
|
+
|
|
1953
|
+
if mqtt_msg.waitResponse:
|
|
1954
|
+
await self._mqtt_proxy.send_command_response(
|
|
1955
|
+
message_id=message_id,
|
|
1956
|
+
response_data={
|
|
1957
|
+
"status": "success",
|
|
1958
|
+
"message": "PX4 reboot command successful",
|
|
1959
|
+
"data": _json_safe(reboot_response.model_dump())
|
|
1960
|
+
}
|
|
1961
|
+
)
|
|
1962
|
+
except ValidationError as ve:
|
|
1963
|
+
error_msg = f"Invalid PX4 reboot payload: {ve}"
|
|
1964
|
+
logger.error(f"PX4 reboot validation error: {error_msg}")
|
|
1965
|
+
if mqtt_msg.waitResponse:
|
|
1966
|
+
await self._mqtt_proxy.send_command_response(
|
|
1967
|
+
message_id=message_id,
|
|
1968
|
+
response_data={
|
|
1969
|
+
"status": "error",
|
|
1970
|
+
"message": error_msg,
|
|
1971
|
+
"error_code": "VALIDATION_ERROR"
|
|
1972
|
+
}
|
|
1973
|
+
)
|
|
1974
|
+
except Exception as e:
|
|
1975
|
+
logger.error(f"Error handling PX4 reboot message: {e}")
|
|
1976
|
+
if mqtt_msg and mqtt_msg.waitResponse:
|
|
1977
|
+
await self._mqtt_proxy.send_command_response(
|
|
1978
|
+
message_id=mqtt_msg.messageId,
|
|
1979
|
+
response_data={"status": "error", "message": str(e)}
|
|
1980
|
+
)
|
|
1981
|
+
return
|
|
1982
|
+
|
|
1625
1983
|
def _create_parameter_message_handler(self, handler_key: str, config_type: str):
|
|
1626
1984
|
"""
|
|
1627
1985
|
Factory method to create parameter message handlers with minimal boilerplate.
|
|
@@ -1983,11 +2341,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
1983
2341
|
|
|
1984
2342
|
return status
|
|
1985
2343
|
|
|
1986
|
-
@http_action(
|
|
1987
|
-
method="GET",
|
|
1988
|
-
path="/health",
|
|
1989
|
-
description="Health check endpoint for this petal"
|
|
1990
|
-
)
|
|
2344
|
+
@http_action(method="GET", path="/health")
|
|
1991
2345
|
async def health_check(self):
|
|
1992
2346
|
"""
|
|
1993
2347
|
Health check endpoint that reports proxy requirements and petal status.
|
|
@@ -1999,7 +2353,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
1999
2353
|
health_info = {
|
|
2000
2354
|
"petal_name": self.name,
|
|
2001
2355
|
"petal_version": self.version,
|
|
2002
|
-
"timestamp": datetime.now().isoformat(),
|
|
2356
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
2003
2357
|
"status": "healthy",
|
|
2004
2358
|
"required_proxies": self.get_required_proxies(),
|
|
2005
2359
|
"optional_proxies": self.get_optional_proxies(),
|
|
@@ -2008,11 +2362,7 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
2008
2362
|
|
|
2009
2363
|
return health_info
|
|
2010
2364
|
|
|
2011
|
-
@http_action(
|
|
2012
|
-
method="GET",
|
|
2013
|
-
path="/px4-parameter",
|
|
2014
|
-
description="get a specific PX4 parameter"
|
|
2015
|
-
)
|
|
2365
|
+
@http_action(method="GET", path="/px4-parameter")
|
|
2016
2366
|
async def get_px4_parameter(self, data: ParameterRequestModel) -> MavlinkParameterResponseModel:
|
|
2017
2367
|
"""
|
|
2018
2368
|
Get a specific PX4 parameter value.
|
|
@@ -2035,17 +2385,13 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
2035
2385
|
return {
|
|
2036
2386
|
"parameter_name": parameter_name,
|
|
2037
2387
|
"parameter_value": parameter_value,
|
|
2038
|
-
"timestamp": datetime.now().isoformat()
|
|
2388
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2039
2389
|
}
|
|
2040
2390
|
except TimeoutError as exc:
|
|
2041
2391
|
logger.error(f"Timeout while waiting for PX4 parameter: {str(exc)}")
|
|
2042
2392
|
raise HTTPException(status_code=504, detail=str(exc))
|
|
2043
2393
|
|
|
2044
|
-
@http_action(
|
|
2045
|
-
method="GET",
|
|
2046
|
-
path="/px4-parameters",
|
|
2047
|
-
description="get a specific PX4 parameter"
|
|
2048
|
-
)
|
|
2394
|
+
@http_action(method="GET", path="/px4-parameters")
|
|
2049
2395
|
async def get_all_parameters(self) -> MavlinkParametersResponseModel:
|
|
2050
2396
|
"""
|
|
2051
2397
|
Get a specific PX4 parameter value.
|
|
@@ -2066,18 +2412,14 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
2066
2412
|
)
|
|
2067
2413
|
payload = {
|
|
2068
2414
|
"parameters": parameters,
|
|
2069
|
-
"timestamp": datetime.now().isoformat()
|
|
2415
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2070
2416
|
}
|
|
2071
2417
|
return _json_safe(payload) # ← sanitize before FastAPI encodes
|
|
2072
2418
|
except TimeoutError as exc:
|
|
2073
2419
|
logger.error(f"Timeout while waiting for PX4 parameter: {str(exc)}")
|
|
2074
2420
|
raise HTTPException(status_code=504, detail=str(exc))
|
|
2075
2421
|
|
|
2076
|
-
@http_action(
|
|
2077
|
-
method="POST",
|
|
2078
|
-
path="/px4-parameter",
|
|
2079
|
-
description="set a specific PX4 parameter"
|
|
2080
|
-
)
|
|
2422
|
+
@http_action(method="POST", path="/px4-parameter")
|
|
2081
2423
|
async def set_px4_parameter(self, data: ParameterBaseModel) -> ParameterResponseModel:
|
|
2082
2424
|
"""
|
|
2083
2425
|
Get a specific PX4 parameter value.
|
|
@@ -2111,18 +2453,14 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
2111
2453
|
payload = {
|
|
2112
2454
|
"parameter_name": parameter_name,
|
|
2113
2455
|
"parameter_value": parameter_value,
|
|
2114
|
-
"timestamp": datetime.now().isoformat()
|
|
2456
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2115
2457
|
}
|
|
2116
2458
|
return _json_safe(payload) # ← sanitize before FastAPI encodes
|
|
2117
2459
|
except TimeoutError as exc:
|
|
2118
2460
|
logger.error(f"Timeout while waiting for PX4 parameter: {str(exc)}")
|
|
2119
2461
|
raise HTTPException(status_code=504, detail=str(exc))
|
|
2120
2462
|
|
|
2121
|
-
@http_action(
|
|
2122
|
-
method="POST",
|
|
2123
|
-
path="/rotor-count",
|
|
2124
|
-
description="set a specific PX4 parameter"
|
|
2125
|
-
)
|
|
2463
|
+
@http_action(method="POST", path="/rotor-count")
|
|
2126
2464
|
async def set_rotor_count(self, data: RotorCountParameter) -> ParameterResponseModel:
|
|
2127
2465
|
"""
|
|
2128
2466
|
Get a specific PX4 parameter value.
|
|
@@ -2159,9 +2497,160 @@ class PetalUserJourneyCoordinator(Petal):
|
|
|
2159
2497
|
payload = {
|
|
2160
2498
|
"parameter_name": parameter_name,
|
|
2161
2499
|
"parameter_value": parameter_value,
|
|
2162
|
-
"timestamp": datetime.now().isoformat()
|
|
2500
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2163
2501
|
}
|
|
2164
2502
|
return _json_safe(payload) # ← sanitize before FastAPI encodes
|
|
2165
2503
|
except TimeoutError as exc:
|
|
2166
2504
|
logger.error(f"Timeout while waiting for PX4 parameter: {str(exc)}")
|
|
2167
2505
|
raise HTTPException(status_code=504, detail=str(exc))
|
|
2506
|
+
|
|
2507
|
+
@http_action(method="POST", path="/bulk-px4-parameters")
|
|
2508
|
+
async def set_bulk_px4_parameters(self, data: BulkParameterSetRequest) -> BulkParameterResponse:
|
|
2509
|
+
"""
|
|
2510
|
+
Set multiple PX4 parameters in bulk.
|
|
2511
|
+
"""
|
|
2512
|
+
|
|
2513
|
+
parameters = data.parameters
|
|
2514
|
+
|
|
2515
|
+
if not parameters:
|
|
2516
|
+
logger.error("No parameters provided for bulk set")
|
|
2517
|
+
raise HTTPException(
|
|
2518
|
+
status_code=400,
|
|
2519
|
+
detail="No parameters provided",
|
|
2520
|
+
headers={"source": "bulk_px4_parameters"}
|
|
2521
|
+
)
|
|
2522
|
+
|
|
2523
|
+
logger.info(f"Setting bulk PX4 parameters for {self.name}")
|
|
2524
|
+
|
|
2525
|
+
results = {}
|
|
2526
|
+
try:
|
|
2527
|
+
set_param_dict = {}
|
|
2528
|
+
for parameter in parameters:
|
|
2529
|
+
set_param_dict[parameter.parameter_name] = (
|
|
2530
|
+
parameter.parameter_value,
|
|
2531
|
+
parameter.parameter_type
|
|
2532
|
+
)
|
|
2533
|
+
|
|
2534
|
+
confirmed = await self._mavlink_proxy.set_params_bulk_lossy(
|
|
2535
|
+
set_param_dict,
|
|
2536
|
+
max_in_flight=6,
|
|
2537
|
+
resend_interval=0.8,
|
|
2538
|
+
max_retries=5,
|
|
2539
|
+
timeout_total=10.0,
|
|
2540
|
+
)
|
|
2541
|
+
|
|
2542
|
+
# confirmed[pname] = {
|
|
2543
|
+
# "name": pname,
|
|
2544
|
+
# "value": decoded_value,
|
|
2545
|
+
# "raw": float(pkt.param_value),
|
|
2546
|
+
# "type": pkt.param_type,
|
|
2547
|
+
# "count": pkt.param_count,
|
|
2548
|
+
# "index": pkt.param_index,
|
|
2549
|
+
# }
|
|
2550
|
+
|
|
2551
|
+
if not confirmed:
|
|
2552
|
+
logger.error("No parameters were set in bulk PX4 parameter set")
|
|
2553
|
+
raise HTTPException(
|
|
2554
|
+
status_code=500,
|
|
2555
|
+
detail="No parameters were set",
|
|
2556
|
+
headers={"source": "bulk_px4_parameters"}
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2559
|
+
# check that all requested parameters were confirmed
|
|
2560
|
+
success = True
|
|
2561
|
+
for parameter in parameters:
|
|
2562
|
+
pname = parameter.parameter_name
|
|
2563
|
+
if pname in confirmed:
|
|
2564
|
+
results[pname] = confirmed[pname]
|
|
2565
|
+
# check that the set value matches the requested value
|
|
2566
|
+
confirmed_value = results[pname].get("value")
|
|
2567
|
+
if confirmed_value == parameter.parameter_value:
|
|
2568
|
+
results[pname]["success"] = True
|
|
2569
|
+
else:
|
|
2570
|
+
results[pname]["success"] = False
|
|
2571
|
+
results[pname]["error"] = f"Parameter value mismatch: requested {parameter.parameter_value}, got {confirmed_value}"
|
|
2572
|
+
success = False
|
|
2573
|
+
else:
|
|
2574
|
+
results[pname] = {
|
|
2575
|
+
"name": pname,
|
|
2576
|
+
"error": "Parameter value could not be retrieved after set",
|
|
2577
|
+
"success": False
|
|
2578
|
+
}
|
|
2579
|
+
success = False
|
|
2580
|
+
|
|
2581
|
+
payload = {
|
|
2582
|
+
"results": results,
|
|
2583
|
+
"success": success,
|
|
2584
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
payload = BulkParameterResponse(**payload)
|
|
2588
|
+
return _json_safe(payload.model_dump()) # ← sanitize before FastAPI encodes
|
|
2589
|
+
|
|
2590
|
+
except TimeoutError as exc:
|
|
2591
|
+
logger.error(f"Timeout while setting bulk PX4 parameters: {str(exc)}")
|
|
2592
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
2593
|
+
except Exception as e:
|
|
2594
|
+
logger.error(f"Error while setting bulk PX4 parameters: {str(e)}")
|
|
2595
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2596
|
+
|
|
2597
|
+
@http_action(method="GET", path="/bulk-px4-parameters")
|
|
2598
|
+
async def get_bulk_px4_parameters(self, data: BulkParameterGetRequest) -> BulkParameterResponse:
|
|
2599
|
+
"""
|
|
2600
|
+
Get multiple PX4 parameters in bulk.
|
|
2601
|
+
"""
|
|
2602
|
+
parameter_names = data.parameter_names
|
|
2603
|
+
|
|
2604
|
+
if not parameter_names:
|
|
2605
|
+
logger.error("No parameter names provided for bulk get")
|
|
2606
|
+
raise HTTPException(
|
|
2607
|
+
status_code=400,
|
|
2608
|
+
detail="No parameter names provided",
|
|
2609
|
+
headers={"source": "bulk_px4_parameters"}
|
|
2610
|
+
)
|
|
2611
|
+
|
|
2612
|
+
logger.info(f"Getting bulk PX4 parameters for {self.name}")
|
|
2613
|
+
|
|
2614
|
+
try:
|
|
2615
|
+
|
|
2616
|
+
parameters = await self._mavlink_proxy.get_params_bulk_lossy(
|
|
2617
|
+
names=parameter_names,
|
|
2618
|
+
timeout_total=6.0,
|
|
2619
|
+
max_retries=4,
|
|
2620
|
+
max_in_flight=6,
|
|
2621
|
+
resend_interval=0.8,
|
|
2622
|
+
inter_send_delay=0.02,
|
|
2623
|
+
)
|
|
2624
|
+
|
|
2625
|
+
if not parameters:
|
|
2626
|
+
logger.error("No parameters were retrieved in bulk PX4 parameter get")
|
|
2627
|
+
raise HTTPException(
|
|
2628
|
+
status_code=500,
|
|
2629
|
+
detail="No parameters were retrieved",
|
|
2630
|
+
headers={"source": "bulk_px4_parameters"}
|
|
2631
|
+
)
|
|
2632
|
+
|
|
2633
|
+
success = True
|
|
2634
|
+
for parameter in parameter_names:
|
|
2635
|
+
if parameter not in parameters:
|
|
2636
|
+
parameters[parameter] = {
|
|
2637
|
+
"name": parameter,
|
|
2638
|
+
"error": "Parameter value could not be retrieved",
|
|
2639
|
+
"success": False
|
|
2640
|
+
}
|
|
2641
|
+
success = False
|
|
2642
|
+
else:
|
|
2643
|
+
parameters[parameter]["success"] = True
|
|
2644
|
+
|
|
2645
|
+
|
|
2646
|
+
payload = {
|
|
2647
|
+
"success": success,
|
|
2648
|
+
"results": parameters,
|
|
2649
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
2650
|
+
}
|
|
2651
|
+
parameters = BulkParameterResponse(**payload)
|
|
2652
|
+
return _json_safe(parameters.model_dump()) # ← sanitize before FastAPI encodes
|
|
2653
|
+
except TimeoutError as exc:
|
|
2654
|
+
logger.error(f"Timeout while getting bulk PX4 parameters: {str(exc)}")
|
|
2655
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
2656
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_plotting.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|