Hide keyboard shortcuts

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

6 

7import logging 

8from abc import ABC, abstractmethod 

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

10 

11from ..comm import CommunicationProtocol 

12from ..configuration import ConfigurationMixin, configdataclass 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class DeviceExistingException(Exception): 

18 """ 

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

20 """ 

21 

22 pass 

23 

24 

25class 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 

38class EmptyConfig: 

39 """ 

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

41 """ 

42 

43 pass 

44 

45 

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. 

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 

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

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 

234class 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()