Skip to content

Commit e3b5f23

Browse files
committed
Use ephemeral OTA Provider instances
Instead of trying to reuse the same OTA Provider instance for multiple OTA requests, create a new instance for each request. This is easier to implement and also allows parallel updates. Because the OTA Provider is now ephemeral, we need to commission it on every update. But this is quick and reliable, so not a big deal. To support multiple updates at once, we need to make sure the OTA Providers use a distinct Matter port (hence passing 0) and distinct node ids. The current implementation simply uses the target node id plus a fixed offset. Since a single node can only run one update at a time, this is sufficient. Furthermore, some updates seem to have a difference in reported versionNumberString value in the DCL vs. what is actually in the OTA metadata. Specifically Eve updates from the Testnet DCL are such updates (e.g. 3.2.0 vs 3.2.6705). When using the OTA Provider with the --otaImageList option, this discrepancy is an issue and causes OTA Provider to abort. Using the single OTA update per OTA Provider instance allows us to use --filepath, which doesn't check the versionNumberString.
1 parent 45e693e commit e3b5f23

File tree

2 files changed

+96
-195
lines changed

2 files changed

+96
-195
lines changed

matter_server/server/device_controller.py

+29-26
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
from .const import DATA_MODEL_SCHEMA_VERSION
6161

6262
if TYPE_CHECKING:
63-
from collections.abc import Iterable
63+
from collections.abc import Callable, Iterable
6464
from pathlib import Path
6565

6666
from chip.native import PyChipError
@@ -110,11 +110,6 @@
110110
0, Clusters.BasicInformation.Attributes.SoftwareVersionString
111111
)
112112
)
113-
OTA_SOFTWARE_UPDATE_REQUESTOR_UPDATE_STATE_ATTRIBUTE_PATH = (
114-
create_attribute_path_from_attribute(
115-
0, Clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState
116-
)
117-
)
118113

119114

120115
# pylint: disable=too-many-lines,too-many-instance-attributes,too-many-public-methods
@@ -131,6 +126,7 @@ def __init__(
131126
):
132127
"""Initialize the device controller."""
133128
self.server = server
129+
self._ota_provider_dir = ota_provider_dir
134130

135131
self._chip_device_controller = ChipDeviceControllerWrapper(
136132
server, paa_root_cert_dir
@@ -157,15 +153,14 @@ def __init__(
157153
self._polled_attributes: dict[int, set[str]] = {}
158154
self._custom_attribute_poller_timer: asyncio.TimerHandle | None = None
159155
self._custom_attribute_poller_task: asyncio.Task | None = None
160-
self._ota_provider = ExternalOtaProvider(server.vendor_id, ota_provider_dir)
156+
self._attribute_update_callbacks: dict[int, list[Callable]] = {}
161157

162158
async def initialize(self) -> None:
163159
"""Initialize the device controller."""
164160
self._compressed_fabric_id = (
165161
await self._chip_device_controller.get_compressed_fabric_id()
166162
)
167163
self._fabric_id_hex = hex(self._compressed_fabric_id)[2:]
168-
await self._ota_provider.initialize()
169164

170165
async def start(self) -> None:
171166
"""Handle logic on controller start."""
@@ -229,9 +224,6 @@ async def stop(self) -> None:
229224

230225
# shutdown the sdk device controller
231226
await self._chip_device_controller.shutdown()
232-
# shutdown the OTA Provider
233-
if self._ota_provider:
234-
await self._ota_provider.stop()
235227
LOGGER.debug("Stopped.")
236228

237229
@property
@@ -916,14 +908,29 @@ async def update_node(
916908
)
917909

918910
# Add update to the OTA provider
919-
await self._ota_provider.download_update(update)
911+
ota_provider = ExternalOtaProvider(
912+
self.server.vendor_id, self._ota_provider_dir / f"{node_id}"
913+
)
920914

921-
# Make sure any previous instances get stopped
922-
await self._ota_provider.start_update(
923-
self._chip_device_controller,
924-
node_id,
915+
await ota_provider.initialize()
916+
917+
await ota_provider.download_update(update)
918+
919+
self._attribute_update_callbacks.setdefault(node_id, []).append(
920+
ota_provider.check_update_state
925921
)
926922

923+
try:
924+
# Make sure any previous instances get stopped
925+
await ota_provider.start_update(
926+
self._chip_device_controller,
927+
node_id,
928+
)
929+
finally:
930+
self._attribute_update_callbacks[node_id].remove(
931+
ota_provider.check_update_state
932+
)
933+
927934
return update
928935

929936
async def _check_node_update(
@@ -1017,16 +1024,6 @@ def attribute_updated(
10171024
# schedule a full interview of the node if the software version changed
10181025
loop.create_task(self.interview_node(node_id))
10191026

1020-
# work out if update state changed
1021-
if (
1022-
str(path) == OTA_SOFTWARE_UPDATE_REQUESTOR_UPDATE_STATE_ATTRIBUTE_PATH
1023-
and new_value != old_value
1024-
):
1025-
if self._ota_provider:
1026-
loop.create_task(
1027-
self._ota_provider.check_update_state(node_id, new_value)
1028-
)
1029-
10301027
# store updated value in node attributes
10311028
node.attributes[str(path)] = new_value
10321029

@@ -1058,6 +1055,11 @@ def attribute_updated_callback(
10581055
return
10591056

10601057
loop.call_soon_threadsafe(attribute_updated, path, old_value, new_value)
1058+
if node_id in self._attribute_update_callbacks:
1059+
for callback in self._attribute_update_callbacks[node_id]:
1060+
asyncio.run_coroutine_threadsafe(
1061+
callback(path, old_value, new_value), loop
1062+
)
10611063

10621064
def event_callback(
10631065
data: Attribute.EventReadResult,
@@ -1084,6 +1086,7 @@ def event_callback(
10841086
data=data.Data,
10851087
)
10861088
self.event_history.append(node_event)
1089+
10871090
loop.call_soon_threadsafe(
10881091
self.server.signal_event, EventType.NODE_EVENT, node_event
10891092
)

0 commit comments

Comments
 (0)