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.
Files changed (18) hide show
  1. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/PKG-INFO +1 -1
  2. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/pyproject.toml +2 -2
  3. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/data_model.py +104 -24
  4. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/plugin.py +582 -93
  5. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/README.md +0 -0
  6. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/__init__.py +0 -0
  7. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/src/petal_user_journey_coordinator/controllers.py +0 -0
  8. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/__init__.py +0 -0
  9. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/dummy_trajectory_test.py +0 -0
  10. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/fix_expected_results.py +0 -0
  11. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/generate_trajectory_test_data.py +0 -0
  12. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/generated_trajectory_test_data.json +0 -0
  13. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_generated_trajectories.py +0 -0
  14. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_petal_user_journey_coordinator.py +0 -0
  15. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_plotting.py +0 -0
  16. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_trajectory_simple.py +0 -0
  17. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/test_trajectory_verification.py +0 -0
  18. {petal_user_journey_coordinator-0.1.5 → petal_user_journey_coordinator-0.1.6}/tests/trajectory_test_data.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: petal-user-journey-coordinator
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: A petal for the DroneLeaf ecosystem
5
5
  Author-Email: Khalil Al Handawi <khalil.alhandawi@droneleaf.io>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "petal-user-journey-coordinator"
3
- version = "0.1.5"
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==0.1.46",
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 ParameterRequestModel(BaseModel):
22
- """Base fields for flight records"""
23
- parameter_name: str = Field(..., description="Parameter name")
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
- "parameter_name": "CA_ROTOR_COUNT"
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 MQTTMessage(BaseModel):
35
- """MQTT message model"""
36
- waitResponse: bool = Field(..., description="Whether to wait for a response")
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
- "waitResponse": True,
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
- MQTTMessage,
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
- def _create_test_command_message(self, command: str, payload: Dict[str, Any]) -> Dict[str, Any]:
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
+