Skip to content

Commit

Permalink
Merge pull request #428 from ImperialCollegeLondon/typed_device_params
Browse files Browse the repository at this point in the history
Add support for typed device parameters
  • Loading branch information
alexdewar authored Nov 23, 2023
2 parents a0fadd6 + 8818d5c commit 65c79a4
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 240 deletions.
5 changes: 4 additions & 1 deletion finesse/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
DEFAULT_DATA_FILE_PATH = Path.home()
"""The default path to save data files."""

EM27_URL = "http://10.10.0.1/diag_autom.htm"
EM27_HOST = "10.10.0.1"
"""The IP address or hostname of the EM27 device."""

EM27_SENSORS_URL = "http://{host}/diag_autom.htm"
"""The URL of the EM27 monitoring web server."""

EM27_SENSORS_POLL_INTERVAL = 60.0
Expand Down
40 changes: 22 additions & 18 deletions finesse/device_info.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
"""Provides common dataclasses about devices for using in backend and frontend."""
from __future__ import annotations

from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import Any
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, cast


@dataclass(frozen=True)
@dataclass
class DeviceParameter:
"""A parameter that a device needs (e.g. baudrate)."""

name: str
"""Name for the parameter."""

possible_values: Sequence[Any]
"""Possible values the parameter can take."""
description: str
"""A human-readable description of the parameter."""
possible_values: Sequence | type
"""Possible values the parameter can take.
This can either be a Sequence of possible values or a type (e.g. str or float).
"""
default_value: Any = None
"""The default value for this parameter.
A value of None indicates that there is no default value."""

def __post_init__(self) -> None:
"""Check that default value is valid."""
if (
self.default_value is not None
and self.default_value not in self.possible_values
):
raise RuntimeError(
f"Default value of {self.default_value} not in possible values"
)
if self.default_value is None:
return

if isinstance(self.possible_values, Sequence):
if self.default_value not in self.possible_values:
raise RuntimeError(
f"Default value of {self.default_value} not in possible values"
)
elif not isinstance(self.default_value, cast(type, self.possible_values)):
raise RuntimeError("Default value doesn't match type of possible values")

@dataclass(frozen=True)

@dataclass
class DeviceTypeInfo:
"""Description of a device."""

class_name: str
"""The name of the device's class including the module name."""
description: str
"""A human-readable name for the device."""
parameters: Sequence[DeviceParameter]
parameters: Mapping[str, DeviceParameter] = field(default_factory=dict)
"""The device parameters."""


Expand Down
108 changes: 83 additions & 25 deletions finesse/gui/hardware_set/device_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
QComboBox,
QGroupBox,
QHBoxLayout,
QLineEdit,
QPushButton,
QSizePolicy,
QVBoxLayout,
Expand All @@ -23,6 +24,61 @@
from finesse.settings import settings


class ComboParameterWidget(QComboBox):
"""A widget showing the possible parameter values in a combo box."""

def __init__(self, values: Sequence) -> None:
"""Create a new ComboParameterWidget.
Args:
values: The possible values for this parameter
"""
super().__init__()
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)

# Keep the "real" value along with its string representation, so that we can
# pass it back to the backend on device open
for value in values:
self.addItem(str(value), value)

@property
def value(self) -> Any:
"""The currently selected parameter value."""
return self.currentData()

@value.setter
def value(self, new_value: Any) -> Any:
"""Set the parameter value."""
self.setCurrentText(str(new_value))


class TextParameterWidget(QLineEdit):
"""A widget allowing the user to enter parameter values into a text box."""

def __init__(self, param_type: type) -> None:
"""Create a new TextParameterWidget.
Args:
param_type: The type that the parameter must be
"""
super().__init__()
self._param_type = param_type

@property
def value(self) -> Any:
"""The currently selected parameter value.
Raises:
Exception: If relevant type cannot be constructed from string
"""
return self._param_type(self.text())

@value.setter
def value(self, new_value: Any) -> Any:
"""Set the parameter value."""
self.setText(str(new_value))


class DeviceParametersWidget(QWidget):
"""A widget containing controls for setting a device's parameters."""

Expand All @@ -41,30 +97,26 @@ def __init__(self, device_type: DeviceTypeInfo) -> None:
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)

# Make a combo box for each parameter
self._combos: dict[str, QComboBox] = {}
for param in device_type.parameters:
combo = QComboBox()
combo.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)

# Keep the "real" value along with its string representation, so that we can
# pass it back to the backend on device open
for value in param.possible_values:
combo.addItem(str(value), value)
# Make a widget for each parameter
self._param_widgets: dict[str, ComboParameterWidget | TextParameterWidget] = {}
for name, param in device_type.parameters.items():
cls = (
ComboParameterWidget
if isinstance(param.possible_values, Sequence)
else TextParameterWidget
)
widget = cls(param.possible_values)
widget.setToolTip(param.description)

if param.default_value is not None:
combo.setCurrentIndex(param.possible_values.index(param.default_value))
widget.value = param.default_value

layout.addWidget(combo)
self._combos[param.name] = combo
layout.addWidget(widget)
self._param_widgets[name] = widget

# If there are saved parameter values, load them now
self.load_saved_parameter_values()

def set_parameter_value(self, param: str, value: Any) -> None:
"""Set the relevant combo box's parameter value."""
self._combos[param].setCurrentText(str(value))

def load_saved_parameter_values(self) -> None:
"""Set the combo boxes' parameter values according to their saved values."""
params = cast(
Expand All @@ -76,14 +128,14 @@ def load_saved_parameter_values(self) -> None:

for param, value in params.items():
try:
self.set_parameter_value(param, value)
self._param_widgets[param].value = value
except Exception as error:
logging.warn(f"Error while setting param {param}: {error!s}")

@property
def current_parameter_values(self) -> dict[str, Any]:
"""Get all parameters and their current values."""
return {param: combo.currentData() for param, combo in self._combos.items()}
return {param: widget.value for param, widget in self._param_widgets.items()}


class DeviceTypeControl(QGroupBox):
Expand Down Expand Up @@ -175,7 +227,7 @@ def _update_open_btn_enabled_state(self) -> None:
The "open" button should be disabled if there are no possible values for any
of the params.
"""
all_params = self.current_device_type_widget.device_type.parameters
all_params = self.current_device_type_widget.device_type.parameters.values()
self._open_close_btn.setEnabled(all(p.possible_values for p in all_params))

def _on_device_selected(self) -> None:
Expand Down Expand Up @@ -234,11 +286,17 @@ def _set_device_closed(self, **kwargs) -> None:
def _open_device(self) -> None:
"""Open the currently selected device."""
widget = self.current_device_type_widget
open_device(
widget.device_type.class_name,
self._device_instance,
widget.current_parameter_values,
)

try:
params = widget.current_parameter_values
except ValueError:
show_error_message(
self,
"Invalid value given for at least one parameter",
"Invalid parameter value",
)
else:
open_device(widget.device_type.class_name, self._device_instance, params)

def _on_device_opened(
self, instance: DeviceInstanceRef, class_name: str, params: Mapping[str, Any]
Expand Down
10 changes: 5 additions & 5 deletions finesse/gui/hardware_set/finesse_dp9800.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
name: FINESSE (with DP9800)
devices:
stepper_motor:
class_name: finesse.hardware.plugins.stepper_motor.st10_controller.ST10Controller
class_name: stepper_motor.st10_controller.ST10Controller
params:
port: "0403:6011 FT1NMSVR (4)"
baudrate: 9600
temperature_controller.hot_bb:
class_name: finesse.hardware.plugins.temperature.tc4820.TC4820
class_name: temperature.tc4820.TC4820
params:
port: "0403:6011 FT1NMSVR (2)"
baudrate: 115200
temperature_controller.cold_bb:
class_name: finesse.hardware.plugins.temperature.tc4820.TC4820
class_name: temperature.tc4820.TC4820
params:
port: "0403:6011 FT1NMSVR (3)"
baudrate: 115200
temperature_monitor:
class_name: finesse.hardware.plugins.temperature.dp9800.DP9800
class_name: temperature.dp9800.DP9800
params:
port: "0403:6001"
baudrate: 38400
em27_sensors:
class_name: finesse.hardware.plugins.em27.em27_sensors.EM27Sensors
class_name: em27.em27_sensors.EM27Sensors
10 changes: 5 additions & 5 deletions finesse/gui/hardware_set/finesse_dummy.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: "FINESSE (dummy devices)"
devices:
stepper_motor:
class_name: finesse.hardware.plugins.stepper_motor.dummy.DummyStepperMotor
class_name: stepper_motor.dummy.DummyStepperMotor
temperature_controller.hot_bb:
class_name: finesse.hardware.plugins.temperature.dummy_temperature_controller.DummyTemperatureController
class_name: temperature.dummy_temperature_controller.DummyTemperatureController
temperature_controller.cold_bb:
class_name: finesse.hardware.plugins.temperature.dummy_temperature_controller.DummyTemperatureController
class_name: temperature.dummy_temperature_controller.DummyTemperatureController
temperature_monitor:
class_name: finesse.hardware.plugins.temperature.dummy_temperature_monitor.DummyTemperatureMonitor
class_name: temperature.dummy_temperature_monitor.DummyTemperatureMonitor
em27_sensors:
class_name: finesse.hardware.plugins.em27.dummy_em27_sensors.DummyEM27Sensors
class_name: em27.dummy_em27_sensors.DummyEM27Sensors
10 changes: 5 additions & 5 deletions finesse/gui/hardware_set/finesse_seneca.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
name: FINESSE (with Seneca K107)
devices:
stepper_motor:
class_name: finesse.hardware.plugins.stepper_motor.st10_controller.ST10Controller
class_name: stepper_motor.st10_controller.ST10Controller
params:
port: "0403:6011 FT1NMSVR (4)"
baudrate: 9600
temperature_controller.hot_bb:
class_name: finesse.hardware.plugins.temperature.tc4820.TC4820
class_name: temperature.tc4820.TC4820
params:
port: "0403:6011 FT1NMSVR (2)"
baudrate: 115200
temperature_controller.cold_bb:
class_name: finesse.hardware.plugins.temperature.tc4820.TC4820
class_name: temperature.tc4820.TC4820
params:
port: "0403:6011 FT1NMSVR (3)"
baudrate: 115200
temperature_monitor:
class_name: finesse.hardware.plugins.temperature.senecak107.SenecaK107
class_name: temperature.senecak107.SenecaK107
params:
port: "0403:6001 AB0LMVI5"
baudrate: 57600
em27_sensors:
class_name: finesse.hardware.plugins.em27.em27_sensors.EM27Sensors
class_name: em27.em27_sensors.EM27Sensors
Loading

0 comments on commit 65c79a4

Please sign in to comment.