petal-user-journey-coordinator 0.1.7__tar.gz → 0.1.9__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.7 → petal_user_journey_coordinator-0.1.9}/PKG-INFO +1 -1
  2. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/pyproject.toml +1 -1
  3. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/src/petal_user_journey_coordinator/data_model.py +23 -0
  4. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/src/petal_user_journey_coordinator/plugin.py +147 -38
  5. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/README.md +0 -0
  6. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/src/petal_user_journey_coordinator/__init__.py +0 -0
  7. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/src/petal_user_journey_coordinator/controllers.py +0 -0
  8. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/__init__.py +0 -0
  9. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/dummy_trajectory_test.py +0 -0
  10. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/fix_expected_results.py +0 -0
  11. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/generate_trajectory_test_data.py +0 -0
  12. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/generated_trajectory_test_data.json +0 -0
  13. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/test_generated_trajectories.py +0 -0
  14. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/test_petal_user_journey_coordinator.py +0 -0
  15. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/test_plotting.py +0 -0
  16. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/test_trajectory_simple.py +0 -0
  17. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/tests/test_trajectory_verification.py +0 -0
  18. {petal_user_journey_coordinator-0.1.7 → petal_user_journey_coordinator-0.1.9}/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.7
3
+ Version: 0.1.9
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.7"
3
+ version = "0.1.9"
4
4
  description = "A petal for the DroneLeaf ecosystem"
5
5
  authors = [
6
6
  { name = "Khalil Al Handawi", email = "khalil.alhandawi@droneleaf.io" },
@@ -633,4 +633,27 @@ class BulkParameterResponse(BaseModel):
633
633
  "timestamp": "2023-01-01T00:00:00Z"
634
634
  }
635
635
  }
636
+ }
637
+
638
+
639
+ class RebootPX4StatusPayload(BaseModel):
640
+ """Payload for PX4 reboot status published via MQTT after reboot completes."""
641
+ reboot_initiated: bool = Field(..., description="Whether the reboot command was initiated")
642
+ reboot_success: Optional[bool] = Field(default=None, description="Whether the reboot completed successfully")
643
+ status: Literal["success", "failed"] = Field(..., description="Reboot status")
644
+ message: str = Field(..., description="Human-readable status message")
645
+ error_code: Optional[str] = Field(default=None, description="Error code if reboot failed")
646
+ timestamp: datetime = Field(..., description="Timestamp of the status update")
647
+
648
+ model_config = {
649
+ "json_schema_extra": {
650
+ "example": {
651
+ "reboot_initiated": True,
652
+ "reboot_success": True,
653
+ "status": "success",
654
+ "message": "PX4 reboot completed successfully",
655
+ "error_code": None,
656
+ "timestamp": "2026-01-07T12:00:00Z"
657
+ }
658
+ }
636
659
  }
@@ -93,7 +93,10 @@ from .data_model import (
93
93
  OpticalFlowModulePayload,
94
94
  BulkParameterSetRequest,
95
95
  BulkParameterGetRequest,
96
- BulkParameterResponse
96
+ BulkParameterResponse,
97
+
98
+ # Reboot status payload
99
+ RebootPX4StatusPayload
97
100
  )
98
101
 
99
102
  from petal_app_manager.models.mavlink import (
@@ -1711,7 +1714,18 @@ class PetalUserJourneyCoordinator(Petal):
1711
1714
  results[pname] = confirmed[pname]
1712
1715
  # check that the set value matches the requested value
1713
1716
  confirmed_value = results[pname].get("value")
1714
- if confirmed_value == parameter.parameter_value:
1717
+ requested_value = parameter.parameter_value
1718
+
1719
+ # Check for equality, handling floating point precision issues
1720
+ is_match = False
1721
+ if isinstance(confirmed_value, (float, int)) and isinstance(requested_value, (float, int)):
1722
+ # Use 1e-5 relative tolerance to handle float32/float64 mismatch
1723
+ # This should be enough for values like 0.2 vs 0.20000000298...
1724
+ is_match = math.isclose(confirmed_value, requested_value, rel_tol=1e-5)
1725
+ else:
1726
+ is_match = confirmed_value == requested_value
1727
+
1728
+ if is_match:
1715
1729
  results[pname]["success"] = True
1716
1730
  else:
1717
1731
  results[pname]["success"] = False
@@ -1929,13 +1943,22 @@ class PetalUserJourneyCoordinator(Petal):
1929
1943
 
1930
1944
  @http_action(method="POST", path="/mqtt/reboot_px4")
1931
1945
  async def _reboot_px4_message_handler(self, topic: str, message: Dict[str, Any]):
1932
- """Handle reboot PX4 MQTT messages."""
1946
+ """
1947
+ Handle reboot PX4 MQTT messages.
1948
+
1949
+ Flow:
1950
+ 1. Validate message and check for active operations
1951
+ 2. Immediately send_command_response to acknowledge command received
1952
+ 3. Await reboot_autopilot to execute the reboot
1953
+ 4. Publish reboot status via MQTT publish_message
1954
+ """
1955
+ mqtt_msg = None
1933
1956
  try:
1934
- # Parse base MQTT message
1957
+ # Parse and validate MQTT message
1935
1958
  mqtt_msg = MQTTMessage(**message)
1936
1959
  message_id = mqtt_msg.messageId
1937
1960
 
1938
- # Check if controller is in emergency mode
1961
+ # Check if any controller has an active operation
1939
1962
  for controller in self._active_controllers.values():
1940
1963
  if controller is not None and controller.is_active:
1941
1964
  error_msg = "PX4 reboot blocked - Active operation in progress"
@@ -1949,61 +1972,137 @@ class PetalUserJourneyCoordinator(Petal):
1949
1972
  "error_code": "OPERATION_ACTIVE"
1950
1973
  }
1951
1974
  )
1952
-
1953
1975
  return
1954
-
1955
- logger.info(f"Restarting PX4 for {self.name} petal")
1956
- reboot_response = await self._mavlink_proxy.reboot_autopilot(
1957
- reboot_onboard_computer=False,
1958
- timeout=5.0
1959
- )
1960
-
1961
- if not reboot_response.success:
1962
- error_msg = "PX4 reboot command failed or timed out"
1963
- logger.error(f"[{message_id}] {error_msg}")
1964
- if mqtt_msg.waitResponse:
1965
- await self._mqtt_proxy.send_command_response(
1966
- message_id=message_id,
1967
- response_data={
1968
- "status": "error",
1969
- "message": error_msg,
1970
- "error_code": "REBOOT_FAILED",
1971
- "data": _json_safe(reboot_response.model_dump())
1972
- }
1973
- )
1974
- return
1975
-
1976
- logger.info(f"[{message_id}] PX4 reboot command successful")
1977
1976
 
1977
+ # Step 1: Immediately respond to acknowledge command received
1978
+ logger.info(f"[{message_id}] PX4 reboot command received, sending acknowledgement")
1978
1979
  if mqtt_msg.waitResponse:
1979
1980
  await self._mqtt_proxy.send_command_response(
1980
1981
  message_id=message_id,
1981
1982
  response_data={
1982
1983
  "status": "success",
1983
- "message": "PX4 reboot command successful",
1984
- "data": _json_safe(reboot_response.model_dump())
1984
+ "message": "PX4 reboot command initiated",
1985
+ "data": {
1986
+ "reboot_initiated": True,
1987
+ "message": "Reboot in progress, status will be published to command/web"
1988
+ }
1985
1989
  }
1986
1990
  )
1991
+
1987
1992
  except ValidationError as ve:
1988
- error_msg = f"Invalid PX4 reboot payload: {ve}"
1993
+ error_msg = f"Invalid PX4 reboot message: {ve}"
1989
1994
  logger.error(f"PX4 reboot validation error: {error_msg}")
1990
- if mqtt_msg.waitResponse:
1995
+ if mqtt_msg and mqtt_msg.waitResponse:
1991
1996
  await self._mqtt_proxy.send_command_response(
1992
- message_id=message_id,
1997
+ message_id=mqtt_msg.messageId,
1993
1998
  response_data={
1994
1999
  "status": "error",
1995
2000
  "message": error_msg,
1996
2001
  "error_code": "VALIDATION_ERROR"
1997
2002
  }
1998
2003
  )
2004
+ return
1999
2005
  except Exception as e:
2000
- logger.error(f"Error handling PX4 reboot message: {e}")
2006
+ error_msg = f"PX4 reboot message handler error: {str(e)}"
2007
+ logger.error(f"Unexpected PX4 reboot error: {error_msg}")
2001
2008
  if mqtt_msg and mqtt_msg.waitResponse:
2002
2009
  await self._mqtt_proxy.send_command_response(
2003
2010
  message_id=mqtt_msg.messageId,
2004
- response_data={"status": "error", "message": str(e)}
2011
+ response_data={
2012
+ "status": "error",
2013
+ "message": error_msg,
2014
+ "error_code": "HANDLER_ERROR"
2015
+ }
2005
2016
  )
2006
- return
2017
+ return
2018
+
2019
+ try:
2020
+ # Step 2: Await the actual reboot operation
2021
+ logger.info(f"[{message_id}] Executing PX4 reboot for {self.name} petal")
2022
+ reboot_response: RebootAutopilotResponse = await self._mavlink_proxy.reboot_autopilot(
2023
+ reboot_onboard_computer=False,
2024
+ timeout=5.0
2025
+ )
2026
+
2027
+ # Step 3: Build and publish reboot status via MQTT
2028
+ if reboot_response.success:
2029
+ logger.info(f"[{message_id}] PX4 reboot successful: {reboot_response.reason}")
2030
+ status_payload = RebootPX4StatusPayload(
2031
+ reboot_initiated=True,
2032
+ reboot_success=True,
2033
+ status="success",
2034
+ message=reboot_response.reason,
2035
+ error_code=None,
2036
+ timestamp=datetime.now(timezone.utc)
2037
+ )
2038
+ else:
2039
+ logger.error(f"[{message_id}] PX4 reboot failed: {reboot_response.reason}")
2040
+ status_payload = RebootPX4StatusPayload(
2041
+ reboot_initiated=True,
2042
+ reboot_success=False,
2043
+ status="failed",
2044
+ message=reboot_response.reason,
2045
+ error_code=reboot_response.status_code.value,
2046
+ timestamp=datetime.now(timezone.utc)
2047
+ )
2048
+
2049
+ # Publish reboot status via MQTT
2050
+ mqtt_message = {
2051
+ "messageId": message_id,
2052
+ "command": f"/{self.name}/reboot_px4_status",
2053
+ "timestamp": datetime.now(timezone.utc).isoformat(),
2054
+ "payload": status_payload.model_dump(mode="json")
2055
+ }
2056
+ await self._mqtt_proxy.publish_message(payload=mqtt_message)
2057
+ logger.info(f"[{message_id}] Published reboot status to command/web")
2058
+
2059
+ except ValidationError as ve:
2060
+ error_msg = f"PX4 reboot status payload validation error: {ve}"
2061
+ logger.error(f"[{mqtt_msg.messageId if mqtt_msg else 'unknown'}] {error_msg}")
2062
+ # Try to publish error status if we got past validation
2063
+ if mqtt_msg:
2064
+ try:
2065
+ error_payload = RebootPX4StatusPayload(
2066
+ reboot_initiated=True,
2067
+ reboot_success=False,
2068
+ status="failed",
2069
+ message=error_msg,
2070
+ error_code="PAYLOAD_VALIDATION_ERROR",
2071
+ timestamp=datetime.now(timezone.utc)
2072
+ )
2073
+ mqtt_message = {
2074
+ "messageId": mqtt_msg.messageId,
2075
+ "command": f"/{self.name}/reboot_px4_status",
2076
+ "timestamp": datetime.now(timezone.utc).isoformat(),
2077
+ "payload": error_payload.model_dump(mode="json")
2078
+ }
2079
+ await self._mqtt_proxy.publish_message(payload=mqtt_message)
2080
+ except Exception as publish_error:
2081
+ logger.error(f"[{mqtt_msg.messageId}] Failed to publish error status: {publish_error}")
2082
+
2083
+ except Exception as e:
2084
+ error_msg = f"PX4 reboot error: {str(e)}"
2085
+ logger.error(f"[{mqtt_msg.messageId if mqtt_msg else 'unknown'}] {error_msg}")
2086
+ # Try to publish error status if we got past validation
2087
+ if mqtt_msg:
2088
+ try:
2089
+ error_payload = RebootPX4StatusPayload(
2090
+ reboot_initiated=True,
2091
+ reboot_success=False,
2092
+ status="failed",
2093
+ message=error_msg,
2094
+ error_code="EXECUTION_ERROR",
2095
+ timestamp=datetime.now(timezone.utc)
2096
+ )
2097
+ mqtt_message = {
2098
+ "messageId": mqtt_msg.messageId,
2099
+ "command": f"/{self.name}/reboot_px4_status",
2100
+ "timestamp": datetime.now(timezone.utc).isoformat(),
2101
+ "payload": error_payload.model_dump(mode="json")
2102
+ }
2103
+ await self._mqtt_proxy.publish_message(payload=mqtt_message)
2104
+ except Exception as publish_error:
2105
+ logger.error(f"[{mqtt_msg.messageId}] Failed to publish error status: {publish_error}")
2007
2106
 
2008
2107
  def _create_parameter_message_handler(self, handler_key: str, config_type: str):
2009
2108
  """
@@ -2589,7 +2688,17 @@ class PetalUserJourneyCoordinator(Petal):
2589
2688
  results[pname] = confirmed[pname]
2590
2689
  # check that the set value matches the requested value
2591
2690
  confirmed_value = results[pname].get("value")
2592
- if confirmed_value == parameter.parameter_value:
2691
+ requested_value = parameter.parameter_value
2692
+
2693
+ # Check for equality, handling floating point precision issues
2694
+ is_match = False
2695
+ if isinstance(confirmed_value, (float, int)) and isinstance(requested_value, (float, int)):
2696
+ # Use 1e-5 relative tolerance to handle float32/float64 mismatch
2697
+ is_match = math.isclose(confirmed_value, requested_value, rel_tol=1e-5)
2698
+ else:
2699
+ is_match = confirmed_value == requested_value
2700
+
2701
+ if is_match:
2593
2702
  results[pname]["success"] = True
2594
2703
  else:
2595
2704
  results[pname]["success"] = False