Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for typed device parameters #428

Merged
merged 10 commits into from
Nov 23, 2023
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(

Check warning on line 32 in finesse/device_info.py

View check run for this annotation

Codecov / codecov/patch

finesse/device_info.py#L32

Added line #L32 was not covered by tests
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")

Check warning on line 36 in finesse/device_info.py

View check run for this annotation

Codecov / codecov/patch

finesse/device_info.py#L36

Added line #L36 was not covered by tests

@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