Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\base.py : 48%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Module with base classes for devices.
5"""
7import logging
8from abc import ABC, abstractmethod
9from typing import Dict, Type, List, Tuple, Union
11from ..comm import CommunicationProtocol
12from ..configuration import ConfigurationMixin, configdataclass
14logger = logging.getLogger(__name__)
17class DeviceExistingException(Exception):
18 """
19 Exception to indicate that a device with that name already exists.
20 """
22 pass
25class DeviceFailuresException(Exception):
26 """
27 Exception to indicate that one or several devices failed.
28 """
30 def __init__(self, failures: Dict[str, Exception], *args):
31 super().__init__(failures)
32 self.failures: Dict[str, Exception] = failures
33 """A dictionary of named devices failures (exceptions).
34 """
37@configdataclass
38class EmptyConfig:
39 """
40 Empty configuration dataclass that is the default configuration for a Device.
41 """
43 pass
46class Device(ConfigurationMixin, ABC):
47 """
48 Base class for devices. Implement this class for a concrete device,
49 such as measurement equipment or voltage sources.
51 Specifies the methods to implement for a device.
52 """
54 def __init__(self, dev_config=None):
55 """
56 Constructor for Device.
57 """
59 super().__init__(dev_config)
61 @abstractmethod
62 def start(self) -> None:
63 """
64 Start or restart this Device. To be implemented in the subclass.
65 """
67 @abstractmethod
68 def stop(self) -> None:
69 """
70 Stop this Device. To be implemented in the subclass.
71 """
73 def __enter__(self):
74 self.start()
75 return self
77 def __exit__(self, exc_type, exc_val, exc_tb):
78 self.stop()
80 @staticmethod
81 def config_cls():
82 return EmptyConfig
85class DeviceSequenceMixin(ABC):
86 """
87 Mixin that can be used on a device or other classes to provide facilities for
88 handling multiple devices in a sequence.
89 """
91 def __init__(self, devices: Dict[str, Device]):
92 """
93 Constructor for the DeviceSequenceMixin.
95 :param devices: is a dictionary of devices to be added to this sequence.
96 """
98 super().__init__()
100 self._devices: Dict[str, Device] = {}
101 for (name, device) in devices.items():
102 self.add_device(name, device)
104 self.devices_failed_start: Dict[str, Device] = dict()
105 """Dictionary of named device instances from the sequence for which the most
106 recent `start()` attempt failed.
108 Empty if `stop()` was called last; cf. `devices_failed_stop`."""
109 self.devices_failed_stop: Dict[str, Device] = dict()
110 """Dictionary of named device instances from the sequence for which the most
111 recent `stop()` attempt failed.
113 Empty if `start()` was called last; cf. `devices_failed_start`."""
115 def __getattribute__(self, item: str) -> Union[Device, object]:
116 """
117 Gets Device from the sequence or object's attribute.
119 :param item: Item name to get
120 :return: Device or object's attribute.
121 """
122 if item != "_devices" and item in self._devices:
123 return self.get_device(item)
124 return super().__getattribute__(item)
126 def __eq__(self, other):
127 return (
128 isinstance(other, DeviceSequenceMixin) and self._devices == other._devices
129 )
131 def get_devices(self) -> List[Tuple[str, Device]]:
132 """
133 Get list of name, device pairs according to current sequence.
135 :return: A list of tuples with name and device each.
136 """
137 return list(self._devices.items())
139 def get_device(self, name: str) -> Device:
140 """
141 Get a device by name.
143 :param name: is the name of the device.
144 :return: the device object from this sequence.
145 """
147 return self._devices.get(name) # type: ignore
149 def add_device(self, name: str, device: Device) -> None:
150 """
151 Add a new device to the device sequence.
153 :param name: is the name of the device.
154 :param device: is the instantiated Device object.
155 :raise DeviceExistingException:
156 """
158 if name in self._devices:
159 raise DeviceExistingException
161 # disallow over-shadowing via ".DEVICE_NAME" lookup
162 if hasattr(self, name):
163 raise ValueError(
164 f"This sequence already has an attribute called"
165 f" {name}. Use different name for this device."
166 )
168 self._devices[name] = device
170 def remove_device(self, name: str) -> Device:
171 """
172 Remove a device from this sequence and return the device object.
174 :param name: is the name of the device.
175 :return: device object or `None` if such device was not in the sequence.
176 :raises ValueError: when device with given name was not found
177 """
179 if name not in self._devices:
180 raise ValueError(f'No device named "{name}" in this sequence.')
182 if name in self.devices_failed_start:
183 self.devices_failed_start.pop(name)
184 elif name in self.devices_failed_stop:
185 self.devices_failed_stop.pop(name)
187 return self._devices.pop(name)
189 def start(self) -> None:
190 """
191 Start all devices in this sequence in their added order.
193 :raises DeviceFailuresException: if one or several devices failed to start
194 """
196 # reset the failure dicts
197 failures = dict()
198 self.devices_failed_start = dict()
199 self.devices_failed_stop = dict()
201 for name, device in self._devices.items():
202 try:
203 device.start()
204 except Exception as e:
205 logger.error(f"Could not start {name}: {e}")
206 failures[name] = e
207 self.devices_failed_start[name] = device
208 if failures:
209 raise DeviceFailuresException(failures)
211 def stop(self) -> None:
212 """
213 Stop all devices in this sequence in their reverse order.
215 :raises DeviceFailuresException: if one or several devices failed to stop
216 """
218 # reset the failure dicts
219 failures: Dict[str, Exception] = dict()
220 self.devices_failed_start = dict()
221 self.devices_failed_stop = dict()
223 for name, device in self._devices.items():
224 try:
225 device.stop()
226 except Exception as e:
227 logger.error(f"Could not stop {name}: {e}")
228 failures[name] = e
229 self.devices_failed_stop[name] = device
230 if failures:
231 raise DeviceFailuresException(failures)
234class SingleCommDevice(Device, ABC):
235 """
236 Base class for devices with a single communication protocol.
237 """
239 # Omitting typing hint `com: CommunicationProtocol` on purpose
240 # to enable PyCharm autocompletion for subtypes.
241 def __init__(self, com, dev_config=None) -> None:
242 """
243 Constructor for Device. Links the communication protocol and provides a
244 configuration for the device.
246 :param com: Communication protocol to be used with
247 this device. Can be of type: - CommunicationProtocol instance, - dictionary
248 with keys and values to be used as configuration together with the
249 default communication protocol, or - @configdataclass to be used together
250 with the default communication protocol.
252 :param dev_config: configuration of the device. Can be:
253 - None: empty configuration is used, or the specified config_cls()
254 - @configdataclass decorated class
255 - Dictionary, which is then used to instantiate the specified config_cls()
256 """
258 super().__init__(dev_config)
260 if isinstance(com, CommunicationProtocol):
261 self._com = com
262 else:
263 self._com = self.default_com_cls()(com)
265 @staticmethod
266 @abstractmethod
267 def default_com_cls() -> Type[CommunicationProtocol]:
268 """
269 Get the class for the default communication protocol used with this device.
271 :return: the type of the standard communication protocol for this device
272 """
274 @property
275 def com(self):
276 """
277 Get the communication protocol of this device.
279 :return: an instance of CommunicationProtocol subtype
280 """
281 return self._com
283 def start(self) -> None:
284 """
285 Open the associated communication protocol.
286 """
288 self.com.open()
290 def stop(self) -> None:
291 """
292 Close the associated communication protocol.
293 """
295 self.com.close()