From a1a9e7ef240c0fe8bffc346af805febeb1bbc0a6 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 6 Mar 2026 10:59:12 +1000 Subject: [PATCH 1/2] tools: localhost: browser requests data Switch the data updates from an unconditional 1Hz data send by the python server to a browser requested 1Hz poll model. This fixes two problems. If the browser suspends the tab due to inactivity, there is no longer a backlog of data to process when the user returns, as the requests were also suspended. The python server gets immediate notification when the browser disconnects, as it is handling events from the browser. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/localhost.py | 8 ++++++-- src/infuse_iot/tools/localhost/index.html | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index 3d3e32e..addc189 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -15,6 +15,7 @@ from aiohttp import web from aiohttp.web_request import BaseRequest from aiohttp.web_runner import GracefulExit +from aiohttp.client_exceptions import WSMessageTypeError import infuse_iot.epacket.interface as interface import infuse_iot.epacket.packet as packet @@ -67,6 +68,9 @@ async def websocket_handler(self, request: BaseRequest): try: while True: + # Wait for request from browser (contents don't matter) + _ = await ws.receive_str() + # Example data sent to the client self._data_lock.acquire(blocking=True) columns = [ @@ -139,8 +143,8 @@ async def websocket_handler(self, request: BaseRequest): self._data_lock.release() await ws.send_json(message) - await asyncio.sleep(1) - except (asyncio.CancelledError, ConnectionResetError): + + except (asyncio.CancelledError, ConnectionResetError, WSMessageTypeError): pass finally: await ws.close() diff --git a/src/infuse_iot/tools/localhost/index.html b/src/infuse_iot/tools/localhost/index.html index 2055f9a..15ca515 100644 --- a/src/infuse_iot/tools/localhost/index.html +++ b/src/infuse_iot/tools/localhost/index.html @@ -143,6 +143,7 @@

Infuse-IoT TDF Viewer

let current_num_columns = 0; let currentApps = []; let currentNetworks = [] + let dataRequestId = 0; const arraysEqual = (a, b) => a.length === b.length && @@ -246,12 +247,22 @@

Infuse-IoT TDF Viewer

} }; + function dataUpdateRequest() { + ws.send("dataUpdateRequest"); + } + ws.onopen = function () { console.log("WebSocket connected"); + // Initial data request + dataUpdateRequest(); + // Setup data request interval + dataRequestId = setInterval(dataUpdateRequest, 1000); }; ws.onerror = function (error) { console.error("WebSocket error:", error); + // Cancel data requests + clearInterval(dataRequestId); }; From 67db23861a513260741fdfc64b5e68878d7eddbc Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 6 Mar 2026 11:26:03 +1000 Subject: [PATCH 2/2] tools: localhost: extract message generation Extract websocket message generation to a dedicated function. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/localhost.py | 146 +++++++++++++++--------------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index addc189..68aad5f 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -13,9 +13,9 @@ from typing import Any from aiohttp import web +from aiohttp.client_exceptions import WSMessageTypeError from aiohttp.web_request import BaseRequest from aiohttp.web_runner import GracefulExit -from aiohttp.client_exceptions import WSMessageTypeError import infuse_iot.epacket.interface as interface import infuse_iot.epacket.packet as packet @@ -60,6 +60,78 @@ async def handle_index(self, _request): return web.FileResponse(this_folder / "localhost" / "index.html") + def websocket_message(self) -> dict: + self._data_lock.acquire(blocking=True) + columns = [ + { + "title": "Metadata", + "headerHozAlign": "center", + "frozen": True, + "columns": [ + { + "title": "Device", + "field": "infuse_id", + "headerHozAlign": "center", + }, + { + "title": "App ID", + "field": "application", + "headerHozAlign": "center", + }, + { + "title": "Network", + "field": "network_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", + }, + ], + }, + ], + } + ] + # Put the announce TDFs first for clarity + priorities = {"ANNOUNCE_V2": 0, "ANNOUNCE": 1} + sorted_tdfs = sorted(self._columns, key=lambda x: priorities.get(x, 2)) + + for tdf_name in sorted_tdfs: + 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": sorted(list(self._columns.keys())), + "apps": sorted(list(self._apps)), + "networks": sorted(list(self._networks)), + } + self._data_lock.release() + return message + async def websocket_handler(self, request: BaseRequest): ws = web.WebSocketResponse() await ws.prepare(request) @@ -71,76 +143,8 @@ async def websocket_handler(self, request: BaseRequest): # Wait for request from browser (contents don't matter) _ = await ws.receive_str() - # 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": "App ID", - "field": "application", - "headerHozAlign": "center", - }, - { - "title": "Network", - "field": "network_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", - }, - ], - }, - ], - } - ] - # Put the announce TDFs first for clarity - priorities = {"ANNOUNCE_V2": 0, "ANNOUNCE": 1} - sorted_tdfs = sorted(self._columns, key=lambda x: priorities.get(x, 2)) - - for tdf_name in sorted_tdfs: - 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": sorted(list(self._columns.keys())), - "apps": sorted(list(self._apps)), - "networks": sorted(list(self._networks)), - } - self._data_lock.release() + # Data sent to the client + message = self.websocket_message() await ws.send_json(message)