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

4Device class for controlling a MBW 973 SF6 Analyzer over a serial connection. 

5 

6The MBW 973 is a gas analyzer designed for gas insulated switchgear and measures 

7humidity, SF6 purity and SO2 contamination in one go. 

8Manufacturer homepage: https://www.mbw.ch/products/sf6-gas-analysis/973-sf6-analyzer/ 

9""" 

10 

11import logging 

12from typing import Dict, Type, Union 

13 

14from .base import SingleCommDevice 

15from .utils import Poller 

16from ..comm import SerialCommunication, SerialCommunicationConfig 

17from ..comm.serial import ( 

18 SerialCommunicationParity, 

19 SerialCommunicationStopbits, 

20 SerialCommunicationBytesize, 

21) 

22from ..configuration import configdataclass 

23from ..utils.typing import Number 

24 

25 

26class MBW973Error(Exception): 

27 """ 

28 General error with the MBW973 dew point mirror device. 

29 """ 

30 

31 pass 

32 

33 

34class MBW973ControlRunningException(MBW973Error): 

35 """ 

36 Error indicating there is still a measurement running, and a new one cannot be 

37 started. 

38 """ 

39 

40 pass 

41 

42 

43class MBW973PumpRunningException(MBW973Error): 

44 """ 

45 Error indicating the pump of the dew point mirror is still recovering gas, 

46 unable to start a new measurement. 

47 """ 

48 

49 pass 

50 

51 

52@configdataclass 

53class MBW973SerialCommunicationConfig(SerialCommunicationConfig): 

54 #: Baudrate for MBW973 is 9600 baud 

55 baudrate: int = 9600 

56 

57 #: MBW973 does not use parity 

58 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE 

59 

60 #: MBW973 does use one stop bit 

61 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE 

62 

63 #: One byte is eight bits long 

64 bytesize: Union[ 

65 int, SerialCommunicationBytesize 

66 ] = SerialCommunicationBytesize.EIGHTBITS 

67 

68 #: The terminator is only CR 

69 terminator: bytes = b"\r" 

70 

71 #: use 3 seconds timeout as default 

72 timeout: Number = 3 

73 

74 

75class MBW973SerialCommunication(SerialCommunication): 

76 """ 

77 Specific communication protocol implementation for the MBW973 dew point mirror. 

78 Already predefines device-specific protocol parameters in config. 

79 """ 

80 

81 @staticmethod 

82 def config_cls(): 

83 return MBW973SerialCommunicationConfig 

84 

85 

86@configdataclass 

87class MBW973Config: 

88 """ 

89 Device configuration dataclass for MBW973. 

90 """ 

91 

92 #: Polling period for `is_done` status queries [in seconds]. 

93 polling_interval: Number = 2 

94 

95 def clean_values(self): 

96 if self.polling_interval <= 0: 

97 raise ValueError("Polling interval needs to be positive.") 

98 

99 

100class MBW973(SingleCommDevice): 

101 """ 

102 MBW 973 dew point mirror device class. 

103 """ 

104 

105 def __init__(self, com, dev_config=None): 

106 

107 # Call superclass constructor 

108 super().__init__(com, dev_config) 

109 

110 # polling status 

111 self.status_poller = Poller( 

112 self.is_done, 

113 polling_delay_sec=self.config.polling_interval, 

114 polling_interval_sec=self.config.polling_interval, 

115 ) 

116 

117 # is done with dew point = True, new measurement sample required and 

118 # not ready yet = False 

119 self.is_done_with_measurements = True 

120 

121 # dict telling what measurement options are selected 

122 self.measurement_options = { 

123 "dewpoint": True, 

124 "SF6_Vol": False, 

125 } 

126 

127 self.last_measurement_values = {} 

128 

129 @staticmethod 

130 def default_com_cls(): 

131 return MBW973SerialCommunication 

132 

133 @staticmethod 

134 def config_cls(): 

135 return MBW973Config 

136 

137 def start(self) -> None: 

138 """ 

139 Start this device. Opens the communication protocol and retrieves the 

140 set measurement options from the device. 

141 

142 :raises SerialCommunicationIOError: when communication port cannot be opened. 

143 """ 

144 

145 logging.info("Starting device " + str(self)) 

146 super().start() 

147 

148 # check test options 

149 self.write("HumidityTest?") 

150 self.measurement_options["dewpoint"] = bool(self.read_int()) 

151 

152 self.write("SF6PurityTest?") 

153 self.measurement_options["SF6_Vol"] = bool(self.read_int()) 

154 

155 def stop(self) -> None: 

156 """ 

157 Stop the device. Closes also the communication protocol. 

158 """ 

159 

160 logging.info("Stopping device " + str(self)) 

161 super().stop() 

162 

163 def write(self, value) -> None: 

164 """ 

165 Send `value` to `self.com`. 

166 

167 :param value: Value to send, converted to `str`. 

168 :raises SerialCommunicationIOError: when communication port is not opened 

169 """ 

170 self.com.write_text(str(value)) 

171 

172 def read(self, cast_type: Type = str): 

173 """ 

174 Read value from `self.com` and cast to `cast_type`. 

175 Raises `ValueError` if read text (`str`) is not convertible to `cast_type`, 

176 e.g. to `float` or to `int`. 

177 

178 :return: Read value of `cast_type` type. 

179 """ 

180 return cast_type(self.com.read_text()) 

181 

182 def read_float(self) -> float: 

183 """ 

184 Convenience wrapper for `self.read()`, with typing hint for return value. 

185 

186 :return: Read `float` value. 

187 """ 

188 return self.read(float) 

189 

190 def read_int(self) -> int: 

191 """ 

192 Convenience wrapper for `self.read()`, with typing hint for return value. 

193 

194 :return: Read `int` value. 

195 """ 

196 return self.read(int) 

197 

198 def is_done(self) -> bool: 

199 """ 

200 Poll status of the dew point mirror and return True, if all 

201 measurements are done. 

202 

203 :return: True, if all measurements are done; False otherwise. 

204 :raises SerialCommunicationIOError: when communication port is not opened 

205 """ 

206 

207 # assume everything is done 

208 done = True 

209 

210 if self.measurement_options["dewpoint"]: 

211 # ask if done with DP 

212 self.write("DoneWithDP?") 

213 done = done and bool(self.read_int()) 

214 

215 if self.measurement_options["SF6_Vol"]: 

216 # ask if done with SF6 volume measurement 

217 self.write("SF6VolHold?") 

218 done = done and bool(self.read_int()) 

219 

220 self.is_done_with_measurements = done 

221 

222 if self.is_done_with_measurements: 

223 self.status_poller.stop_polling() 

224 self.read_measurements() 

225 

226 return self.is_done_with_measurements 

227 

228 def start_control(self) -> None: 

229 """ 

230 Start dew point control to acquire a new value set. 

231 

232 :raises SerialCommunicationIOError: when communication port is not opened 

233 """ 

234 

235 # send control? 

236 self.write("control?") 

237 

238 if self.read_float(): 

239 raise MBW973ControlRunningException 

240 

241 # send Pump.on? to check, whether gas is still being pumped back 

242 self.write("Pump.on?") 

243 

244 if self.read_float(): 

245 raise MBW973PumpRunningException 

246 

247 # start control of device 

248 self.write("control=1") 

249 logging.info("Starting dew point control") 

250 self.is_done_with_measurements = False 

251 self.status_poller.start_polling() 

252 

253 def read_measurements(self) -> Dict[str, float]: 

254 """ 

255 Read out measurement values and return them as a dictionary. 

256 

257 :return: Dictionary with values. 

258 :raises SerialCommunicationIOError: when communication port is not opened 

259 """ 

260 

261 self.write("Fp?") 

262 frostpoint = self.read_float() 

263 

264 self.write("Fp1?") 

265 frostpoint_ambient = self.read_float() 

266 

267 self.write("Px?") 

268 pressure = self.read_float() 

269 

270 self.write("PPMv?") 

271 ppmv = self.read_float() 

272 

273 self.write("PPMw?") 

274 ppmw = self.read_float() 

275 

276 self.write("SF6Vol?") 

277 sf6_vol = self.read_float() 

278 

279 values = { 

280 "frostpoint": frostpoint, 

281 "frostpoint_ambient": frostpoint_ambient, 

282 "pressure": pressure, 

283 "ppmv": ppmv, 

284 "ppmw": ppmw, 

285 "sf6_vol": sf6_vol, 

286 } 

287 

288 logging.info("Read out values") 

289 logging.info(values) 

290 

291 self.last_measurement_values = values 

292 

293 return values 

294 

295 def set_measuring_options( 

296 self, humidity: bool = True, sf6_purity: bool = False 

297 ) -> None: 

298 """ 

299 Send measuring options to the dew point mirror. 

300 

301 :param humidity: Perform humidity test or not? 

302 :param sf6_purity: Perform SF6 purity test or not? 

303 :raises SerialCommunicationIOError: when communication port is not opened 

304 """ 

305 

306 self.write(f"HumidityTest={1 if humidity else 0}") 

307 self.write(f"SF6PurityTest={1 if sf6_purity else 0}") 

308 

309 self.measurement_options["dewpoint"] = humidity 

310 self.measurement_options["SF6_Vol"] = sf6_purity