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

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

## 107 statements

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"""

4 Module with base classes for devices.

5"""

6

7 import logging

8 from abc import ABC, abstractmethod

9 from typing import Dict, Type, List, Tuple, Union

10

11 from ..comm import CommunicationProtocol

12 from ..configuration import ConfigurationMixin, configdataclass

13

14 logger = logging.getLogger(__name__)

15

16

17 class DeviceExistingException(Exception):

18 """

19 Exception to indicate that a device with that name already exists.

20 """

21

22 pass

23

24

25 class DeviceFailuresException(Exception):

26 """

27 Exception to indicate that one or several devices failed.

28 """

29

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 """

35

36

37@configdataclass

38 class EmptyConfig:

39 """

40 Empty configuration dataclass that is the default configuration for a Device.

41 """

42

43 pass

44

45

46 class Device(ConfigurationMixin, ABC):

47 """

48 Base class for devices. Implement this class for a concrete device,

49 such as measurement equipment or voltage sources.

50

51 Specifies the methods to implement for a device.

52 """

53

54 def __init__(self, dev_config=None):

55 """

56 Constructor for Device.

57 """

58

59 super().__init__(dev_config)

60

61 @abstractmethod

62 def start(self) -> None:

63 """

64 Start or restart this Device. To be implemented in the subclass.

65 """

66

67 @abstractmethod

68 def stop(self) -> None:

69 """

70 Stop this Device. To be implemented in the subclass.

71 """

72

73 def __enter__(self):

74 self.start()

75 return self

76

77 def __exit__(self, exc_type, exc_val, exc_tb):

78 self.stop()

79

80 @staticmethod

81 def config_cls():

82 return EmptyConfig

83

84

85 class 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 """

90

91 def __init__(self, devices: Dict[str, Device]):

92 """

93 Constructor for the DeviceSequenceMixin.

94

95 :param devices: is a dictionary of devices to be added to this sequence.

96 """

97

98 super().__init__()

99

100 self._devices: Dict[str, Device] = {}

101 for (name, device) in devices.items():

102 self.add_device(name, device)

103

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.

107

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.

112

113 Empty if `start()` was called last; cf. `devices_failed_start`."""

114

115 def __getattribute__(self, item: str) -> Union[Device, object]:

116 """

117 Gets Device from the sequence or object's attribute.

118

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)

125

126 def __eq__(self, other):

127 return (

128 isinstance(other, DeviceSequenceMixin) and self._devices == other._devices

129 )

130

131 def get_devices(self) -> List[Tuple[str, Device]]:

132 """

133 Get list of name, device pairs according to current sequence.

134

135 :return: A list of tuples with name and device each.

136 """

137 return list(self._devices.items())

138

139 def get_device(self, name: str) -> Device:

140 """

141 Get a device by name.

142

143 :param name: is the name of the device.

144 :return: the device object from this sequence.

145 """

146

147 return self._devices.get(name) # type: ignore

148

149 def add_device(self, name: str, device: Device) -> None:

150 """

151 Add a new device to the device sequence.

152

153 :param name: is the name of the device.

154 :param device: is the instantiated Device object.

155 :raise DeviceExistingException:

156 """

157

158 if name in self._devices:

159 raise DeviceExistingException

160

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 )

167

168 self._devices[name] = device

169

170 def remove_device(self, name: str) -> Device:

171 """

172 Remove a device from this sequence and return the device object.

173

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 """

178

179 if name not in self._devices:

180 raise ValueError(f'No device named "{name}" in this sequence.')

181

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)

186

187 return self._devices.pop(name)

188

189 def start(self) -> None:

190 """

191 Start all devices in this sequence in their added order.

192

193 :raises DeviceFailuresException: if one or several devices failed to start

194 """

195

196 # reset the failure dicts

197 failures = dict()

198 self.devices_failed_start = dict()

199 self.devices_failed_stop = dict()

200

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)

210

211 def stop(self) -> None:

212 """

213 Stop all devices in this sequence in their reverse order.

214

215 :raises DeviceFailuresException: if one or several devices failed to stop

216 """

217

218 # reset the failure dicts

219 failures: Dict[str, Exception] = dict()

220 self.devices_failed_start = dict()

221 self.devices_failed_stop = dict()

222

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)

232

233

234 class SingleCommDevice(Device, ABC):

235 """

236 Base class for devices with a single communication protocol.

237 """

238

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.

245

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.

251

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 """

257

258 super().__init__(dev_config)

259

260 if isinstance(com, CommunicationProtocol):

261 self._com = com

262 else:

263 self._com = self.default_com_cls()(com)

264

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.

270

271 :return: the type of the standard communication protocol for this device

272 """

273

274 @property

275 def com(self):

276 """

277 Get the communication protocol of this device.

278

279 :return: an instance of CommunicationProtocol subtype

280 """

281 return self._com

282

283 def start(self) -> None:

284 """

285 Open the associated communication protocol.

286 """

287

288 self.com.open()

289

290 def stop(self) -> None:

291 """

292 Close the associated communication protocol.

293 """

294

295 self.com.close()