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) 2020 ETH Zurich, SIS ID and HVL D-ITET 

2# 

3""" 

4Device class for a SST Luminox Oxygen sensor. This device can measure the oxygen 

5concentration between 0 % and 25 %. 

6 

7Furthermore, it measures the barometric pressure and internal temperature. 

8The device supports two operating modes: in streaming mode the device measures all 

9parameters every second, in polling mode the device measures only after a query. 

10 

11Technical specification and documentation for the device can be found a the 

12manufacturer's page: 

13https://www.sstsensing.com/product/luminox-optical-oxygen-sensors-2/ 

14""" 

15 

16import logging 

17import re 

18from enum import Enum 

19from time import sleep 

20from typing import Union, Tuple, List, cast, Optional, Dict 

21 

22from .base import SingleCommDevice 

23from ..comm import SerialCommunication, SerialCommunicationConfig 

24from ..comm.serial import ( 

25 SerialCommunicationParity, 

26 SerialCommunicationStopbits, 

27 SerialCommunicationBytesize, 

28) 

29from ..configuration import configdataclass 

30from ..utils.enum import ValueEnum 

31from ..utils.typing import Number 

32 

33 

34class LuminoxOutputModeError(Exception): 

35 """ 

36 Wrong output mode for requested data 

37 """ 

38 

39 pass 

40 

41 

42class LuminoxOutputMode(Enum): 

43 """ 

44 output mode. 

45 """ 

46 

47 streaming = 0 

48 polling = 1 

49 

50 

51class LuminoxMeasurementTypeError(Exception): 

52 """ 

53 Wrong measurement type for requested data 

54 """ 

55 

56 pass 

57 

58 

59LuminoxMeasurementTypeValue = Union[float, int, str] 

60"""A typing hint for all possible LuminoxMeasurementType values as read in either 

61streaming mode or in a polling mode with `LuminoxMeasurementType.all_measurements`. 

62 

63Beware: has to be manually kept in sync with `LuminoxMeasurementType` instances 

64`cast_type` attribute values. 

65""" 

66 

67 

68LuminoxMeasurementTypeDict = Dict[ 

69 Union[str, "LuminoxMeasurementType"], LuminoxMeasurementTypeValue 

70] 

71"""A typing hint for a dictionary holding LuminoxMeasurementType values. Keys are 

72allowed as strings because `LuminoxMeasurementType` is of a `StrEnumBase` type. 

73""" 

74 

75 

76class LuminoxMeasurementType(ValueEnum): 

77 """ 

78 Measurement types for `LuminoxOutputMode.polling`. 

79 

80 The `all_measurements` type will read values for the actual measurement types 

81 as given in `LuminoxOutputMode.all_measurements_types()`; it parses multiple 

82 single values using regexp's for other measurement types, therefore, no regexp is 

83 defined for this measurement type. 

84 """ 

85 

86 _init_ = "value cast_type value_re" 

87 partial_pressure_o2 = "O", float, r"[0-9]{4}.[0-9]" 

88 percent_o2 = "%", float, r"[0-9]{3}.[0-9]{2}" 

89 temperature_sensor = "T", float, r"[+-][0-9]{2}.[0-9]" 

90 barometric_pressure = "P", int, r"[0-9]{4}" 

91 sensor_status = "e", int, r"[0-9]{4}" 

92 date_of_manufacture = "# 0", str, r"[0-9]{5} [0-9]{5}" 

93 serial_number = "# 1", str, r"[0-9]{5} [0-9]{5}" 

94 software_revision = "# 2", str, r"[0-9]{5}" 

95 all_measurements = "A", str, None 

96 

97 @classmethod 

98 def all_measurements_types(cls) -> Tuple["LuminoxMeasurementType", ...]: 

99 """ 

100 A tuple of `LuminoxMeasurementType` enum instances which are actual 

101 measurements, i.e. not date of manufacture or software revision. 

102 """ 

103 return cast( 

104 Tuple["LuminoxMeasurementType", ...], 

105 ( 

106 cls.partial_pressure_o2, 

107 cls.temperature_sensor, 

108 cls.barometric_pressure, 

109 cls.percent_o2, 

110 cls.sensor_status, 

111 ), 

112 ) 

113 

114 @property 

115 def command(self) -> str: 

116 return self.value.split(" ")[0] 

117 

118 def parse_read_measurement_value( 

119 self, read_txt: str 

120 ) -> Union[LuminoxMeasurementTypeDict, LuminoxMeasurementTypeValue]: 

121 if self is LuminoxMeasurementType.all_measurements: 

122 return { 

123 measurement: measurement._parse_single_measurement_value(read_txt) 

124 for measurement in LuminoxMeasurementType.all_measurements_types() 

125 } 

126 return self._parse_single_measurement_value(read_txt) 

127 

128 def _parse_single_measurement_value( 

129 self, read_txt: str 

130 ) -> LuminoxMeasurementTypeValue: 

131 

132 parsed_data: List[str] = re.findall(f"{self.command} {self.value_re}", read_txt) 

133 if len(parsed_data) != 1: 

134 self._parse_error(parsed_data) 

135 

136 parsed_measurement: str = parsed_data[0] 

137 try: 

138 parsed_value = self.cast_type( 

139 # don't check for empty match - we know already that there is one 

140 re.search(self.value_re, parsed_measurement).group() # type: ignore 

141 ) 

142 except ValueError: 

143 self._parse_error(parsed_data) 

144 

145 return parsed_value 

146 

147 def _parse_error(self, parsed_data: List[str]) -> None: 

148 err_msg = ( 

149 f"Expected measurement value for {self.name.replace('_', ' ')} of type " 

150 f'{self.cast_type}; instead tyring to parse: "{parsed_data}"' 

151 ) 

152 logging.error(err_msg) 

153 raise LuminoxMeasurementTypeError(err_msg) 

154 

155 

156@configdataclass 

157class LuminoxSerialCommunicationConfig(SerialCommunicationConfig): 

158 #: Baudrate for SST Luminox is 9600 baud 

159 baudrate: int = 9600 

160 

161 #: SST Luminox does not use parity 

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

163 

164 #: SST Luminox does use one stop bit 

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

166 

167 #: One byte is eight bits long 

168 bytesize: Union[ 

169 int, SerialCommunicationBytesize 

170 ] = SerialCommunicationBytesize.EIGHTBITS 

171 

172 #: The terminator is CR LF 

173 terminator: bytes = b"\r\n" 

174 

175 #: use 3 seconds timeout as default 

176 timeout: Number = 3 

177 

178 

179class LuminoxSerialCommunication(SerialCommunication): 

180 """ 

181 Specific communication protocol implementation for the SST Luminox oxygen sensor. 

182 Already predefines device-specific protocol parameters in config. 

183 """ 

184 

185 @staticmethod 

186 def config_cls(): 

187 return LuminoxSerialCommunicationConfig 

188 

189 

190@configdataclass 

191class LuminoxConfig: 

192 """ 

193 Configuration for the SST Luminox oxygen sensor. 

194 """ 

195 

196 # wait between set and validation of output mode 

197 wait_sec_post_activate: Number = 0.5 

198 wait_sec_trials_activate: Number = 0.1 

199 nr_trials_activate: int = 5 

200 

201 def clean_values(self): 

202 if self.wait_sec_post_activate <= 0: 

203 raise ValueError( 

204 "Wait time (sec) post output mode activation must be a positive number." 

205 ) 

206 if self.wait_sec_trials_activate <= 0: 

207 raise ValueError( 

208 "Re-try wait time (sec) for mode activation must be a positive number." 

209 ) 

210 if self.nr_trials_activate <= 0: 

211 raise ValueError( 

212 "Trials for mode activation must be a positive integer >=1)." 

213 ) 

214 

215 

216class Luminox(SingleCommDevice): 

217 """ 

218 Luminox oxygen sensor device class. 

219 """ 

220 

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

222 

223 # Call superclass constructor 

224 super().__init__(com, dev_config) 

225 self.output: Optional[LuminoxOutputMode] = None 

226 

227 @staticmethod 

228 def config_cls(): 

229 return LuminoxConfig 

230 

231 @staticmethod 

232 def default_com_cls(): 

233 return LuminoxSerialCommunication 

234 

235 def start(self) -> None: 

236 """ 

237 Start this device. Opens the communication protocol. 

238 """ 

239 

240 logging.info(f"Starting device {self}") 

241 super().start() 

242 

243 def stop(self) -> None: 

244 """ 

245 Stop the device. Closes also the communication protocol. 

246 """ 

247 

248 logging.info(f"Stopping device {self}") 

249 super().stop() 

250 

251 def _write(self, value: str) -> None: 

252 """ 

253 Write given `value` string to `self.com`. 

254 

255 :param value: String value to send. 

256 :raises SerialCommunicationIOError: when communication port is not opened 

257 """ 

258 

259 self.com.write_text(value) 

260 

261 def _read(self) -> str: 

262 """ 

263 Read a string value from `self.com`. 

264 

265 :return: Read text from the serial port, without the trailing terminator, 

266 as defined in the communcation protocol configuration. 

267 :raises SerialCommunicationIOError: when communication port is not opened 

268 """ 

269 return self.com.read_text().rstrip(self.com.config.terminator_str()) 

270 

271 def activate_output(self, mode: LuminoxOutputMode) -> None: 

272 """ 

273 activate the selected output mode of the Luminox Sensor. 

274 :param mode: polling or streaming 

275 """ 

276 with self.com.access_lock: 

277 self._write(f"M {mode.value}") 

278 # needs a little bit of time ot activate 

279 sleep(self.config.wait_sec_post_activate) 

280 

281 for trial in range(self.config.nr_trials_activate + 1): 

282 msg = self._read() 

283 if ( 

284 not msg == f"M 0{mode.value}" 

285 and trial == self.config.nr_trials_activate 

286 ): 

287 err_msg = ( 

288 f"Stream mode activation was not possible " 

289 f"after {self.config.nr_trials_activate} trials {self}" 

290 ) 

291 logging.error(err_msg) 

292 raise LuminoxOutputModeError(err_msg) 

293 if msg == f"M 0{mode.value}": 

294 msg = ( 

295 f"Stream mode activation possible " 

296 f"in trial {trial} out of {self.config.nr_trials_activate}" 

297 ) 

298 logging.info(msg) 

299 break 

300 sleep(self.config.wait_sec_trials_activate) 

301 

302 self.output = mode 

303 logging.info(f"{mode.name} mode activated {self}") 

304 

305 def read_streaming(self) -> LuminoxMeasurementTypeDict: 

306 """ 

307 Read values of Luminox in the streaming mode. Convert the single string 

308 into separate values. 

309 

310 :return: dictionary with `LuminoxMeasurementType.all_measurements_types()` keys 

311 and accordingly type-parsed values. 

312 :raises LuminoxOutputModeError: when streaming mode is not activated 

313 :raises LuminoxMeasurementTypeError: when any of expected measurement values is 

314 not read 

315 """ 

316 if not self.output == LuminoxOutputMode.streaming: 

317 err_msg = f"Streaming mode not activated {self}" 

318 logging.error(err_msg) 

319 raise LuminoxOutputModeError(err_msg) 

320 

321 read_txt = self._read() 

322 return cast( 

323 LuminoxMeasurementTypeDict, 

324 cast( 

325 LuminoxMeasurementType, LuminoxMeasurementType.all_measurements 

326 ).parse_read_measurement_value(read_txt), 

327 ) 

328 

329 def query_polling( 

330 self, 

331 measurement: Union[str, LuminoxMeasurementType], 

332 ) -> Union[LuminoxMeasurementTypeDict, LuminoxMeasurementTypeValue]: 

333 """ 

334 Query a value or values of Luminox measurements in the polling mode, 

335 according to a given measurement type. 

336 

337 :param measurement: type of measurement 

338 :return: value of requested measurement 

339 :raises ValueError: when a wrong key for LuminoxMeasurementType is provided 

340 :raises LuminoxOutputModeError: when polling mode is not activated 

341 :raises LuminoxMeasurementTypeError: when expected measurement value is not read 

342 """ 

343 if not isinstance(measurement, LuminoxMeasurementType): 

344 try: 

345 measurement = cast( 

346 LuminoxMeasurementType, 

347 LuminoxMeasurementType[measurement], # type: ignore 

348 ) 

349 except KeyError: 

350 measurement = cast( 

351 LuminoxMeasurementType, 

352 LuminoxMeasurementType(measurement), 

353 ) 

354 

355 if not self.output == LuminoxOutputMode.polling: 

356 err_msg = f"Polling mode not activated {self}" 

357 logging.error(err_msg) 

358 raise LuminoxOutputModeError(err_msg) 

359 

360 with self.com.access_lock: 

361 self._write(str(measurement)) 

362 read_txt = self._read() 

363 read_value = measurement.parse_read_measurement_value(read_txt) 

364 return read_value