Skip to content

Commit

Permalink
Merge pull request #720 from ImperialCollegeLondon/net-devices-async
Browse files Browse the repository at this point in the history
Add async open support for various network-based devices
  • Loading branch information
alexdewar authored Dec 18, 2024
2 parents 894b8a1 + 9e5d3ad commit 6680116
Show file tree
Hide file tree
Showing 6 changed files with 38 additions and 37 deletions.
10 changes: 7 additions & 3 deletions finesse/hardware/plugins/sensors/decades.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Decades(
"documentation."
),
},
async_open=True,
):
"""A class for monitoring a DECADES sensor server."""

Expand All @@ -119,14 +120,13 @@ def __init__(
self._params: list[DecadesParameter]
"""Parameters returned by the server."""

super().__init__(poll_interval)

# Obtain full parameter list in order to parse received data
self.obtain_parameter_list(
frozenset(params.split(",")) if params else frozenset()
)

# We only want to start polling after we have loaded the parameter list
super().__init__(poll_interval, start_polling=False)

def obtain_parameter_list(self, params: Set[str]) -> None:
"""Request the parameter list from the DECADES server and wait for response."""
self._requester.make_request(
Expand Down Expand Up @@ -204,5 +204,9 @@ def _on_params_received(self, reply: QNetworkReply, params: Set[str]) -> None:
else:
self._params = list(_get_selected_params(all_params_info, params))

# Tell the frontend that the device is ready
self.signal_is_opened()

# Now we have enough information to start parsing sensor readings
self.start_polling()
self.request_readings()
15 changes: 14 additions & 1 deletion finesse/hardware/plugins/sensors/em27_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ class EM27Error(Exception):
"""Indicates than an error occurred while parsing the webpage."""


class EM27SensorsBase(SensorsBase, class_type=DeviceClassType.IGNORE):
class EM27SensorsBase(
SensorsBase,
class_type=DeviceClassType.IGNORE,
async_open=True,
):
"""An interface for monitoring EM27 properties."""

def __init__(self, url: str, poll_interval: float = float("nan")) -> None:
Expand All @@ -62,9 +66,12 @@ def __init__(self, url: str, poll_interval: float = float("nan")) -> None:
"""
self._url: str = url
self._requester = HTTPRequester()
self._connected = False

super().__init__(poll_interval)

self.request_readings()

def request_readings(self) -> None:
"""Request the EM27 property data from the web server.
Expand All @@ -87,6 +94,12 @@ def _on_reply_received(self, reply: QNetworkReply) -> None:
data: bytes = reply.readAll().data()
content = data.decode()
readings = get_em27_sensor_data(content)

if not self._connected:
self._connected = True
self.signal_is_opened()
self.start_polling()

self.send_readings_message(readings)


Expand Down
13 changes: 1 addition & 12 deletions finesse/hardware/plugins/sensors/sensors_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,24 @@ class SensorsBase(
calling send_readings_message() with new sensor values (at some point).
"""

def __init__(
self, poll_interval: float = float("nan"), start_polling: bool = True
) -> None:
def __init__(self, poll_interval: float = float("nan")) -> None:
"""Create a new SensorsBase.
Args:
poll_interval: How often to poll the sensor device (seconds). If set to nan,
the device will only be polled once on device open
start_polling: Whether to start polling the device immediately
"""
super().__init__()

self._poll_timer = QTimer()
self._poll_timer.timeout.connect(self.request_readings)
self._poll_interval = poll_interval

if start_polling:
self.start_polling()

def start_polling(self) -> None:
"""Begin polling the device."""
if not isnan(self._poll_interval):
self._poll_timer.start(int(self._poll_interval * 1000))

# Poll device once on open.
# TODO: Run this synchronously so we can check that things work before the
# device.opened message is sent
self.request_readings()

@abstractmethod
def request_readings(self) -> None:
"""Request new sensor readings from the device."""
Expand Down
7 changes: 6 additions & 1 deletion finesse/hardware/plugins/spectrometer/opus_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class OPUSInterface(
"This is rate limited to around one request every two seconds by OPUS."
),
},
async_open=True,
):
"""Interface for communicating with the OPUS program.
Expand All @@ -95,7 +96,7 @@ def __init__(
self._url = f"http://{host}:{port}/opusrs"
"""URL to make requests."""

self._status = SpectrometerStatus.UNDEFINED
self._status: SpectrometerStatus | None = None
"""The last known status of the spectrometer."""
self._status_timer = QTimer()
self._status_timer.timeout.connect(self._request_status)
Expand All @@ -121,6 +122,10 @@ def _on_reply_received(self, reply: QNetworkReply) -> None:

# If the status has changed, notify listeners
if new_status != self._status:
# On first update, we need to signal that the device is now open
if self._status is None:
self.signal_is_opened()

self._status = new_status
self.send_status_message(new_status)

Expand Down
28 changes: 9 additions & 19 deletions tests/hardware/plugins/sensors/test_sensors_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,39 @@ def device(timer_mock: Mock) -> SensorsBase:


class _MockSensorsDevice(SensorsBase, description="Mock sensors device"):
def __init__(self, poll_interval: float = float("nan"), start_polling=True):
def __init__(self, poll_interval: float = float("nan")):
self.request_readings_mock = MagicMock()
super().__init__(poll_interval, start_polling)
super().__init__(poll_interval)

def request_readings(self) -> None:
self.request_readings_mock()


@pytest.mark.parametrize("start_polling", (False, True))
@patch("finesse.hardware.plugins.sensors.sensors_base.QTimer")
def test_init(timer_mock: Mock, start_polling: bool) -> None:
def test_init(timer_mock: Mock) -> None:
"""Test for the constructor."""
with patch.object(_MockSensorsDevice, "start_polling") as start_mock:
device = _MockSensorsDevice(1.0, start_polling)
assert device._poll_interval == 1.0
timer = cast(Mock, device._poll_timer)
timer.timeout.connect.assert_called_once_with(device.request_readings)

if start_polling:
start_mock.assert_called_once_with()
else:
start_mock.assert_not_called()
device = _MockSensorsDevice(1.0)
assert device._poll_interval == 1.0
timer = cast(Mock, device._poll_timer)
timer.timeout.connect.assert_called_once_with(device.request_readings)


@patch("finesse.hardware.plugins.sensors.sensors_base.QTimer")
def test_start_polling_oneshot(timer_mock: Mock) -> None:
"""Test the start_polling() method when polling is only done once."""
device = _MockSensorsDevice(start_polling=False)
device = _MockSensorsDevice()

device.start_polling()
device.request_readings_mock.assert_called_once_with()
timer = cast(Mock, device._poll_timer)
timer.start.assert_not_called()


@patch("finesse.hardware.plugins.sensors.sensors_base.QTimer")
def test_start_polling_repeated(timer_mock: Mock) -> None:
"""Test the start_polling() method when polling is only done repeatedly."""
device = _MockSensorsDevice(1.0, start_polling=False)
device = _MockSensorsDevice(1.0)

device.start_polling()
device.request_readings_mock.assert_called_once_with()
timer = cast(Mock, device._poll_timer)
timer.start.assert_called_once_with(1000)

Expand All @@ -68,7 +59,6 @@ def test_init_no_timer(timer_mock: Mock) -> None:
device = _MockSensorsDevice()
timer = cast(Mock, device._poll_timer)
timer.start.assert_not_called()
device.request_readings_mock.assert_called_once_with()


def test_send_readings_message(device: SensorsBase) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/hardware/plugins/spectrometer/test_opus_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_init(timer_mock: Mock, subscribe_mock: Mock) -> None:
assert opus._url == f"http://{DEFAULT_OPUS_HOST}:{DEFAULT_OPUS_PORT}/opusrs"
status_mock.assert_called_once_with()

assert opus._status == SpectrometerStatus.UNDEFINED
assert opus._status is None
timer.setSingleShot.assert_called_once_with(True)
timer.setInterval.assert_called_once_with(1000)
timer.timeout.connect.assert_called_once_with(opus._request_status)
Expand Down

0 comments on commit 6680116

Please sign in to comment.