From 6aa9a7da25f761d7b5948902c9c27d0bc40e7f9c Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:09:08 +1000
Subject: [PATCH 1/8] api_client: update from spec
Update the api_client generation from the latest spec.
Signed-off-by: Jordan Yates
---
.../api/default/get_devices_by_board_id.py | 194 ++++++++++++++++++
src/infuse_iot/api_client/models/__init__.py | 4 +
src/infuse_iot/api_client/models/board.py | 34 ++-
src/infuse_iot/api_client/models/device.py | 24 ++-
.../api_client/models/metadata_field.py | 74 +++++++
src/infuse_iot/api_client/models/new_board.py | 34 ++-
.../api_client/models/new_device.py | 24 ++-
.../api_client/models/new_device_metadata.py | 48 +++++
8 files changed, 432 insertions(+), 4 deletions(-)
create mode 100644 src/infuse_iot/api_client/api/default/get_devices_by_board_id.py
create mode 100644 src/infuse_iot/api_client/models/metadata_field.py
create mode 100644 src/infuse_iot/api_client/models/new_device_metadata.py
diff --git a/src/infuse_iot/api_client/api/default/get_devices_by_board_id.py b/src/infuse_iot/api_client/api/default/get_devices_by_board_id.py
new file mode 100644
index 0000000..687fc16
--- /dev/null
+++ b/src/infuse_iot/api_client/api/default/get_devices_by_board_id.py
@@ -0,0 +1,194 @@
+from http import HTTPStatus
+from typing import Any, Dict, List, Optional, Union, cast
+
+import httpx
+
+from ... import errors
+from ...client import AuthenticatedClient, Client
+from ...models.device import Device
+from ...types import UNSET, Response, Unset
+
+
+def _get_kwargs(
+ id: str,
+ *,
+ metadata_name: Union[Unset, str] = UNSET,
+ metadata_value: Union[Unset, str] = UNSET,
+) -> Dict[str, Any]:
+ params: Dict[str, Any] = {}
+
+ params["metadataName"] = metadata_name
+
+ params["metadataValue"] = metadata_value
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: Dict[str, Any] = {
+ "method": "get",
+ "url": f"/board/id/{id}/devices",
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Union[Any, List["Device"]]]:
+ if response.status_code == HTTPStatus.OK:
+ response_200 = []
+ _response_200 = response.json()
+ for response_200_item_data in _response_200:
+ response_200_item = Device.from_dict(response_200_item_data)
+
+ response_200.append(response_200_item)
+
+ return response_200
+ if response.status_code == HTTPStatus.NOT_FOUND:
+ response_404 = cast(Any, None)
+ return response_404
+ if client.raise_on_unexpected_status:
+ raise errors.UnexpectedStatus(response.status_code, response.content)
+ else:
+ return None
+
+
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Union[Any, List["Device"]]]:
+ return Response(
+ status_code=HTTPStatus(response.status_code),
+ content=response.content,
+ headers=response.headers,
+ parsed=_parse_response(client=client, response=response),
+ )
+
+
+def sync_detailed(
+ id: str,
+ *,
+ client: Union[AuthenticatedClient, Client],
+ metadata_name: Union[Unset, str] = UNSET,
+ metadata_value: Union[Unset, str] = UNSET,
+) -> Response[Union[Any, List["Device"]]]:
+ """Get devices by board id and optional metadata field
+
+ Args:
+ id (str):
+ metadata_name (Union[Unset, str]):
+ metadata_value (Union[Unset, str]):
+
+ Raises:
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Response[Union[Any, List['Device']]]
+ """
+
+ kwargs = _get_kwargs(
+ id=id,
+ metadata_name=metadata_name,
+ metadata_value=metadata_value,
+ )
+
+ response = client.get_httpx_client().request(
+ **kwargs,
+ )
+
+ return _build_response(client=client, response=response)
+
+
+def sync(
+ id: str,
+ *,
+ client: Union[AuthenticatedClient, Client],
+ metadata_name: Union[Unset, str] = UNSET,
+ metadata_value: Union[Unset, str] = UNSET,
+) -> Optional[Union[Any, List["Device"]]]:
+ """Get devices by board id and optional metadata field
+
+ Args:
+ id (str):
+ metadata_name (Union[Unset, str]):
+ metadata_value (Union[Unset, str]):
+
+ Raises:
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Union[Any, List['Device']]
+ """
+
+ return sync_detailed(
+ id=id,
+ client=client,
+ metadata_name=metadata_name,
+ metadata_value=metadata_value,
+ ).parsed
+
+
+async def asyncio_detailed(
+ id: str,
+ *,
+ client: Union[AuthenticatedClient, Client],
+ metadata_name: Union[Unset, str] = UNSET,
+ metadata_value: Union[Unset, str] = UNSET,
+) -> Response[Union[Any, List["Device"]]]:
+ """Get devices by board id and optional metadata field
+
+ Args:
+ id (str):
+ metadata_name (Union[Unset, str]):
+ metadata_value (Union[Unset, str]):
+
+ Raises:
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Response[Union[Any, List['Device']]]
+ """
+
+ kwargs = _get_kwargs(
+ id=id,
+ metadata_name=metadata_name,
+ metadata_value=metadata_value,
+ )
+
+ response = await client.get_async_httpx_client().request(**kwargs)
+
+ return _build_response(client=client, response=response)
+
+
+async def asyncio(
+ id: str,
+ *,
+ client: Union[AuthenticatedClient, Client],
+ metadata_name: Union[Unset, str] = UNSET,
+ metadata_value: Union[Unset, str] = UNSET,
+) -> Optional[Union[Any, List["Device"]]]:
+ """Get devices by board id and optional metadata field
+
+ Args:
+ id (str):
+ metadata_name (Union[Unset, str]):
+ metadata_value (Union[Unset, str]):
+
+ Raises:
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Union[Any, List['Device']]
+ """
+
+ return (
+ await asyncio_detailed(
+ id=id,
+ client=client,
+ metadata_name=metadata_name,
+ metadata_value=metadata_value,
+ )
+ ).parsed
diff --git a/src/infuse_iot/api_client/models/__init__.py b/src/infuse_iot/api_client/models/__init__.py
index 31286f9..a89386d 100644
--- a/src/infuse_iot/api_client/models/__init__.py
+++ b/src/infuse_iot/api_client/models/__init__.py
@@ -9,8 +9,10 @@
from .error import Error
from .health_check import HealthCheck
from .key import Key
+from .metadata_field import MetadataField
from .new_board import NewBoard
from .new_device import NewDevice
+from .new_device_metadata import NewDeviceMetadata
from .new_organisation import NewOrganisation
from .organisation import Organisation
@@ -24,8 +26,10 @@
"Error",
"HealthCheck",
"Key",
+ "MetadataField",
"NewBoard",
"NewDevice",
+ "NewDeviceMetadata",
"NewOrganisation",
"Organisation",
)
diff --git a/src/infuse_iot/api_client/models/board.py b/src/infuse_iot/api_client/models/board.py
index 71a2277..fff26b8 100644
--- a/src/infuse_iot/api_client/models/board.py
+++ b/src/infuse_iot/api_client/models/board.py
@@ -1,10 +1,16 @@
import datetime
-from typing import Any, Dict, List, Type, TypeVar
+from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.metadata_field import MetadataField
+
+
T = TypeVar("T", bound="Board")
@@ -19,6 +25,8 @@ class Board:
description (str): Description of board Example: Extended description of board.
soc (str): System on Chip (SoC) of board Example: nRF9151.
organisation_id (str): ID of organisation for board to exist in
+ metadata_fields (Union[Unset, List['MetadataField']]): Metadata fields for board Example: [{'name': 'Field
+ Name', 'required': True, 'unique': False}].
"""
id: str
@@ -28,6 +36,7 @@ class Board:
description: str
soc: str
organisation_id: str
+ metadata_fields: Union[Unset, List["MetadataField"]] = UNSET
additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
@@ -45,6 +54,15 @@ def to_dict(self) -> Dict[str, Any]:
organisation_id = self.organisation_id
+ metadata_fields: Union[Unset, List[Dict[str, Any]]] = UNSET
+ if not isinstance(self.metadata_fields, Unset):
+ metadata_fields = []
+ for componentsschemas_board_metadata_fields_item_data in self.metadata_fields:
+ componentsschemas_board_metadata_fields_item = (
+ componentsschemas_board_metadata_fields_item_data.to_dict()
+ )
+ metadata_fields.append(componentsschemas_board_metadata_fields_item)
+
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
@@ -58,11 +76,15 @@ def to_dict(self) -> Dict[str, Any]:
"organisationId": organisation_id,
}
)
+ if metadata_fields is not UNSET:
+ field_dict["metadataFields"] = metadata_fields
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ from ..models.metadata_field import MetadataField
+
d = src_dict.copy()
id = d.pop("id")
@@ -78,6 +100,15 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
organisation_id = d.pop("organisationId")
+ metadata_fields = []
+ _metadata_fields = d.pop("metadataFields", UNSET)
+ for componentsschemas_board_metadata_fields_item_data in _metadata_fields or []:
+ componentsschemas_board_metadata_fields_item = MetadataField.from_dict(
+ componentsschemas_board_metadata_fields_item_data
+ )
+
+ metadata_fields.append(componentsschemas_board_metadata_fields_item)
+
board = cls(
id=id,
created_at=created_at,
@@ -86,6 +117,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
description=description,
soc=soc,
organisation_id=organisation_id,
+ metadata_fields=metadata_fields,
)
board.additional_properties = d
diff --git a/src/infuse_iot/api_client/models/device.py b/src/infuse_iot/api_client/models/device.py
index e033a03..958a99f 100644
--- a/src/infuse_iot/api_client/models/device.py
+++ b/src/infuse_iot/api_client/models/device.py
@@ -1,5 +1,5 @@
import datetime
-from typing import Any, Dict, List, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
@@ -7,6 +7,10 @@
from ..types import UNSET, Unset
+if TYPE_CHECKING:
+ from ..models.new_device_metadata import NewDeviceMetadata
+
+
T = TypeVar("T", bound="Device")
@@ -22,6 +26,7 @@ class Device:
organisation_id (str): ID of organisation for board to exist in
device_id (Union[Unset, str]): 8 byte DeviceID as a hex string (if not provided will be auto-generated) Example:
d291d4d66bf0a955.
+ metadata (Union[Unset, NewDeviceMetadata]): Metadata fields for device Example: {'Field Name': 'Field Value'}.
"""
id: str
@@ -31,6 +36,7 @@ class Device:
board_id: str
organisation_id: str
device_id: Union[Unset, str] = UNSET
+ metadata: Union[Unset, "NewDeviceMetadata"] = UNSET
additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
@@ -48,6 +54,10 @@ def to_dict(self) -> Dict[str, Any]:
device_id = self.device_id
+ metadata: Union[Unset, Dict[str, Any]] = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
@@ -62,11 +72,15 @@ def to_dict(self) -> Dict[str, Any]:
)
if device_id is not UNSET:
field_dict["deviceId"] = device_id
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ from ..models.new_device_metadata import NewDeviceMetadata
+
d = src_dict.copy()
id = d.pop("id")
@@ -82,6 +96,13 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
device_id = d.pop("deviceId", UNSET)
+ _metadata = d.pop("metadata", UNSET)
+ metadata: Union[Unset, NewDeviceMetadata]
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = NewDeviceMetadata.from_dict(_metadata)
+
device = cls(
id=id,
created_at=created_at,
@@ -90,6 +111,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
board_id=board_id,
organisation_id=organisation_id,
device_id=device_id,
+ metadata=metadata,
)
device.additional_properties = d
diff --git a/src/infuse_iot/api_client/models/metadata_field.py b/src/infuse_iot/api_client/models/metadata_field.py
new file mode 100644
index 0000000..8143960
--- /dev/null
+++ b/src/infuse_iot/api_client/models/metadata_field.py
@@ -0,0 +1,74 @@
+from typing import Any, Dict, List, Type, TypeVar
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+T = TypeVar("T", bound="MetadataField")
+
+
+@_attrs_define
+class MetadataField:
+ """
+ Attributes:
+ name (str): Name of metadata field Example: Field Name.
+ required (bool): Whether field is required Example: True.
+ unique (bool): Whether field is unique (across all devices of the same board)
+ """
+
+ name: str
+ required: bool
+ unique: bool
+ additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> Dict[str, Any]:
+ name = self.name
+
+ required = self.required
+
+ unique = self.unique
+
+ field_dict: Dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "name": name,
+ "required": required,
+ "unique": unique,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ d = src_dict.copy()
+ name = d.pop("name")
+
+ required = d.pop("required")
+
+ unique = d.pop("unique")
+
+ metadata_field = cls(
+ name=name,
+ required=required,
+ unique=unique,
+ )
+
+ metadata_field.additional_properties = d
+ return metadata_field
+
+ @property
+ def additional_keys(self) -> List[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/src/infuse_iot/api_client/models/new_board.py b/src/infuse_iot/api_client/models/new_board.py
index faa2a12..76d8d5e 100644
--- a/src/infuse_iot/api_client/models/new_board.py
+++ b/src/infuse_iot/api_client/models/new_board.py
@@ -1,8 +1,14 @@
-from typing import Any, Dict, List, Type, TypeVar
+from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.metadata_field import MetadataField
+
+
T = TypeVar("T", bound="NewBoard")
@@ -14,12 +20,15 @@ class NewBoard:
description (str): Description of board Example: Extended description of board.
soc (str): System on Chip (SoC) of board Example: nRF9151.
organisation_id (str): ID of organisation for board to exist in
+ metadata_fields (Union[Unset, List['MetadataField']]): Metadata fields for board Example: [{'name': 'Field
+ Name', 'required': True, 'unique': False}].
"""
name: str
description: str
soc: str
organisation_id: str
+ metadata_fields: Union[Unset, List["MetadataField"]] = UNSET
additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
@@ -31,6 +40,15 @@ def to_dict(self) -> Dict[str, Any]:
organisation_id = self.organisation_id
+ metadata_fields: Union[Unset, List[Dict[str, Any]]] = UNSET
+ if not isinstance(self.metadata_fields, Unset):
+ metadata_fields = []
+ for componentsschemas_board_metadata_fields_item_data in self.metadata_fields:
+ componentsschemas_board_metadata_fields_item = (
+ componentsschemas_board_metadata_fields_item_data.to_dict()
+ )
+ metadata_fields.append(componentsschemas_board_metadata_fields_item)
+
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
@@ -41,11 +59,15 @@ def to_dict(self) -> Dict[str, Any]:
"organisationId": organisation_id,
}
)
+ if metadata_fields is not UNSET:
+ field_dict["metadataFields"] = metadata_fields
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ from ..models.metadata_field import MetadataField
+
d = src_dict.copy()
name = d.pop("name")
@@ -55,11 +77,21 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
organisation_id = d.pop("organisationId")
+ metadata_fields = []
+ _metadata_fields = d.pop("metadataFields", UNSET)
+ for componentsschemas_board_metadata_fields_item_data in _metadata_fields or []:
+ componentsschemas_board_metadata_fields_item = MetadataField.from_dict(
+ componentsschemas_board_metadata_fields_item_data
+ )
+
+ metadata_fields.append(componentsschemas_board_metadata_fields_item)
+
new_board = cls(
name=name,
description=description,
soc=soc,
organisation_id=organisation_id,
+ metadata_fields=metadata_fields,
)
new_board.additional_properties = d
diff --git a/src/infuse_iot/api_client/models/new_device.py b/src/infuse_iot/api_client/models/new_device.py
index 5f12723..e5b58c7 100644
--- a/src/infuse_iot/api_client/models/new_device.py
+++ b/src/infuse_iot/api_client/models/new_device.py
@@ -1,10 +1,14 @@
-from typing import Any, Dict, List, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
+if TYPE_CHECKING:
+ from ..models.new_device_metadata import NewDeviceMetadata
+
+
T = TypeVar("T", bound="NewDevice")
@@ -17,12 +21,14 @@ class NewDevice:
organisation_id (str): ID of organisation for board to exist in
device_id (Union[Unset, str]): 8 byte DeviceID as a hex string (if not provided will be auto-generated) Example:
d291d4d66bf0a955.
+ metadata (Union[Unset, NewDeviceMetadata]): Metadata fields for device Example: {'Field Name': 'Field Value'}.
"""
mcu_id: str
board_id: str
organisation_id: str
device_id: Union[Unset, str] = UNSET
+ metadata: Union[Unset, "NewDeviceMetadata"] = UNSET
additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
@@ -34,6 +40,10 @@ def to_dict(self) -> Dict[str, Any]:
device_id = self.device_id
+ metadata: Union[Unset, Dict[str, Any]] = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
@@ -45,11 +55,15 @@ def to_dict(self) -> Dict[str, Any]:
)
if device_id is not UNSET:
field_dict["deviceId"] = device_id
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ from ..models.new_device_metadata import NewDeviceMetadata
+
d = src_dict.copy()
mcu_id = d.pop("mcuId")
@@ -59,11 +73,19 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
device_id = d.pop("deviceId", UNSET)
+ _metadata = d.pop("metadata", UNSET)
+ metadata: Union[Unset, NewDeviceMetadata]
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = NewDeviceMetadata.from_dict(_metadata)
+
new_device = cls(
mcu_id=mcu_id,
board_id=board_id,
organisation_id=organisation_id,
device_id=device_id,
+ metadata=metadata,
)
new_device.additional_properties = d
diff --git a/src/infuse_iot/api_client/models/new_device_metadata.py b/src/infuse_iot/api_client/models/new_device_metadata.py
new file mode 100644
index 0000000..e34d494
--- /dev/null
+++ b/src/infuse_iot/api_client/models/new_device_metadata.py
@@ -0,0 +1,48 @@
+from typing import Any, Dict, List, Type, TypeVar
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+T = TypeVar("T", bound="NewDeviceMetadata")
+
+
+@_attrs_define
+class NewDeviceMetadata:
+ """Metadata fields for device
+
+ Example:
+ {'Field Name': 'Field Value'}
+
+ """
+
+ additional_properties: Dict[str, str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> Dict[str, Any]:
+ field_dict: Dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+ d = src_dict.copy()
+ new_device_metadata = cls()
+
+ new_device_metadata.additional_properties = d
+ return new_device_metadata
+
+ @property
+ def additional_keys(self) -> List[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
From 34b27ae0aa8dcb231a03680398d2c5096839de20 Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:02:52 +1000
Subject: [PATCH 2/8] rpc_wrappers: updates and new RPC
Update existing RPCs with new options and add `application_info`.
Signed-off-by: Jordan Yates
---
.gitignore | 1 +
src/infuse_iot/generated/rpc_definitions.py | 2 +
.../rpc_wrappers/application_info.py | 55 +++++++++++++++++++
.../rpc_wrappers/bt_connect_infuse.py | 7 +++
.../rpc_wrappers/file_write_basic.py | 7 +++
5 files changed, 72 insertions(+)
create mode 100644 src/infuse_iot/rpc_wrappers/application_info.py
diff --git a/.gitignore b/.gitignore
index 3fafd07..ab166da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
__pycache__
*.egg-info
+.vscode
diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py
index 7edfc68..c2f3812 100644
--- a/src/infuse_iot/generated/rpc_definitions.py
+++ b/src/infuse_iot/generated/rpc_definitions.py
@@ -150,6 +150,7 @@ class rpc_enum_file_action(enum.IntEnum):
DISCARD = 0
APP_IMG = 1
BT_CTLR_IMG = 2
+ APP_CPATCH = 11
NRF91_MODEM_DIFF = 20
@@ -158,4 +159,5 @@ class rpc_enum_infuse_bt_characteristic(enum.IntEnum):
COMMAND = 1
DATA = 2
+ LOGGING = 4
diff --git a/src/infuse_iot/rpc_wrappers/application_info.py b/src/infuse_iot/rpc_wrappers/application_info.py
new file mode 100644
index 0000000..dcc6bad
--- /dev/null
+++ b/src/infuse_iot/rpc_wrappers/application_info.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+
+import ctypes
+
+from infuse_iot.commands import InfuseRpcCommand
+from infuse_iot.generated.rpc_definitions import rpc_struct_mcuboot_img_sem_ver
+
+
+class application_info(InfuseRpcCommand):
+ HELP = "Get the current application info"
+ DESCRIPTION = "Get the current application info"
+ COMMAND_ID = 9
+
+ class request(ctypes.LittleEndianStructure):
+ _fields_ = []
+ _pack_ = 1
+
+ class response(ctypes.LittleEndianStructure):
+ _fields_ = [
+ ("application_id", ctypes.c_uint32),
+ ("version", rpc_struct_mcuboot_img_sem_ver),
+ ("network_id", ctypes.c_uint32),
+ ("uptime", ctypes.c_uint32),
+ ("reboots", ctypes.c_uint32),
+ ("kv_crc", ctypes.c_uint32),
+ ("data_blocks_internal", ctypes.c_uint32),
+ ("data_blocks_external", ctypes.c_uint32),
+ ]
+ _pack_ = 1
+
+ @classmethod
+ def add_parser(cls, _parser):
+ pass
+
+ def __init__(self, _args):
+ pass
+
+ def request_struct(self):
+ return self.request()
+
+ def handle_response(self, return_code, response):
+ if return_code != 0:
+ print(f"Failed to query current time ({return_code})")
+ return
+
+ r = response
+ v = r.version
+ print(f"\tApplication: 0x{r.application_id:08x}")
+ print(f"\t Version: {v.major}.{v.minor}.{v.revision}+{v.build_num:08x}")
+ print(f"\t Network: 0x{r.network_id:08x}")
+ print(f"\t Uptime: {r.uptime}")
+ print(f"\t Reboots: {r.reboots}")
+ print(f"\t KV CRC: 0x{r.kv_crc:08x}")
+ print(f"\t O Blocks: {r.data_blocks_internal}")
+ print(f"\t E Blocks: {r.data_blocks_external}")
diff --git a/src/infuse_iot/rpc_wrappers/bt_connect_infuse.py b/src/infuse_iot/rpc_wrappers/bt_connect_infuse.py
index 24bbdc5..3e4c000 100644
--- a/src/infuse_iot/rpc_wrappers/bt_connect_infuse.py
+++ b/src/infuse_iot/rpc_wrappers/bt_connect_infuse.py
@@ -45,6 +45,11 @@ def add_parser(cls, parser):
parser.add_argument(
"--data", action="store_true", help="Subscribe to data characteristic"
)
+ parser.add_argument(
+ "--logging",
+ action="store_true",
+ help="Subscribe to serial logging characteristic",
+ )
addr_group = parser.add_mutually_exclusive_group(required=True)
addr_group.add_argument(
"--public", type=BtLeAddress, help="Public Bluetooth address"
@@ -72,6 +77,8 @@ def request_struct(self):
sub = rpc_enum_infuse_bt_characteristic.COMMAND
if self.args.data:
sub |= rpc_enum_infuse_bt_characteristic.DATA
+ if self.args.logging:
+ sub |= rpc_enum_infuse_bt_characteristic.LOGGING
return self.request(
peer,
diff --git a/src/infuse_iot/rpc_wrappers/file_write_basic.py b/src/infuse_iot/rpc_wrappers/file_write_basic.py
index 0a5095a..319109f 100644
--- a/src/infuse_iot/rpc_wrappers/file_write_basic.py
+++ b/src/infuse_iot/rpc_wrappers/file_write_basic.py
@@ -58,6 +58,13 @@ def add_parser(cls, parser):
const=rpc_enum_file_action.APP_IMG,
help="Write complete image file and perform DFU",
)
+ group.add_argument(
+ "--cpatch",
+ dest="action",
+ action="store_const",
+ const=rpc_enum_file_action.APP_CPATCH,
+ help="Write complete image file and perform DFU",
+ )
group.add_argument(
"--bt-ctlr-dfu",
dest="action",
From 564ae30e17245a11e4a167ea819fff9ab088603d Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 15:04:56 +1000
Subject: [PATCH 3/8] database: generate Bluetooth advertising keys
Update the database to support generating the derived Bluetooth
advertising keys.
Signed-off-by: Jordan Yates
---
src/infuse_iot/database.py | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py
index b22753e..531a2ed 100644
--- a/src/infuse_iot/database.py
+++ b/src/infuse_iot/database.py
@@ -46,7 +46,6 @@ def observe_serial(
"""Update device state based on observed packet"""
if self.gateway is None:
self.gateway = address
- print("GATEWAY", hex(self.gateway))
if address not in self.devices:
self.devices[address] = self.DeviceState(address)
if network_id is not None:
@@ -83,6 +82,9 @@ def observe_security_state(
def _serial_key(self, base, time_idx):
return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"serial")
+ def _bt_adv_key(self, base, time_idx):
+ return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"bt_adv")
+
def has_public_key(self, address: int):
"""Does the database have the public key for this device?"""
if address not in self.devices:
@@ -112,3 +114,26 @@ def serial_device_key(self, address: int, gps_time: int):
time_idx = gps_time // (60 * 60 * 24)
return self._serial_key(base, time_idx)
+
+ def bt_adv_network_key(self, address: int, gps_time: int):
+ """Network key for Bluetooth advertising interface"""
+ if address not in self.devices:
+ raise NoKeyError
+ base = self._network_keys[self.devices[address].network_id]
+ time_idx = gps_time // (60 * 60 * 24)
+
+ return self._bt_adv_key(base, time_idx)
+
+ def bt_adv_device_key(self, address: int, gps_time: int):
+ """Device key for Bluetooth advertising interface"""
+ if address not in self.devices:
+ raise NoKeyError
+ d = self.devices[address]
+ if d.device_id is None:
+ raise NoKeyError
+ base = self.devices[address].shared_key
+ if base is None:
+ raise NoKeyError
+ time_idx = gps_time // (60 * 60 * 24)
+
+ return self._bt_adv_key(base, time_idx)
From 1b48041cf38cb3e4ef520491304a3e896e630794 Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 15:07:11 +1000
Subject: [PATCH 4/8] epacket: support decrypting Bluetooth advertising
Add support for decrypting Bluetooth advertising payloads.
Signed-off-by: Jordan Yates
---
src/infuse_iot/epacket.py | 132 +++++++++++++++++++++++++++-----------
1 file changed, 93 insertions(+), 39 deletions(-)
diff --git a/src/infuse_iot/epacket.py b/src/infuse_iot/epacket.py
index aee328b..a4abef7 100644
--- a/src/infuse_iot/epacket.py
+++ b/src/infuse_iot/epacket.py
@@ -10,7 +10,7 @@
from typing_extensions import Self
from infuse_iot.util.crypto import chachapoly_decrypt, chachapoly_encrypt
-from infuse_iot.database import DeviceDatabase
+from infuse_iot.database import DeviceDatabase, NoKeyError
from infuse_iot.time import InfuseTime
@@ -235,17 +235,7 @@ def from_json(cls, values: Dict) -> Self:
@classmethod
def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self]:
- header = CtypeSerialFrame.from_buffer_copy(serial_frame)
- if header.flags & Flags.ENCR_DEVICE:
- database.observe_serial(header.device_id, device_id=header.key_metadata)
- key = database.serial_device_key(header.device_id, header.gps_time)
- else:
- database.observe_serial(header.device_id, network_id=header.key_metadata)
- key = database.serial_network_key(header.device_id, header.gps_time)
-
- decrypted = chachapoly_decrypt(
- key, serial_frame[:11], serial_frame[11:23], serial_frame[23:]
- )
+ header, decrypted = CtypeSerialFrame.decrypt(database, serial_frame)
# Packet from local gateway
if header.type != InfuseType.RECEIVED_EPACKET:
@@ -263,32 +253,65 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self
# Only Bluetooth advertising supported for now
if common_header.interface != Interface.BT_ADV:
raise NotImplementedError
- # Decrypting payloads not currently supported
- if common_header.encrypted:
- raise NotImplementedError
# Extract interface address (Only Bluetooth supported)
addr = InterfaceAddress.from_bytes(common_header.interface, packet_bytes)
del packet_bytes[: addr.len()]
- # Extract payload metadata
- decr_header = CtypePacketReceived.DecryptedHeader.from_buffer_copy(
- packet_bytes
- )
- del packet_bytes[: ctypes.sizeof(decr_header)]
-
- bt_hop = HopReceived(
- decr_header.device_id,
- common_header.interface,
- addr,
- Auth.DEVICE if decr_header.flags & Flags.ENCR_DEVICE else Auth.NETWORK,
- decr_header.key_id,
- decr_header.gps_time,
- decr_header.sequence,
- common_header.rssi,
- )
- packet = cls(
- [bt_hop, header.hop_received()], decr_header.type, bytes(packet_bytes)
- )
+
+ # Decrypting packet
+ if common_header.encrypted:
+ try:
+ bt_header, bt_decrypted = CtypeBtAdvFrame.decrypt(
+ database, packet_bytes
+ )
+ except NoKeyError:
+ continue
+
+ bt_hop = HopReceived(
+ bt_header.device_id,
+ common_header.interface,
+ addr,
+ (
+ Auth.DEVICE
+ if bt_header.flags & Flags.ENCR_DEVICE
+ else Auth.NETWORK
+ ),
+ bt_header.key_metadata,
+ bt_header.gps_time,
+ bt_header.sequence,
+ common_header.rssi,
+ )
+ packet = cls(
+ [bt_hop, header.hop_received()],
+ bt_header.type,
+ bytes(bt_decrypted),
+ )
+ else:
+ # Extract payload metadata
+ decr_header = CtypePacketReceived.DecryptedHeader.from_buffer_copy(
+ packet_bytes
+ )
+ del packet_bytes[: ctypes.sizeof(decr_header)]
+
+ bt_hop = HopReceived(
+ decr_header.device_id,
+ common_header.interface,
+ addr,
+ (
+ Auth.DEVICE
+ if decr_header.flags & Flags.ENCR_DEVICE
+ else Auth.NETWORK
+ ),
+ decr_header.key_id,
+ decr_header.gps_time,
+ decr_header.sequence,
+ common_header.rssi,
+ )
+ packet = cls(
+ [bt_hop, header.hop_received()],
+ decr_header.type,
+ bytes(packet_bytes),
+ )
packets.append(packet)
return packets
@@ -354,9 +377,7 @@ def from_json(cls, values: Dict) -> Self:
)
-class CtypeSerialFrame(ctypes.Structure):
- """Serial packet header"""
-
+class CtypeV0VersionedFrame(ctypes.Structure):
_fields_ = [
("version", ctypes.c_uint8),
("_type", ctypes.c_uint8),
@@ -395,10 +416,14 @@ def device_id(self, value):
def parse(cls, frame: bytes) -> Tuple[Self, int]:
"""Parse serial frame into header and payload length"""
return (
- CtypeSerialFrame.from_buffer_copy(frame),
- len(frame) - ctypes.sizeof(CtypeSerialFrame) - 16,
+ CtypeV0VersionedFrame.from_buffer_copy(frame),
+ len(frame) - ctypes.sizeof(CtypeV0VersionedFrame) - 16,
)
+
+class CtypeSerialFrame(CtypeV0VersionedFrame):
+ """Serial packet header"""
+
def hop_received(self) -> HopReceived:
auth = Auth.DEVICE if self.flags & Flags.ENCR_DEVICE else Auth.NETWORK
return HopReceived(
@@ -412,6 +437,35 @@ def hop_received(self) -> HopReceived:
0,
)
+ @classmethod
+ def decrypt(cls, database: DeviceDatabase, frame: bytes):
+ header = cls.from_buffer_copy(frame)
+ if header.flags & Flags.ENCR_DEVICE:
+ database.observe_serial(header.device_id, device_id=header.key_metadata)
+ key = database.serial_device_key(header.device_id, header.gps_time)
+ else:
+ database.observe_serial(header.device_id, network_id=header.key_metadata)
+ key = database.serial_network_key(header.device_id, header.gps_time)
+
+ decrypted = chachapoly_decrypt(key, frame[:11], frame[11:23], frame[23:])
+ return header, decrypted
+
+
+class CtypeBtAdvFrame(CtypeV0VersionedFrame):
+ """Bluetooth Advertising packet header"""
+
+ @classmethod
+ def decrypt(cls, database: DeviceDatabase, frame: bytes):
+ header = cls.from_buffer_copy(frame)
+ if header.flags & Flags.ENCR_DEVICE:
+ raise NotImplementedError
+ else:
+ database.observe_serial(header.device_id, network_id=header.key_metadata)
+ key = database.bt_adv_network_key(header.device_id, header.gps_time)
+
+ decrypted = chachapoly_decrypt(key, frame[:11], frame[11:23], frame[23:])
+ return header, decrypted
+
class CtypePacketReceived:
class CommonHeader(ctypes.Structure):
From 0d266583e2ad235ff661349fe514343f765350e8 Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:03:51 +1000
Subject: [PATCH 5/8] tools: gateway: proactively query keys
Proactively query devices keys if a `KEY_IDS` packet is received.
Signed-off-by: Jordan Yates
---
src/infuse_iot/tools/gateway.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py
index e32e673..5ab8c32 100644
--- a/src/infuse_iot/tools/gateway.py
+++ b/src/infuse_iot/tools/gateway.py
@@ -200,6 +200,9 @@ def _handle_serial_frame(self, frame):
# Handle any Memfault chunks
if pkt.ptype == InfuseType.MEMFAULT_CHUNK:
self._handle_memfault_pkt(pkt)
+ # Proactively requery keys
+ elif pkt.ptype == InfuseType.KEY_IDS:
+ self._common.query_device_key(None)
# Forward to clients
self._common.server.broadcast(pkt)
except (ValueError, KeyError) as e:
From eb5c1259d8c8a6d23eb15d6ef8c4d513207f7523 Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:04:43 +1000
Subject: [PATCH 6/8] tools: tdf_list: use new string formatting
Update the string formatting of TDFs from the new definitions.
Signed-off-by: Jordan Yates
---
src/infuse_iot/generated/tdf_definitions.py | 291 +++++++++++++++++---
src/infuse_iot/tools/tdf_list.py | 3 +-
2 files changed, 252 insertions(+), 42 deletions(-)
diff --git a/src/infuse_iot/generated/tdf_definitions.py b/src/infuse_iot/generated/tdf_definitions.py
index 5bcfbdd..50969b0 100644
--- a/src/infuse_iot/generated/tdf_definitions.py
+++ b/src/infuse_iot/generated/tdf_definitions.py
@@ -17,7 +17,7 @@ def iter_fields(self) -> Generator[str, ctypes._SimpleCData, str, Callable]:
else:
f_name = field[0]
val = getattr(self, f_name)
- yield f_name, val, self._postfix_[f_name], self._display_fn_.get(f_name)
+ yield f_name, val, self._postfix_[f_name], self._display_fmt_[f_name]
class tdf_struct_mcuboot_img_sem_ver(_struct_type):
"""MCUboot semantic versioning struct"""
@@ -35,8 +35,11 @@ class tdf_struct_mcuboot_img_sem_ver(_struct_type):
"revision": "",
"build_num": "",
}
- _display_fn_ = {
- "build_num": hex,
+ _display_fmt_ = {
+ "major": "{}",
+ "minor": "{}",
+ "revision": "{}",
+ "build_num": "0x{:08x}",
}
class tdf_struct_xyz_16bit(_struct_type):
@@ -53,7 +56,10 @@ class tdf_struct_xyz_16bit(_struct_type):
"y": "",
"z": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "x": "{}",
+ "y": "{}",
+ "z": "{}",
}
class tdf_struct_gcs_location(_struct_type):
@@ -70,7 +76,10 @@ class tdf_struct_gcs_location(_struct_type):
"longitude": "deg",
"height": "m",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "latitude": "{:.5f}",
+ "longitude": "{:.5f}",
+ "height": "{:.3f}",
}
@property
@@ -97,7 +106,9 @@ class tdf_struct_lte_cell_id_local(_struct_type):
"eci": "",
"tac": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "eci": "{}",
+ "tac": "{}",
}
class tdf_struct_lte_cell_id_global(_struct_type):
@@ -116,7 +127,11 @@ class tdf_struct_lte_cell_id_global(_struct_type):
"eci": "",
"tac": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "mcc": "{}",
+ "mnc": "{}",
+ "eci": "{}",
+ "tac": "{}",
}
class readings:
@@ -129,12 +144,12 @@ def iter_fields(self) -> Generator[str, ctypes._SimpleCData, str, Callable]:
f_name = field[0]
val = getattr(self, f_name)
if isinstance(val, ctypes.LittleEndianStructure):
- for subfield_name, subfield_val, subfield_postfix, display_fn in val.iter_fields():
- yield f'{f_name}.{subfield_name}', subfield_val, subfield_postfix, display_fn
+ for subfield_name, subfield_val, subfield_postfix, display_fmt in val.iter_fields():
+ yield f'{f_name}.{subfield_name}', subfield_val, subfield_postfix, display_fmt
elif isinstance(val, ctypes.Array):
- yield f_name, list(val), self._postfix_[f_name], self._display_fn_.get(f_name)
+ yield f_name, list(val), self._postfix_[f_name], self._display_fmt_[f_name]
else:
- yield f_name, val, self._postfix_[f_name], self._display_fn_.get(f_name)
+ yield f_name, val, self._postfix_[f_name], self._display_fmt_[f_name]
class announce(_reading_type):
"""Common announcement packet"""
@@ -159,9 +174,14 @@ class announce(_reading_type):
"reboots": "",
"flags": "",
}
- _display_fn_ = {
- "kv_crc": hex,
- "flags": hex,
+ _display_fmt_ = {
+ "application": "0x{:08x}",
+ "version": "{}",
+ "kv_crc": "0x{:08x}",
+ "blocks": "{}",
+ "uptime": "{}",
+ "reboots": "{}",
+ "flags": "0x{:02x}",
}
class battery_state(_reading_type):
@@ -179,7 +199,10 @@ class battery_state(_reading_type):
"current_ua": "uA",
"soc": "%",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "voltage_mv": "{}",
+ "current_ua": "{}",
+ "soc": "{}",
}
class ambient_temp_pres_hum(_reading_type):
@@ -194,10 +217,13 @@ class ambient_temp_pres_hum(_reading_type):
_pack_ = 1
_postfix_ = {
"temperature": "deg",
- "pressure": "kPa",
+ "pressure": "kPA",
"humidity": "%",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "temperature": "{:.3f}",
+ "pressure": "{:.3f}",
+ "humidity": "{:.2f}",
}
@property
@@ -223,7 +249,8 @@ class ambient_temperature(_reading_type):
_postfix_ = {
"temperature": "deg",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "temperature": "{:.3f}",
}
@property
@@ -243,7 +270,9 @@ class time_sync(_reading_type):
"source": "",
"shift": "us",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "source": "{}",
+ "shift": "{}",
}
@property
@@ -273,10 +302,14 @@ class reboot_info(_reading_type):
"param_2": "",
"thread": "",
}
- _display_fn_ = {
- "hardware_flags": hex,
- "param_1": hex,
- "param_2": hex,
+ _display_fmt_ = {
+ "reason": "{}",
+ "hardware_flags": "0x{:08x}",
+ "count": "{}",
+ "uptime": "{}",
+ "param_1": "0x{:08x}",
+ "param_2": "0x{:08x}",
+ "thread": "{}",
}
class acc_2g(_reading_type):
@@ -290,7 +323,8 @@ class acc_2g(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class acc_4g(_reading_type):
@@ -304,7 +338,8 @@ class acc_4g(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class acc_8g(_reading_type):
@@ -318,7 +353,8 @@ class acc_8g(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class acc_16g(_reading_type):
@@ -332,7 +368,8 @@ class acc_16g(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gyr_125dps(_reading_type):
@@ -346,7 +383,8 @@ class gyr_125dps(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gyr_250dps(_reading_type):
@@ -360,7 +398,8 @@ class gyr_250dps(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gyr_500dps(_reading_type):
@@ -374,7 +413,8 @@ class gyr_500dps(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gyr_1000dps(_reading_type):
@@ -388,7 +428,8 @@ class gyr_1000dps(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gyr_2000dps(_reading_type):
@@ -402,7 +443,8 @@ class gyr_2000dps(_reading_type):
_postfix_ = {
"sample": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "sample": "{}",
}
class gcs_wgs84_llha(_reading_type):
@@ -420,7 +462,10 @@ class gcs_wgs84_llha(_reading_type):
"h_acc": "m",
"v_acc": "m",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "location": "{}",
+ "h_acc": "{:.3f}",
+ "v_acc": "{:.3f}",
}
@property
@@ -506,11 +551,40 @@ class ubx_nav_pvt(_reading_type):
"mag_dec": "deg",
"mag_acc": "deg",
}
- _display_fn_ = {
- "valid": hex,
- "flags": hex,
- "flags2": hex,
- "flags3": hex,
+ _display_fmt_ = {
+ "itow": "{}",
+ "year": "{}",
+ "month": "{}",
+ "day": "{}",
+ "hour": "{}",
+ "min": "{}",
+ "sec": "{}",
+ "valid": "{}",
+ "t_acc": "{}",
+ "nano": "{}",
+ "fix_type": "{}",
+ "flags": "{}",
+ "flags2": "{}",
+ "num_sv": "{}",
+ "lon": "{}",
+ "lat": "{}",
+ "height": "{}",
+ "h_msl": "{}",
+ "h_acc": "{}",
+ "v_acc": "{}",
+ "vel_n": "{}",
+ "vel_e": "{}",
+ "vel_d": "{}",
+ "g_speed": "{}",
+ "head_mot": "{}",
+ "s_acc": "{}",
+ "head_acc": "{}",
+ "p_dop": "{}",
+ "flags3": "{}",
+ "reserved0": "{}",
+ "head_veh": "{}",
+ "mag_dec": "{}",
+ "mag_acc": "{}",
}
@property
@@ -602,13 +676,142 @@ class lte_conn_status(_reading_type):
"rsrp": "dBm",
"rsrq": "dB",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "cell": "{}",
+ "earfcn": "{}",
+ "status": "{}",
+ "tech": "{}",
+ "rsrp": "{}",
+ "rsrq": "{}",
}
@property
def rsrp(self):
return self._rsrp * -1
+ class globalstar_pkt(_reading_type):
+ """9 byte payload transmitted over the Globalstar Simplex network"""
+
+ name = "GLOBALSTAR_PKT"
+ _fields_ = [
+ ("payload", 9 * ctypes.c_uint8),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "payload": "",
+ }
+ _display_fmt_ = {
+ "payload": "{}",
+ }
+
+ class acc_magnitude_std_dev(_reading_type):
+ """Accelerometer magnitude standard deviation over a window"""
+
+ name = "ACC_MAGNITUDE_STD_DEV"
+ _fields_ = [
+ ("count", ctypes.c_uint32),
+ ("std_dev", ctypes.c_uint32),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "count": "",
+ "std_dev": "",
+ }
+ _display_fmt_ = {
+ "count": "{}",
+ "std_dev": "{}",
+ }
+
+ class activity_metric(_reading_type):
+ """Generic activity metric"""
+
+ name = "ACTIVITY_METRIC"
+ _fields_ = [
+ ("value", ctypes.c_uint32),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "value": "",
+ }
+ _display_fmt_ = {
+ "value": "{}",
+ }
+
+ class algorithm_output(_reading_type):
+ """Generic activity metric"""
+
+ name = "ALGORITHM_OUTPUT"
+ _fields_ = [
+ ("algorithm_id", ctypes.c_uint32),
+ ("algorithm_version", ctypes.c_uint16),
+ ("output", 0 * ctypes.c_uint8),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "algorithm_id": "",
+ "algorithm_version": "",
+ "output": "",
+ }
+ _display_fmt_ = {
+ "algorithm_id": "{}",
+ "algorithm_version": "{}",
+ "output": "{}",
+ }
+
+ class runtime_error(_reading_type):
+ """Runtime error logging"""
+
+ name = "RUNTIME_ERROR"
+ _fields_ = [
+ ("error_id", ctypes.c_uint32),
+ ("error_ctx", ctypes.c_uint32),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "error_id": "",
+ "error_ctx": "",
+ }
+ _display_fmt_ = {
+ "error_id": "{}",
+ "error_ctx": "{}",
+ }
+
+ class charger_en_control(_reading_type):
+ """Battery charging enable state"""
+
+ name = "CHARGER_EN_CONTROL"
+ _fields_ = [
+ ("enabled", ctypes.c_uint8),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "enabled": "",
+ }
+ _display_fmt_ = {
+ "enabled": "{}",
+ }
+
+ class gnss_fix_info(_reading_type):
+ """Metadata about a GNSS location fix"""
+
+ name = "GNSS_FIX_INFO"
+ _fields_ = [
+ ("time_fix", ctypes.c_uint16),
+ ("location_fix", ctypes.c_uint16),
+ ("num_sv", ctypes.c_uint8),
+ ]
+ _pack_ = 1
+ _postfix_ = {
+ "time_fix": "",
+ "location_fix": "",
+ "num_sv": "",
+ }
+ _display_fmt_ = {
+ "time_fix": "{}",
+ "location_fix": "{}",
+ "num_sv": "{}",
+ }
+
class array_type(_reading_type):
"""Example array type"""
@@ -620,7 +823,8 @@ class array_type(_reading_type):
_postfix_ = {
"array": "",
}
- _display_fn_ = {
+ _display_fmt_ = {
+ "array": "{}",
}
id_type_mapping = {
@@ -642,5 +846,12 @@ class array_type(_reading_type):
19: readings.gcs_wgs84_llha,
20: readings.ubx_nav_pvt,
21: readings.lte_conn_status,
+ 22: readings.globalstar_pkt,
+ 23: readings.acc_magnitude_std_dev,
+ 24: readings.activity_metric,
+ 25: readings.algorithm_output,
+ 26: readings.runtime_error,
+ 27: readings.charger_en_control,
+ 28: readings.gnss_fix_info,
100: readings.array_type,
}
diff --git a/src/infuse_iot/tools/tdf_list.py b/src/infuse_iot/tools/tdf_list.py
index ee6d55b..591c134 100644
--- a/src/infuse_iot/tools/tdf_list.py
+++ b/src/infuse_iot/tools/tdf_list.py
@@ -44,8 +44,7 @@ def run(self):
name = t.name
for idx, (n, f, p, d) in enumerate(t.iter_fields()):
- if d is not None:
- f = d(f)
+ f = d.format(f)
if idx == 0:
if tdf.time is not None:
if tdf.period is None:
From 497e0813ec2a12f0a29acfb2315699b4f6f6306b Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:05:33 +1000
Subject: [PATCH 7/8] tools: provision: support device metadata
Support specifying device metadata when provisioning a device.
Signed-off-by: Jordan Yates
---
src/infuse_iot/tools/provision.py | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/src/infuse_iot/tools/provision.py b/src/infuse_iot/tools/provision.py
index 9b58af1..e17b691 100644
--- a/src/infuse_iot/tools/provision.py
+++ b/src/infuse_iot/tools/provision.py
@@ -24,7 +24,7 @@
get_boards,
get_board_by_id,
)
-from infuse_iot.api_client.models import NewDevice
+from infuse_iot.api_client.models import NewDevice, NewDeviceMetadata
from infuse_iot.credentials import get_api_key
from infuse_iot.commands import InfuseCommand
@@ -52,7 +52,18 @@ def add_parser(cls, parser):
parser.add_argument("--board", "-b", type=str, help="Board ID")
parser.add_argument("--organisation", "-o", type=str, help="Organisation ID")
parser.add_argument(
- "--id", "-i", type=int, help="Infuse device ID to provision as"
+ "--id",
+ "-i",
+ type=lambda x: int(x, 0),
+ help="Infuse device ID to provision as",
+ )
+ parser.add_argument(
+ "--metadata",
+ "-m",
+ metavar="KEY=VALUE",
+ nargs="+",
+ type=str,
+ help="Define a number of key-value pairs for metadata",
)
def __init__(self, args):
@@ -60,6 +71,11 @@ def __init__(self, args):
self._board = args.board
self._org = args.organisation
self._id = args.id
+ self._metadata = {}
+ if args.metadata:
+ for meta in args.metadata:
+ key, val = meta.strip().split("=", 1)
+ self._metadata[key.strip()] = val
def nrf_device_info(self, api: LowLevel.API) -> tuple[int, int]:
"""Retrive device ID and customer UICR address"""
@@ -138,7 +154,10 @@ def create_device(self, client, soc, hardware_id_str):
)
new_board = NewDevice(
- mcu_id=hardware_id_str, organisation_id=self._org, board_id=self._board
+ mcu_id=hardware_id_str,
+ organisation_id=self._org,
+ board_id=self._board,
+ metadata=NewDeviceMetadata.from_dict(self._metadata),
)
if self._id:
new_board.device_id = f"{self._id:016x}"
From 7a24bb1fbbf93ae57c312e9b245ab799a53886bc Mon Sep 17 00:00:00 2001
From: Jordan Yates
Date: Mon, 18 Nov 2024 14:06:12 +1000
Subject: [PATCH 8/8] tools: localhost: added
Add a basic local webserver for displaying recent TDFs.
Signed-off-by: Jordan Yates
---
pyproject.toml | 1 +
src/infuse_iot/tools/localhost.py | 215 ++++++++++++++++++++++
src/infuse_iot/tools/localhost/index.html | 114 ++++++++++++
3 files changed, 330 insertions(+)
create mode 100644 src/infuse_iot/tools/localhost.py
create mode 100644 src/infuse_iot/tools/localhost/index.html
diff --git a/pyproject.toml b/pyproject.toml
index 385047a..e073f3c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ classifiers = [
requires-python = ">=3.10"
dependencies = [
"argcomplete",
+ "aiohttp",
"attrs",
"cryptography",
"colorama",
diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py
new file mode 100644
index 0000000..6d37aa8
--- /dev/null
+++ b/src/infuse_iot/tools/localhost.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+
+"""Run a local server for TDF viewing"""
+
+__author__ = "Jordan Yates"
+__copyright__ = "Copyright 2024, Embeint Inc"
+
+import asyncio
+import ctypes
+import json
+import pathlib
+import threading
+import time
+from aiohttp import web
+from aiohttp.web_runner import GracefulExit
+
+from infuse_iot.epacket import InfuseType, Interface
+from infuse_iot.commands import InfuseCommand
+from infuse_iot.socket_comms import LocalClient, default_multicast_address
+from infuse_iot.tdf import TDF
+from infuse_iot.time import InfuseTime
+
+
+class SubCommand(InfuseCommand):
+ NAME = "localhost"
+ HELP = "Run a local server for TDF viewing"
+ DESCRIPTION = "Run a local server for TDF viewing"
+
+ def __init__(self, _):
+ self._data_lock = threading.Lock()
+ self._columns = {}
+ self._data = {}
+
+ self._thread_end = threading.Event()
+ self._client = LocalClient(default_multicast_address(), 1.0)
+ self._decoder = TDF()
+
+ # Serve the HTML file
+ async def handle_index(self, request):
+ this_folder = pathlib.Path(__file__).parent
+
+ return web.FileResponse(this_folder / "localhost" / "index.html")
+
+ async def websocket_handler(self, request):
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+
+ try:
+ while True:
+ # Example data sent to the client
+ self._data_lock.acquire(blocking=True)
+ columns = [
+ {
+ "title": "Metadata",
+ "headerHozAlign": "center",
+ "frozen": True,
+ "columns": [
+ {
+ "title": "Device",
+ "field": "infuse_id",
+ "headerHozAlign": "center",
+ },
+ {
+ "title": "Last Heard",
+ "field": "time",
+ "headerHozAlign": "center",
+ },
+ {
+ "title": "Bluetooth",
+ "headerHozAlign": "center",
+ "columns": [
+ {
+ "title": "Address",
+ "field": "bt_addr",
+ "headerHozAlign": "center",
+ },
+ {
+ "title": "RSSI (dBm)",
+ "field": "bt_rssi",
+ "headerVertical": "flip",
+ "hozAlign": "right",
+ },
+ ],
+ },
+ ],
+ }
+ ]
+ for tdf_name in sorted(self._columns):
+ columns.append(
+ {
+ "title": tdf_name,
+ "field": tdf_name,
+ "columns": self._columns[tdf_name],
+ "headerHozAlign": "center",
+ }
+ )
+ devices = sorted(self._data.keys())
+ message = {
+ "columns": columns,
+ "rows": [self._data[d] for d in devices],
+ "tdfs": list(self._columns.keys()),
+ }
+ self._data_lock.release()
+
+ await ws.send_str(json.dumps(message))
+ await asyncio.sleep(1)
+ except asyncio.CancelledError:
+ print("WebSocket connection closed")
+ finally:
+ await ws.close()
+
+ return ws
+
+ def tdf_columns(self, tdf):
+ out = []
+ for field in tdf._fields_:
+ if field[0][0] == "_":
+ f_name = field[0][1:]
+ else:
+ f_name = field[0]
+ val = getattr(tdf, f_name)
+ if isinstance(val, ctypes.LittleEndianStructure):
+ c = []
+ for subfield_name, _, postfix, _ in val.iter_fields():
+ if postfix != "":
+ title = f"{subfield_name} ({postfix})"
+ else:
+ title = subfield_name
+ c.append(
+ {
+ "title": title,
+ "field": f"{tdf.name}.{f_name}.{subfield_name}",
+ "headerVertical": "flip",
+ "hozAlign": "right",
+ }
+ )
+ s = {"title": f_name, "headerHozAlign": "center", "columns": c}
+ else:
+ if tdf._postfix_[f_name] != "":
+ title = f"{f_name} ({tdf._postfix_[f_name]})"
+ else:
+ title = f_name
+
+ s = {
+ "title": title,
+ "field": f"{tdf.name}.{f_name}",
+ "headerVertical": "flip",
+ "hozAlign": "right",
+ }
+ out.append(s)
+ return out
+
+ def recv_thread(self):
+ while True:
+ msg = self._client.receive()
+ if self._thread_end.is_set():
+ break
+ if msg is None:
+ continue
+ if msg.ptype != InfuseType.TDF:
+ continue
+
+ decoded = self._decoder.decode(msg.payload)
+ source = msg.route[0]
+
+ self._data_lock.acquire(blocking=True)
+
+ if source.infuse_id not in self._data:
+ self._data[source.infuse_id] = {
+ "infuse_id": f"0x{source.infuse_id:016x}",
+ }
+ self._data[source.infuse_id]["time"] = InfuseTime.utc_time_string(
+ time.time()
+ )
+ if source.interface == Interface.BT_ADV:
+ addr_bytes = source.interface_address.val.addr_val.to_bytes(6, "big")
+ addr_str = ":".join([f"{x:02x}" for x in addr_bytes])
+ self._data[source.infuse_id]["bt_addr"] = addr_str
+ self._data[source.infuse_id]["bt_rssi"] = source.rssi
+
+ for tdf in decoded:
+ t = tdf.data[-1]
+ if t.name not in self._columns:
+ self._columns[t.name] = self.tdf_columns(t)
+
+ for n, f, _, d in t.iter_fields():
+ f = d.format(f)
+ if t.name not in self._data[source.infuse_id]:
+ self._data[source.infuse_id][t.name] = {}
+ if "." in n:
+ struct, sub = n.split(".")
+ if struct not in self._data[source.infuse_id][t.name]:
+ self._data[source.infuse_id][t.name][struct] = {}
+ self._data[source.infuse_id][t.name][struct][sub] = f
+
+ self._data[source.infuse_id][t.name][n] = f
+
+ self._data_lock.release()
+
+ def run(self):
+ app = web.Application()
+ # Route for serving the HTML file
+ app.router.add_get("/", self.handle_index)
+ # Route for WebSocket
+ app.router.add_get("/ws", self.websocket_handler)
+
+ rx_thread = threading.Thread(target=self.recv_thread)
+ rx_thread.start()
+
+ # Run server
+ try:
+ web.run_app(app, host="localhost", port=8080)
+ except GracefulExit:
+ self._thread_end.set()
+ rx_thread.join(1.0)
diff --git a/src/infuse_iot/tools/localhost/index.html b/src/infuse_iot/tools/localhost/index.html
new file mode 100644
index 0000000..6713d8b
--- /dev/null
+++ b/src/infuse_iot/tools/localhost/index.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ TDF Viewer
+
+
+
+
+
+
+ Infuse-IoT TDF Viewer
+
+
+
+
+
Table Control
+
+
+
+
+
+
+