Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\mbw973.py : 41%

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.
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"""
11import logging
12from typing import Dict, Type, Union
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
26class MBW973Error(Exception):
27 """
28 General error with the MBW973 dew point mirror device.
29 """
31 pass
34class MBW973ControlRunningException(MBW973Error):
35 """
36 Error indicating there is still a measurement running, and a new one cannot be
37 started.
38 """
40 pass
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 """
49 pass
52@configdataclass
53class MBW973SerialCommunicationConfig(SerialCommunicationConfig):
54 #: Baudrate for MBW973 is 9600 baud
55 baudrate: int = 9600
57 #: MBW973 does not use parity
58 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
60 #: MBW973 does use one stop bit
61 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
63 #: One byte is eight bits long
64 bytesize: Union[
65 int, SerialCommunicationBytesize
66 ] = SerialCommunicationBytesize.EIGHTBITS
68 #: The terminator is only CR
69 terminator: bytes = b"\r"
71 #: use 3 seconds timeout as default
72 timeout: Number = 3
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 """
81 @staticmethod
82 def config_cls():
83 return MBW973SerialCommunicationConfig
86@configdataclass
87class MBW973Config:
88 """
89 Device configuration dataclass for MBW973.
90 """
92 #: Polling period for `is_done` status queries [in seconds].
93 polling_interval: Number = 2
95 def clean_values(self):
96 if self.polling_interval <= 0:
97 raise ValueError("Polling interval needs to be positive.")
100class MBW973(SingleCommDevice):
101 """
102 MBW 973 dew point mirror device class.
103 """
105 def __init__(self, com, dev_config=None):
107 # Call superclass constructor
108 super().__init__(com, dev_config)
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 )
117 # is done with dew point = True, new measurement sample required and
118 # not ready yet = False
119 self.is_done_with_measurements = True
121 # dict telling what measurement options are selected
122 self.measurement_options = {
123 "dewpoint": True,
124 "SF6_Vol": False,
125 }
127 self.last_measurement_values = {}
129 @staticmethod
130 def default_com_cls():
131 return MBW973SerialCommunication
133 @staticmethod
134 def config_cls():
135 return MBW973Config
137 def start(self) -> None:
138 """
139 Start this device. Opens the communication protocol and retrieves the
140 set measurement options from the device.
142 :raises SerialCommunicationIOError: when communication port cannot be opened.
143 """
145 logging.info("Starting device " + str(self))
146 super().start()
148 # check test options
149 self.write("HumidityTest?")
150 self.measurement_options["dewpoint"] = bool(self.read_int())
152 self.write("SF6PurityTest?")
153 self.measurement_options["SF6_Vol"] = bool(self.read_int())
155 def stop(self) -> None:
156 """
157 Stop the device. Closes also the communication protocol.
158 """
160 logging.info("Stopping device " + str(self))
161 super().stop()
163 def write(self, value) -> None:
164 """
165 Send `value` to `self.com`.
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))
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`.
178 :return: Read value of `cast_type` type.
179 """
180 return cast_type(self.com.read_text())
182 def read_float(self) -> float:
183 """
184 Convenience wrapper for `self.read()`, with typing hint for return value.
186 :return: Read `float` value.
187 """
188 return self.read(float)
190 def read_int(self) -> int:
191 """
192 Convenience wrapper for `self.read()`, with typing hint for return value.
194 :return: Read `int` value.
195 """
196 return self.read(int)
198 def is_done(self) -> bool:
199 """
200 Poll status of the dew point mirror and return True, if all
201 measurements are done.
203 :return: True, if all measurements are done; False otherwise.
204 :raises SerialCommunicationIOError: when communication port is not opened
205 """
207 # assume everything is done
208 done = True
210 if self.measurement_options["dewpoint"]:
211 # ask if done with DP
212 self.write("DoneWithDP?")
213 done = done and bool(self.read_int())
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())
220 self.is_done_with_measurements = done
222 if self.is_done_with_measurements:
223 self.status_poller.stop_polling()
224 self.read_measurements()
226 return self.is_done_with_measurements
228 def start_control(self) -> None:
229 """
230 Start dew point control to acquire a new value set.
232 :raises SerialCommunicationIOError: when communication port is not opened
233 """
235 # send control?
236 self.write("control?")
238 if self.read_float():
239 raise MBW973ControlRunningException
241 # send Pump.on? to check, whether gas is still being pumped back
242 self.write("Pump.on?")
244 if self.read_float():
245 raise MBW973PumpRunningException
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()
253 def read_measurements(self) -> Dict[str, float]:
254 """
255 Read out measurement values and return them as a dictionary.
257 :return: Dictionary with values.
258 :raises SerialCommunicationIOError: when communication port is not opened
259 """
261 self.write("Fp?")
262 frostpoint = self.read_float()
264 self.write("Fp1?")
265 frostpoint_ambient = self.read_float()
267 self.write("Px?")
268 pressure = self.read_float()
270 self.write("PPMv?")
271 ppmv = self.read_float()
273 self.write("PPMw?")
274 ppmw = self.read_float()
276 self.write("SF6Vol?")
277 sf6_vol = self.read_float()
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 }
288 logging.info("Read out values")
289 logging.info(values)
291 self.last_measurement_values = values
293 return values
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.
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 """
306 self.write(f"HumidityTest={1 if humidity else 0}")
307 self.write(f"SF6PurityTest={1 if sf6_purity else 0}")
309 self.measurement_options["dewpoint"] = humidity
310 self.measurement_options["SF6_Vol"] = sf6_purity