oxy_lc.oxy_lc
1import minimalmodbus 2from oxy_lc.utilities.modbus_registers import HoldingRegister, InputRegister 3from oxy_lc.utilities.conversions import twos_compliment 4from enum import IntEnum 5from time import sleep 6 7# region Errors 8 9class ValueRangeError(Exception): 10 """Exception raised for value range error scenarios. 11 12 Attributes: 13 message -- explanation of the error 14 """ 15 16 def __init__(self, message): 17 self.message = message 18 super().__init__(self.message) 19 20# endregion 21 22 23class OxyLc(minimalmodbus.Instrument): 24 """ 25 Main object for OxyLC communication 26 27 :param minimalmodbus: Child of minimalmodbus to use all included methods as standard 28 """ 29 30 def __init__(self, portname: str, slaveaddress: int = 1): 31 """ 32 Initialiser for OxyLC communication. This should setup the connection to the device at the required protocol. 33 34 :param portname: Portname for the connection. Likely "COM*" on windows or "/dev/ttyUSB*" in Linux 35 :type portname: str 36 :param slaveaddress: Slave address of the device. Unless changed by the user this defaults to 1 on new products. 37 :type slaveaddress: int 38 """ 39 minimalmodbus.Instrument.__init__(self, portname, slaveaddress) 40 self.serial.baudrate = 9600 41 42 # region enums 43 44 class StatusValues(IntEnum): 45 IDLE = 0 46 START_UP = 1 47 OPERATING = 2 48 SHUT_DOWN = 3 49 STANDBY = 4 50 51 class SensorState(IntEnum): 52 OFF = 0 53 ON = 1 54 STANDBY = 2 55 56 class HeaterOptions(IntEnum): 57 HEATER_4V0 = 0 58 HEATER_4V2 = 1 59 HEATER_4V35 = 2 60 HEATER_4V55 = 3 61 62 class SaveAndApply(IntEnum): 63 IDLE = 0 64 APPLY = 1 65 66 class CalibrationStatus(IntEnum): 67 IDLE = 0 68 IN_PROGRESS = 1 69 COMPLETED = 2 70 71 class CalibrationControl(IntEnum): 72 DEFAULT = 0 73 ACTIVATE = 1 74 RESET = 2 75 76 class BaudRates(IntEnum): 77 _2400 = 0 78 _4800 = 1 79 _9600 = 2 80 _19200 = 3 81 _38400 = 4 82 _57600 = 5 83 _115200 = 6 84 85 class Parity(IntEnum): 86 NONE = 0 87 ODD = 1 88 EVEN = 2 89 90 class StopBits(IntEnum): 91 _1 = 0 92 _2 = 1 93 94 class RS485ApplyChanges(IntEnum): 95 IDLE = 0 96 APPLY = 1 97 98 # endregion 99 100 # region Properties 101 102 @property 103 def o2_average(self) -> float: 104 """ 105 Get live sensor reading - O2 Average 106 107 :return: Current averaged O2 reading from the sensor (%) 108 :rtype: float 109 """ 110 o2_reading = self.read_register(InputRegister.O2_AVERAGE, functioncode=4) 111 return o2_reading / 100 112 113 @property 114 def o2_raw(self) -> float: 115 """ 116 Get live sensor reading - O2 raw 117 118 :return: Current raw O2 reading from the sensor (%) 119 :rtype: float 120 """ 121 o2_reading = self.read_register(InputRegister.O2_RAW, functioncode=4) 122 return o2_reading / 100 123 124 @property 125 def asymmetry(self) -> float: 126 """ 127 Get live sensor reading - O2 asymmetry 128 129 :return: Current asymmetry of the sensor 130 :rtype: float 131 """ 132 asymmetry_reading = self.read_register(InputRegister.ASYMMETRY, functioncode=4) 133 return asymmetry_reading / 1000 134 135 @property 136 def status(self) -> StatusValues: 137 """ 138 Get live sensor reading - current device status 139 Returned as Enum from StatusValues 140 141 :return: Current status of the device 142 :rtype: StatusValues 143 """ 144 status = self.read_register(InputRegister.SYSTEM_STATUS, functioncode=4) 145 return self.StatusValues(status) 146 147 @property 148 def sensor_state(self) -> SensorState: 149 """ 150 Get live sensor reading - Sensor State 151 152 :return: Current state of the sensor 153 :rtype: SensorState 154 """ 155 _sensor_state = self.read_register(HoldingRegister.SENSOR_STATE, functioncode=3) 156 return self.SensorState(_sensor_state) 157 158 @sensor_state.setter 159 def sensor_state(self, state: SensorState) -> None: 160 """ 161 Set the current state of the sensor 162 163 :param state: State option from Enum SensorState 164 :type state: SensorState 165 """ 166 self.write_register(HoldingRegister.SENSOR_STATE, state) 167 168 @property 169 def heater_voltage(self) -> float: 170 """ 171 Get sensor setting - heater voltage 172 173 :return: Current set heater voltage of the sensor (Volts) 174 :rtype: float 175 """ 176 _heater_Voltage = self.read_register( 177 InputRegister.HEATER_VOLTAGE, functioncode=4 178 ) 179 return _heater_Voltage / 100 180 181 @heater_voltage.setter 182 def heater_voltage(self, set_voltage: HeaterOptions) -> None: 183 """ 184 Set the current heater voltage for the sensor 185 186 :param state: State option from Enum HeaterOptions 187 :type state: HeaterOptions 188 """ 189 self.write_register(HoldingRegister.HEATER_VOLTAGE, set_voltage) 190 191 @property 192 def warnings(self) -> str: 193 """ 194 Getting current warnings as a string of bits as received by the sensor 195 196 :return: Warning states as bit string 197 :rtype: str 198 """ 199 200 warnings_hex = self.read_register(InputRegister.WARNINGS, functioncode=4) 201 202 decode_to_bits = f"{warnings_hex:08b}" 203 204 return decode_to_bits 205 206 @property 207 def td_average(self) -> float: 208 """ 209 Get live sensor TD average 210 211 :return: TD Average (ms) 212 :rtype: float 213 """ 214 td_average = self.read_register(InputRegister.TD_AVERAGE, functioncode=4) 215 return td_average / 10 216 217 @property 218 def td_raw(self) -> float: 219 """ 220 Get live sensor TD raw 221 222 :return: TD raw (ms) 223 :rtype: float 224 """ 225 td_raw = self.read_register(InputRegister.TD_RAW, functioncode=4) 226 return td_raw / 10 227 228 @property 229 def tp(self) -> float: 230 """ 231 Get live sensor TP 232 233 :return: TP (ms) 234 :rtype: float 235 """ 236 tp = self.read_register(InputRegister.TP, functioncode=4) 237 return tp / 10 238 239 @property 240 def t1(self) -> float: 241 """ 242 Get live sensor T1 243 244 :return: T1 (ms) 245 :rtype: float 246 """ 247 t1 = self.read_register(InputRegister.T1, functioncode=4) 248 return t1 / 10 249 250 @property 251 def t2(self) -> float: 252 """ 253 Get live sensor T2 254 255 :return: T2 (ms) 256 :rtype: float 257 """ 258 t2 = self.read_register(InputRegister.T2, functioncode=4) 259 return t2 / 10 260 261 @property 262 def t4(self) -> float: 263 """ 264 Get live sensor T4 265 266 :return: T4 (ms) 267 :rtype: float 268 """ 269 t4 = self.read_register(InputRegister.T4, functioncode=4) 270 return t4 / 10 271 272 @property 273 def t5(self) -> float: 274 """ 275 Get live sensor T5 276 277 :return: T5 (ms) 278 :rtype: float 279 """ 280 t5 = self.read_register(InputRegister.T5, functioncode=4) 281 return t5 / 10 282 283 @property 284 def pp_o2_real(self) -> float: 285 """ 286 Get live ppO2 Real 287 288 :return: ppO2 Real 289 :rtype: float 290 """ 291 pp_o2 = self.read_register(InputRegister.PPO2_REAL, functioncode=4) 292 return pp_o2 / 10 293 294 @property 295 def pp_o2_raw(self) -> float: 296 """ 297 Get live ppO2 raw 298 299 :return: ppO2 raw 300 :rtype: float 301 """ 302 pp_o2 = self.read_register(InputRegister.PPO2_RAW, functioncode=4) 303 return pp_o2 / 10 304 305 @property 306 def pressure(self) -> float: 307 """ 308 Get live pressure 309 310 :return: pressure (mbar) 311 :rtype: float 312 """ 313 _pressure = self.read_register(InputRegister.PRESSURE, functioncode=4) 314 return _pressure 315 316 @property 317 def pressure_sens_temperature(self) -> float: 318 """ 319 Get live temperature of the pressure sensor 320 321 :return: temperature (°C) 322 :rtype: float 323 """ 324 temp_decimal = self.read_register( 325 InputRegister.PRESSURE_SENS_TEMP, functioncode=4 326 ) 327 328 converted_temperature = twos_compliment(temp_decimal, 16) 329 330 return converted_temperature 331 332 @property 333 def calibration_status(self) -> CalibrationStatus: 334 """ 335 Get current calibration status of the sensor 336 337 :return: calibration status 338 :rtype: CalibrationStatus 339 """ 340 _cal_status = self.read_register( 341 InputRegister.CALIBRATION_STATUS, functioncode=4 342 ) 343 return self.CalibrationStatus(_cal_status) 344 345 @property 346 def year_of_manufacture(self) -> int: 347 """ 348 Get Year of Manufacture 349 350 :return: Year of Manufacture (YYYY) 351 :rtype: int 352 """ 353 _year_of_manufacture = self.read_register(InputRegister.YOM, functioncode=4) 354 return _year_of_manufacture 355 356 @property 357 def day_of_manufacture(self) -> int: 358 """ 359 Get Day of Manufacture 360 361 :return: Day of Manufacture (DDD) 362 :rtype: int 363 """ 364 _day_of_manufacture = self.read_register(InputRegister.DOM, functioncode=4) 365 return _day_of_manufacture 366 367 @property 368 def serial_number(self) -> int: 369 """ 370 Get Serial Number 371 372 :return: Serial Number (XXXXX) 373 :rtype: int 374 """ 375 _serial_number = self.read_register(InputRegister.SERIAL_NO, functioncode=4) 376 return _serial_number 377 378 @property 379 def software_revision(self) -> int: 380 """ 381 Get Software Revision 382 383 :return: Software Revision (RRR) 384 :rtype: int 385 """ 386 _software_revision = self.read_register( 387 InputRegister.SOFTWARE_REV, functioncode=4 388 ) 389 return _software_revision 390 391 @property 392 def calibration_value(self) -> float: 393 """ 394 Return current calibration value 395 calibration_value defaults to 20.7 on new interface boards. 396 397 :return: calibration (%) 398 :rtype: float 399 """ 400 _calibration_value = self.read_register(HoldingRegister.CALIBRATION_PERCENT) 401 return _calibration_value / 100 402 403 @calibration_value.setter 404 def calibration_value(self, value: float) -> None: 405 """ 406 Set the calibration value for the sensor. 407 Value is then stored in memory even after power cycle 408 409 :param value: Calibration value between 0% and 100% 410 :type value: float 411 :raises self.ValueRangeError: Value out of range 412 """ 413 if (value < 0) or (value > 100): 414 raise ValueRangeError( 415 f"Calibration value {value} out of range. Value Must be between 0 and 100" 416 ) 417 418 self.write_register(HoldingRegister.CALIBRATION_PERCENT, int(value * 100)) 419 420 @property 421 def calibration_control(self) -> CalibrationControl: 422 """ 423 Current Calibration control setting 424 425 :return: calibration control 426 :rtype: CalibrationControl 427 """ 428 _calibration_control = self.read_register(HoldingRegister.CALIBRATION_CONTROL) 429 return self.CalibrationControl(_calibration_control) 430 431 @calibration_control.setter 432 def calibration_control(self, control_setting: CalibrationControl) -> None: 433 """ 434 Set calibration control 435 436 :param control_setting: Control setting 437 :type control_setting: CalibrationControl 438 """ 439 self.write_register(HoldingRegister.CALIBRATION_CONTROL, control_setting) 440 441 @property 442 def device_address(self) -> int: 443 """ 444 Get device address 445 446 :return: Address (1-247) 447 :rtype: int 448 """ 449 _address = self.read_register(HoldingRegister.ADDRESS) 450 return _address 451 452 @device_address.setter 453 def device_address(self, new_address: int) -> None: 454 """ 455 Set new device Address 456 457 :param new_address: between 1 & 247 458 :type new_address: int 459 :raises ValueRangeError: Address out of range 460 """ 461 if (new_address < 1) or (new_address > 247): 462 raise ValueRangeError("Address must be between 1 and 247") 463 464 self.write_register(HoldingRegister.ADDRESS, new_address) 465 466 @property 467 def parity(self) -> Parity: 468 """ 469 Get device parity 470 471 :return: parity 472 :rtype: Parity 473 """ 474 _parity = self.read_register(HoldingRegister.PARITY) 475 return self.Parity(_parity) 476 477 @parity.setter 478 def parity(self, new_parity: Parity) -> None: 479 """ 480 Set new device parity 481 482 :param new_parity: new parity from enum 483 :type new_parity: Parity 484 """ 485 self.write_register(HoldingRegister.PARITY, new_parity) 486 487 # endregion 488 489 # region Methods 490 491 def turn_on(self): 492 """ 493 Turn the sensor On 494 """ 495 self.sensor_state = self.SensorState.ON 496 497 def turn_off(self): 498 """ 499 Turn the sensor Off 500 """ 501 self.sensor_state = self.SensorState.OFF 502 503 def calibrate(self, calibration_value: float | None = None) -> bool: 504 """ 505 Calibrate the sensor to the given percent value 506 507 :param calibration_precent: O2 percent for calibration, If None is passed it will use the stored calibration_value. 508 calibration_value defaults to 20.7 on new interface boards. 509 :type calibration_precent: float, optional 510 :return: boolean indication of success 511 :rtype: bool 512 """ 513 if self.status != self.StatusValues.OPERATING: 514 print("Sensor must be in operation to calibrate") 515 return False 516 517 if calibration_value: 518 try: 519 self.calibration_value = calibration_value 520 except ValueRangeError as e: 521 print(e) 522 return False 523 524 self.calibration_control = self.CalibrationControl.ACTIVATE 525 526 count = 0 527 timeout = 5 528 while (self.calibration_status != self.CalibrationStatus.COMPLETED) and ( 529 count != timeout 530 ): 531 sleep(1) 532 count += 1 533 534 self.write_register( 535 HoldingRegister.CALIBRATION_CONTROL, self.CalibrationControl.RESET 536 ) 537 538 if count == timeout: 539 print("calibration timeout") 540 return False 541 else: 542 return True 543 544 def clear_error_flags(self) -> None: 545 """ 546 Clears error flags on the device 547 """ 548 self.write_register(HoldingRegister.CLEAR_FLAGS, 1) 549 550 def set_and_apply_heater_voltage(self, set_voltage: HeaterOptions | None) -> None: 551 """ 552 Set the current heater voltage for the sensor 553 554 :param state: State option from Enum HeaterOptions 555 :type state: HeaterOptions 556 """ 557 if set_voltage: 558 self.heater_voltage = set_voltage 559 560 # MinimalModbus will throw a NoResonseError when this register is written. 561 # It must be passed or it will crash the program 562 try: 563 self.write_register( 564 HoldingRegister.HEATER_VOLTAGE_SAVE, self.SaveAndApply.APPLY 565 ) 566 except minimalmodbus.NoResponseError: 567 sleep(2) 568 569 self.sensor_state = self.SensorState.ON 570 571 572 def display_warnings(self) -> dict[str:bool]: 573 """ 574 Reads and decodes the warnings and returns a dictionary with each warning/error with a corresponding boolean showing error state 575 576 :return: Warning states for each of the representative bits (True is error) 577 :rtype: dict[str: bool] 578 """ 579 warning_states = { 580 "Pump Error": False, 581 "Heater Voltage Error": False, 582 "Asymmetry Warning": False, 583 "O2 Low Warning": False, 584 "Pressure Sensor Warning": False, 585 "Pressure Sensor Error": False, 586 } 587 warnings_bits = self.warnings 588 589 for count, state in enumerate(warning_states): 590 if warnings_bits[::-1][count] == '1': 591 warning_states[state] = True 592 593 return warning_states 594 595 # endregion
10class ValueRangeError(Exception): 11 """Exception raised for value range error scenarios. 12 13 Attributes: 14 message -- explanation of the error 15 """ 16 17 def __init__(self, message): 18 self.message = message 19 super().__init__(self.message)
Exception raised for value range error scenarios.
Attributes: message -- explanation of the error
24class OxyLc(minimalmodbus.Instrument): 25 """ 26 Main object for OxyLC communication 27 28 :param minimalmodbus: Child of minimalmodbus to use all included methods as standard 29 """ 30 31 def __init__(self, portname: str, slaveaddress: int = 1): 32 """ 33 Initialiser for OxyLC communication. This should setup the connection to the device at the required protocol. 34 35 :param portname: Portname for the connection. Likely "COM*" on windows or "/dev/ttyUSB*" in Linux 36 :type portname: str 37 :param slaveaddress: Slave address of the device. Unless changed by the user this defaults to 1 on new products. 38 :type slaveaddress: int 39 """ 40 minimalmodbus.Instrument.__init__(self, portname, slaveaddress) 41 self.serial.baudrate = 9600 42 43 # region enums 44 45 class StatusValues(IntEnum): 46 IDLE = 0 47 START_UP = 1 48 OPERATING = 2 49 SHUT_DOWN = 3 50 STANDBY = 4 51 52 class SensorState(IntEnum): 53 OFF = 0 54 ON = 1 55 STANDBY = 2 56 57 class HeaterOptions(IntEnum): 58 HEATER_4V0 = 0 59 HEATER_4V2 = 1 60 HEATER_4V35 = 2 61 HEATER_4V55 = 3 62 63 class SaveAndApply(IntEnum): 64 IDLE = 0 65 APPLY = 1 66 67 class CalibrationStatus(IntEnum): 68 IDLE = 0 69 IN_PROGRESS = 1 70 COMPLETED = 2 71 72 class CalibrationControl(IntEnum): 73 DEFAULT = 0 74 ACTIVATE = 1 75 RESET = 2 76 77 class BaudRates(IntEnum): 78 _2400 = 0 79 _4800 = 1 80 _9600 = 2 81 _19200 = 3 82 _38400 = 4 83 _57600 = 5 84 _115200 = 6 85 86 class Parity(IntEnum): 87 NONE = 0 88 ODD = 1 89 EVEN = 2 90 91 class StopBits(IntEnum): 92 _1 = 0 93 _2 = 1 94 95 class RS485ApplyChanges(IntEnum): 96 IDLE = 0 97 APPLY = 1 98 99 # endregion 100 101 # region Properties 102 103 @property 104 def o2_average(self) -> float: 105 """ 106 Get live sensor reading - O2 Average 107 108 :return: Current averaged O2 reading from the sensor (%) 109 :rtype: float 110 """ 111 o2_reading = self.read_register(InputRegister.O2_AVERAGE, functioncode=4) 112 return o2_reading / 100 113 114 @property 115 def o2_raw(self) -> float: 116 """ 117 Get live sensor reading - O2 raw 118 119 :return: Current raw O2 reading from the sensor (%) 120 :rtype: float 121 """ 122 o2_reading = self.read_register(InputRegister.O2_RAW, functioncode=4) 123 return o2_reading / 100 124 125 @property 126 def asymmetry(self) -> float: 127 """ 128 Get live sensor reading - O2 asymmetry 129 130 :return: Current asymmetry of the sensor 131 :rtype: float 132 """ 133 asymmetry_reading = self.read_register(InputRegister.ASYMMETRY, functioncode=4) 134 return asymmetry_reading / 1000 135 136 @property 137 def status(self) -> StatusValues: 138 """ 139 Get live sensor reading - current device status 140 Returned as Enum from StatusValues 141 142 :return: Current status of the device 143 :rtype: StatusValues 144 """ 145 status = self.read_register(InputRegister.SYSTEM_STATUS, functioncode=4) 146 return self.StatusValues(status) 147 148 @property 149 def sensor_state(self) -> SensorState: 150 """ 151 Get live sensor reading - Sensor State 152 153 :return: Current state of the sensor 154 :rtype: SensorState 155 """ 156 _sensor_state = self.read_register(HoldingRegister.SENSOR_STATE, functioncode=3) 157 return self.SensorState(_sensor_state) 158 159 @sensor_state.setter 160 def sensor_state(self, state: SensorState) -> None: 161 """ 162 Set the current state of the sensor 163 164 :param state: State option from Enum SensorState 165 :type state: SensorState 166 """ 167 self.write_register(HoldingRegister.SENSOR_STATE, state) 168 169 @property 170 def heater_voltage(self) -> float: 171 """ 172 Get sensor setting - heater voltage 173 174 :return: Current set heater voltage of the sensor (Volts) 175 :rtype: float 176 """ 177 _heater_Voltage = self.read_register( 178 InputRegister.HEATER_VOLTAGE, functioncode=4 179 ) 180 return _heater_Voltage / 100 181 182 @heater_voltage.setter 183 def heater_voltage(self, set_voltage: HeaterOptions) -> None: 184 """ 185 Set the current heater voltage for the sensor 186 187 :param state: State option from Enum HeaterOptions 188 :type state: HeaterOptions 189 """ 190 self.write_register(HoldingRegister.HEATER_VOLTAGE, set_voltage) 191 192 @property 193 def warnings(self) -> str: 194 """ 195 Getting current warnings as a string of bits as received by the sensor 196 197 :return: Warning states as bit string 198 :rtype: str 199 """ 200 201 warnings_hex = self.read_register(InputRegister.WARNINGS, functioncode=4) 202 203 decode_to_bits = f"{warnings_hex:08b}" 204 205 return decode_to_bits 206 207 @property 208 def td_average(self) -> float: 209 """ 210 Get live sensor TD average 211 212 :return: TD Average (ms) 213 :rtype: float 214 """ 215 td_average = self.read_register(InputRegister.TD_AVERAGE, functioncode=4) 216 return td_average / 10 217 218 @property 219 def td_raw(self) -> float: 220 """ 221 Get live sensor TD raw 222 223 :return: TD raw (ms) 224 :rtype: float 225 """ 226 td_raw = self.read_register(InputRegister.TD_RAW, functioncode=4) 227 return td_raw / 10 228 229 @property 230 def tp(self) -> float: 231 """ 232 Get live sensor TP 233 234 :return: TP (ms) 235 :rtype: float 236 """ 237 tp = self.read_register(InputRegister.TP, functioncode=4) 238 return tp / 10 239 240 @property 241 def t1(self) -> float: 242 """ 243 Get live sensor T1 244 245 :return: T1 (ms) 246 :rtype: float 247 """ 248 t1 = self.read_register(InputRegister.T1, functioncode=4) 249 return t1 / 10 250 251 @property 252 def t2(self) -> float: 253 """ 254 Get live sensor T2 255 256 :return: T2 (ms) 257 :rtype: float 258 """ 259 t2 = self.read_register(InputRegister.T2, functioncode=4) 260 return t2 / 10 261 262 @property 263 def t4(self) -> float: 264 """ 265 Get live sensor T4 266 267 :return: T4 (ms) 268 :rtype: float 269 """ 270 t4 = self.read_register(InputRegister.T4, functioncode=4) 271 return t4 / 10 272 273 @property 274 def t5(self) -> float: 275 """ 276 Get live sensor T5 277 278 :return: T5 (ms) 279 :rtype: float 280 """ 281 t5 = self.read_register(InputRegister.T5, functioncode=4) 282 return t5 / 10 283 284 @property 285 def pp_o2_real(self) -> float: 286 """ 287 Get live ppO2 Real 288 289 :return: ppO2 Real 290 :rtype: float 291 """ 292 pp_o2 = self.read_register(InputRegister.PPO2_REAL, functioncode=4) 293 return pp_o2 / 10 294 295 @property 296 def pp_o2_raw(self) -> float: 297 """ 298 Get live ppO2 raw 299 300 :return: ppO2 raw 301 :rtype: float 302 """ 303 pp_o2 = self.read_register(InputRegister.PPO2_RAW, functioncode=4) 304 return pp_o2 / 10 305 306 @property 307 def pressure(self) -> float: 308 """ 309 Get live pressure 310 311 :return: pressure (mbar) 312 :rtype: float 313 """ 314 _pressure = self.read_register(InputRegister.PRESSURE, functioncode=4) 315 return _pressure 316 317 @property 318 def pressure_sens_temperature(self) -> float: 319 """ 320 Get live temperature of the pressure sensor 321 322 :return: temperature (°C) 323 :rtype: float 324 """ 325 temp_decimal = self.read_register( 326 InputRegister.PRESSURE_SENS_TEMP, functioncode=4 327 ) 328 329 converted_temperature = twos_compliment(temp_decimal, 16) 330 331 return converted_temperature 332 333 @property 334 def calibration_status(self) -> CalibrationStatus: 335 """ 336 Get current calibration status of the sensor 337 338 :return: calibration status 339 :rtype: CalibrationStatus 340 """ 341 _cal_status = self.read_register( 342 InputRegister.CALIBRATION_STATUS, functioncode=4 343 ) 344 return self.CalibrationStatus(_cal_status) 345 346 @property 347 def year_of_manufacture(self) -> int: 348 """ 349 Get Year of Manufacture 350 351 :return: Year of Manufacture (YYYY) 352 :rtype: int 353 """ 354 _year_of_manufacture = self.read_register(InputRegister.YOM, functioncode=4) 355 return _year_of_manufacture 356 357 @property 358 def day_of_manufacture(self) -> int: 359 """ 360 Get Day of Manufacture 361 362 :return: Day of Manufacture (DDD) 363 :rtype: int 364 """ 365 _day_of_manufacture = self.read_register(InputRegister.DOM, functioncode=4) 366 return _day_of_manufacture 367 368 @property 369 def serial_number(self) -> int: 370 """ 371 Get Serial Number 372 373 :return: Serial Number (XXXXX) 374 :rtype: int 375 """ 376 _serial_number = self.read_register(InputRegister.SERIAL_NO, functioncode=4) 377 return _serial_number 378 379 @property 380 def software_revision(self) -> int: 381 """ 382 Get Software Revision 383 384 :return: Software Revision (RRR) 385 :rtype: int 386 """ 387 _software_revision = self.read_register( 388 InputRegister.SOFTWARE_REV, functioncode=4 389 ) 390 return _software_revision 391 392 @property 393 def calibration_value(self) -> float: 394 """ 395 Return current calibration value 396 calibration_value defaults to 20.7 on new interface boards. 397 398 :return: calibration (%) 399 :rtype: float 400 """ 401 _calibration_value = self.read_register(HoldingRegister.CALIBRATION_PERCENT) 402 return _calibration_value / 100 403 404 @calibration_value.setter 405 def calibration_value(self, value: float) -> None: 406 """ 407 Set the calibration value for the sensor. 408 Value is then stored in memory even after power cycle 409 410 :param value: Calibration value between 0% and 100% 411 :type value: float 412 :raises self.ValueRangeError: Value out of range 413 """ 414 if (value < 0) or (value > 100): 415 raise ValueRangeError( 416 f"Calibration value {value} out of range. Value Must be between 0 and 100" 417 ) 418 419 self.write_register(HoldingRegister.CALIBRATION_PERCENT, int(value * 100)) 420 421 @property 422 def calibration_control(self) -> CalibrationControl: 423 """ 424 Current Calibration control setting 425 426 :return: calibration control 427 :rtype: CalibrationControl 428 """ 429 _calibration_control = self.read_register(HoldingRegister.CALIBRATION_CONTROL) 430 return self.CalibrationControl(_calibration_control) 431 432 @calibration_control.setter 433 def calibration_control(self, control_setting: CalibrationControl) -> None: 434 """ 435 Set calibration control 436 437 :param control_setting: Control setting 438 :type control_setting: CalibrationControl 439 """ 440 self.write_register(HoldingRegister.CALIBRATION_CONTROL, control_setting) 441 442 @property 443 def device_address(self) -> int: 444 """ 445 Get device address 446 447 :return: Address (1-247) 448 :rtype: int 449 """ 450 _address = self.read_register(HoldingRegister.ADDRESS) 451 return _address 452 453 @device_address.setter 454 def device_address(self, new_address: int) -> None: 455 """ 456 Set new device Address 457 458 :param new_address: between 1 & 247 459 :type new_address: int 460 :raises ValueRangeError: Address out of range 461 """ 462 if (new_address < 1) or (new_address > 247): 463 raise ValueRangeError("Address must be between 1 and 247") 464 465 self.write_register(HoldingRegister.ADDRESS, new_address) 466 467 @property 468 def parity(self) -> Parity: 469 """ 470 Get device parity 471 472 :return: parity 473 :rtype: Parity 474 """ 475 _parity = self.read_register(HoldingRegister.PARITY) 476 return self.Parity(_parity) 477 478 @parity.setter 479 def parity(self, new_parity: Parity) -> None: 480 """ 481 Set new device parity 482 483 :param new_parity: new parity from enum 484 :type new_parity: Parity 485 """ 486 self.write_register(HoldingRegister.PARITY, new_parity) 487 488 # endregion 489 490 # region Methods 491 492 def turn_on(self): 493 """ 494 Turn the sensor On 495 """ 496 self.sensor_state = self.SensorState.ON 497 498 def turn_off(self): 499 """ 500 Turn the sensor Off 501 """ 502 self.sensor_state = self.SensorState.OFF 503 504 def calibrate(self, calibration_value: float | None = None) -> bool: 505 """ 506 Calibrate the sensor to the given percent value 507 508 :param calibration_precent: O2 percent for calibration, If None is passed it will use the stored calibration_value. 509 calibration_value defaults to 20.7 on new interface boards. 510 :type calibration_precent: float, optional 511 :return: boolean indication of success 512 :rtype: bool 513 """ 514 if self.status != self.StatusValues.OPERATING: 515 print("Sensor must be in operation to calibrate") 516 return False 517 518 if calibration_value: 519 try: 520 self.calibration_value = calibration_value 521 except ValueRangeError as e: 522 print(e) 523 return False 524 525 self.calibration_control = self.CalibrationControl.ACTIVATE 526 527 count = 0 528 timeout = 5 529 while (self.calibration_status != self.CalibrationStatus.COMPLETED) and ( 530 count != timeout 531 ): 532 sleep(1) 533 count += 1 534 535 self.write_register( 536 HoldingRegister.CALIBRATION_CONTROL, self.CalibrationControl.RESET 537 ) 538 539 if count == timeout: 540 print("calibration timeout") 541 return False 542 else: 543 return True 544 545 def clear_error_flags(self) -> None: 546 """ 547 Clears error flags on the device 548 """ 549 self.write_register(HoldingRegister.CLEAR_FLAGS, 1) 550 551 def set_and_apply_heater_voltage(self, set_voltage: HeaterOptions | None) -> None: 552 """ 553 Set the current heater voltage for the sensor 554 555 :param state: State option from Enum HeaterOptions 556 :type state: HeaterOptions 557 """ 558 if set_voltage: 559 self.heater_voltage = set_voltage 560 561 # MinimalModbus will throw a NoResonseError when this register is written. 562 # It must be passed or it will crash the program 563 try: 564 self.write_register( 565 HoldingRegister.HEATER_VOLTAGE_SAVE, self.SaveAndApply.APPLY 566 ) 567 except minimalmodbus.NoResponseError: 568 sleep(2) 569 570 self.sensor_state = self.SensorState.ON 571 572 573 def display_warnings(self) -> dict[str:bool]: 574 """ 575 Reads and decodes the warnings and returns a dictionary with each warning/error with a corresponding boolean showing error state 576 577 :return: Warning states for each of the representative bits (True is error) 578 :rtype: dict[str: bool] 579 """ 580 warning_states = { 581 "Pump Error": False, 582 "Heater Voltage Error": False, 583 "Asymmetry Warning": False, 584 "O2 Low Warning": False, 585 "Pressure Sensor Warning": False, 586 "Pressure Sensor Error": False, 587 } 588 warnings_bits = self.warnings 589 590 for count, state in enumerate(warning_states): 591 if warnings_bits[::-1][count] == '1': 592 warning_states[state] = True 593 594 return warning_states 595 596 # endregion
Main object for OxyLC communication
Parameters
- minimalmodbus: Child of minimalmodbus to use all included methods as standard
31 def __init__(self, portname: str, slaveaddress: int = 1): 32 """ 33 Initialiser for OxyLC communication. This should setup the connection to the device at the required protocol. 34 35 :param portname: Portname for the connection. Likely "COM*" on windows or "/dev/ttyUSB*" in Linux 36 :type portname: str 37 :param slaveaddress: Slave address of the device. Unless changed by the user this defaults to 1 on new products. 38 :type slaveaddress: int 39 """ 40 minimalmodbus.Instrument.__init__(self, portname, slaveaddress) 41 self.serial.baudrate = 9600
Initialiser for OxyLC communication. This should setup the connection to the device at the required protocol.
Parameters
- portname: Portname for the connection. Likely "COM" on windows or "/dev/ttyUSB" in Linux
- slaveaddress: Slave address of the device. Unless changed by the user this defaults to 1 on new products.
103 @property 104 def o2_average(self) -> float: 105 """ 106 Get live sensor reading - O2 Average 107 108 :return: Current averaged O2 reading from the sensor (%) 109 :rtype: float 110 """ 111 o2_reading = self.read_register(InputRegister.O2_AVERAGE, functioncode=4) 112 return o2_reading / 100
Get live sensor reading - O2 Average
Returns
Current averaged O2 reading from the sensor (%)
114 @property 115 def o2_raw(self) -> float: 116 """ 117 Get live sensor reading - O2 raw 118 119 :return: Current raw O2 reading from the sensor (%) 120 :rtype: float 121 """ 122 o2_reading = self.read_register(InputRegister.O2_RAW, functioncode=4) 123 return o2_reading / 100
Get live sensor reading - O2 raw
Returns
Current raw O2 reading from the sensor (%)
125 @property 126 def asymmetry(self) -> float: 127 """ 128 Get live sensor reading - O2 asymmetry 129 130 :return: Current asymmetry of the sensor 131 :rtype: float 132 """ 133 asymmetry_reading = self.read_register(InputRegister.ASYMMETRY, functioncode=4) 134 return asymmetry_reading / 1000
Get live sensor reading - O2 asymmetry
Returns
Current asymmetry of the sensor
136 @property 137 def status(self) -> StatusValues: 138 """ 139 Get live sensor reading - current device status 140 Returned as Enum from StatusValues 141 142 :return: Current status of the device 143 :rtype: StatusValues 144 """ 145 status = self.read_register(InputRegister.SYSTEM_STATUS, functioncode=4) 146 return self.StatusValues(status)
Get live sensor reading - current device status Returned as Enum from StatusValues
Returns
Current status of the device
148 @property 149 def sensor_state(self) -> SensorState: 150 """ 151 Get live sensor reading - Sensor State 152 153 :return: Current state of the sensor 154 :rtype: SensorState 155 """ 156 _sensor_state = self.read_register(HoldingRegister.SENSOR_STATE, functioncode=3) 157 return self.SensorState(_sensor_state)
Get live sensor reading - Sensor State
Returns
Current state of the sensor
169 @property 170 def heater_voltage(self) -> float: 171 """ 172 Get sensor setting - heater voltage 173 174 :return: Current set heater voltage of the sensor (Volts) 175 :rtype: float 176 """ 177 _heater_Voltage = self.read_register( 178 InputRegister.HEATER_VOLTAGE, functioncode=4 179 ) 180 return _heater_Voltage / 100
Get sensor setting - heater voltage
Returns
Current set heater voltage of the sensor (Volts)
192 @property 193 def warnings(self) -> str: 194 """ 195 Getting current warnings as a string of bits as received by the sensor 196 197 :return: Warning states as bit string 198 :rtype: str 199 """ 200 201 warnings_hex = self.read_register(InputRegister.WARNINGS, functioncode=4) 202 203 decode_to_bits = f"{warnings_hex:08b}" 204 205 return decode_to_bits
Getting current warnings as a string of bits as received by the sensor
Returns
Warning states as bit string
207 @property 208 def td_average(self) -> float: 209 """ 210 Get live sensor TD average 211 212 :return: TD Average (ms) 213 :rtype: float 214 """ 215 td_average = self.read_register(InputRegister.TD_AVERAGE, functioncode=4) 216 return td_average / 10
Get live sensor TD average
Returns
TD Average (ms)
218 @property 219 def td_raw(self) -> float: 220 """ 221 Get live sensor TD raw 222 223 :return: TD raw (ms) 224 :rtype: float 225 """ 226 td_raw = self.read_register(InputRegister.TD_RAW, functioncode=4) 227 return td_raw / 10
Get live sensor TD raw
Returns
TD raw (ms)
229 @property 230 def tp(self) -> float: 231 """ 232 Get live sensor TP 233 234 :return: TP (ms) 235 :rtype: float 236 """ 237 tp = self.read_register(InputRegister.TP, functioncode=4) 238 return tp / 10
Get live sensor TP
Returns
TP (ms)
240 @property 241 def t1(self) -> float: 242 """ 243 Get live sensor T1 244 245 :return: T1 (ms) 246 :rtype: float 247 """ 248 t1 = self.read_register(InputRegister.T1, functioncode=4) 249 return t1 / 10
Get live sensor T1
Returns
T1 (ms)
251 @property 252 def t2(self) -> float: 253 """ 254 Get live sensor T2 255 256 :return: T2 (ms) 257 :rtype: float 258 """ 259 t2 = self.read_register(InputRegister.T2, functioncode=4) 260 return t2 / 10
Get live sensor T2
Returns
T2 (ms)
262 @property 263 def t4(self) -> float: 264 """ 265 Get live sensor T4 266 267 :return: T4 (ms) 268 :rtype: float 269 """ 270 t4 = self.read_register(InputRegister.T4, functioncode=4) 271 return t4 / 10
Get live sensor T4
Returns
T4 (ms)
273 @property 274 def t5(self) -> float: 275 """ 276 Get live sensor T5 277 278 :return: T5 (ms) 279 :rtype: float 280 """ 281 t5 = self.read_register(InputRegister.T5, functioncode=4) 282 return t5 / 10
Get live sensor T5
Returns
T5 (ms)
284 @property 285 def pp_o2_real(self) -> float: 286 """ 287 Get live ppO2 Real 288 289 :return: ppO2 Real 290 :rtype: float 291 """ 292 pp_o2 = self.read_register(InputRegister.PPO2_REAL, functioncode=4) 293 return pp_o2 / 10
Get live ppO2 Real
Returns
ppO2 Real
295 @property 296 def pp_o2_raw(self) -> float: 297 """ 298 Get live ppO2 raw 299 300 :return: ppO2 raw 301 :rtype: float 302 """ 303 pp_o2 = self.read_register(InputRegister.PPO2_RAW, functioncode=4) 304 return pp_o2 / 10
Get live ppO2 raw
Returns
ppO2 raw
306 @property 307 def pressure(self) -> float: 308 """ 309 Get live pressure 310 311 :return: pressure (mbar) 312 :rtype: float 313 """ 314 _pressure = self.read_register(InputRegister.PRESSURE, functioncode=4) 315 return _pressure
Get live pressure
Returns
pressure (mbar)
317 @property 318 def pressure_sens_temperature(self) -> float: 319 """ 320 Get live temperature of the pressure sensor 321 322 :return: temperature (°C) 323 :rtype: float 324 """ 325 temp_decimal = self.read_register( 326 InputRegister.PRESSURE_SENS_TEMP, functioncode=4 327 ) 328 329 converted_temperature = twos_compliment(temp_decimal, 16) 330 331 return converted_temperature
Get live temperature of the pressure sensor
Returns
temperature (°C)
333 @property 334 def calibration_status(self) -> CalibrationStatus: 335 """ 336 Get current calibration status of the sensor 337 338 :return: calibration status 339 :rtype: CalibrationStatus 340 """ 341 _cal_status = self.read_register( 342 InputRegister.CALIBRATION_STATUS, functioncode=4 343 ) 344 return self.CalibrationStatus(_cal_status)
Get current calibration status of the sensor
Returns
calibration status
346 @property 347 def year_of_manufacture(self) -> int: 348 """ 349 Get Year of Manufacture 350 351 :return: Year of Manufacture (YYYY) 352 :rtype: int 353 """ 354 _year_of_manufacture = self.read_register(InputRegister.YOM, functioncode=4) 355 return _year_of_manufacture
Get Year of Manufacture
Returns
Year of Manufacture (YYYY)
357 @property 358 def day_of_manufacture(self) -> int: 359 """ 360 Get Day of Manufacture 361 362 :return: Day of Manufacture (DDD) 363 :rtype: int 364 """ 365 _day_of_manufacture = self.read_register(InputRegister.DOM, functioncode=4) 366 return _day_of_manufacture
Get Day of Manufacture
Returns
Day of Manufacture (DDD)
368 @property 369 def serial_number(self) -> int: 370 """ 371 Get Serial Number 372 373 :return: Serial Number (XXXXX) 374 :rtype: int 375 """ 376 _serial_number = self.read_register(InputRegister.SERIAL_NO, functioncode=4) 377 return _serial_number
Get Serial Number
Returns
Serial Number (XXXXX)
379 @property 380 def software_revision(self) -> int: 381 """ 382 Get Software Revision 383 384 :return: Software Revision (RRR) 385 :rtype: int 386 """ 387 _software_revision = self.read_register( 388 InputRegister.SOFTWARE_REV, functioncode=4 389 ) 390 return _software_revision
Get Software Revision
Returns
Software Revision (RRR)
392 @property 393 def calibration_value(self) -> float: 394 """ 395 Return current calibration value 396 calibration_value defaults to 20.7 on new interface boards. 397 398 :return: calibration (%) 399 :rtype: float 400 """ 401 _calibration_value = self.read_register(HoldingRegister.CALIBRATION_PERCENT) 402 return _calibration_value / 100
Return current calibration value calibration_value defaults to 20.7 on new interface boards.
Returns
calibration (%)
421 @property 422 def calibration_control(self) -> CalibrationControl: 423 """ 424 Current Calibration control setting 425 426 :return: calibration control 427 :rtype: CalibrationControl 428 """ 429 _calibration_control = self.read_register(HoldingRegister.CALIBRATION_CONTROL) 430 return self.CalibrationControl(_calibration_control)
Current Calibration control setting
Returns
calibration control
442 @property 443 def device_address(self) -> int: 444 """ 445 Get device address 446 447 :return: Address (1-247) 448 :rtype: int 449 """ 450 _address = self.read_register(HoldingRegister.ADDRESS) 451 return _address
Get device address
Returns
Address (1-247)
467 @property 468 def parity(self) -> Parity: 469 """ 470 Get device parity 471 472 :return: parity 473 :rtype: Parity 474 """ 475 _parity = self.read_register(HoldingRegister.PARITY) 476 return self.Parity(_parity)
Get device parity
Returns
parity
492 def turn_on(self): 493 """ 494 Turn the sensor On 495 """ 496 self.sensor_state = self.SensorState.ON
Turn the sensor On
498 def turn_off(self): 499 """ 500 Turn the sensor Off 501 """ 502 self.sensor_state = self.SensorState.OFF
Turn the sensor Off
504 def calibrate(self, calibration_value: float | None = None) -> bool: 505 """ 506 Calibrate the sensor to the given percent value 507 508 :param calibration_precent: O2 percent for calibration, If None is passed it will use the stored calibration_value. 509 calibration_value defaults to 20.7 on new interface boards. 510 :type calibration_precent: float, optional 511 :return: boolean indication of success 512 :rtype: bool 513 """ 514 if self.status != self.StatusValues.OPERATING: 515 print("Sensor must be in operation to calibrate") 516 return False 517 518 if calibration_value: 519 try: 520 self.calibration_value = calibration_value 521 except ValueRangeError as e: 522 print(e) 523 return False 524 525 self.calibration_control = self.CalibrationControl.ACTIVATE 526 527 count = 0 528 timeout = 5 529 while (self.calibration_status != self.CalibrationStatus.COMPLETED) and ( 530 count != timeout 531 ): 532 sleep(1) 533 count += 1 534 535 self.write_register( 536 HoldingRegister.CALIBRATION_CONTROL, self.CalibrationControl.RESET 537 ) 538 539 if count == timeout: 540 print("calibration timeout") 541 return False 542 else: 543 return True
Calibrate the sensor to the given percent value
Parameters
- calibration_precent: O2 percent for calibration, If None is passed it will use the stored calibration_value. calibration_value defaults to 20.7 on new interface boards.
Returns
boolean indication of success
545 def clear_error_flags(self) -> None: 546 """ 547 Clears error flags on the device 548 """ 549 self.write_register(HoldingRegister.CLEAR_FLAGS, 1)
Clears error flags on the device
551 def set_and_apply_heater_voltage(self, set_voltage: HeaterOptions | None) -> None: 552 """ 553 Set the current heater voltage for the sensor 554 555 :param state: State option from Enum HeaterOptions 556 :type state: HeaterOptions 557 """ 558 if set_voltage: 559 self.heater_voltage = set_voltage 560 561 # MinimalModbus will throw a NoResonseError when this register is written. 562 # It must be passed or it will crash the program 563 try: 564 self.write_register( 565 HoldingRegister.HEATER_VOLTAGE_SAVE, self.SaveAndApply.APPLY 566 ) 567 except minimalmodbus.NoResponseError: 568 sleep(2) 569 570 self.sensor_state = self.SensorState.ON
Set the current heater voltage for the sensor
Parameters
- state: State option from Enum HeaterOptions
573 def display_warnings(self) -> dict[str:bool]: 574 """ 575 Reads and decodes the warnings and returns a dictionary with each warning/error with a corresponding boolean showing error state 576 577 :return: Warning states for each of the representative bits (True is error) 578 :rtype: dict[str: bool] 579 """ 580 warning_states = { 581 "Pump Error": False, 582 "Heater Voltage Error": False, 583 "Asymmetry Warning": False, 584 "O2 Low Warning": False, 585 "Pressure Sensor Warning": False, 586 "Pressure Sensor Error": False, 587 } 588 warnings_bits = self.warnings 589 590 for count, state in enumerate(warning_states): 591 if warnings_bits[::-1][count] == '1': 592 warning_states[state] = True 593 594 return warning_states
Reads and decodes the warnings and returns a dictionary with each warning/error with a corresponding boolean showing error state
Returns
Warning states for each of the representative bits (True is error)