Skip to content

Commit

Permalink
Merge pull request #135 from SiLab-Bonn/enhance_temperature
Browse files Browse the repository at this point in the history
Enhance temperature control and measurement
  • Loading branch information
Michael Daas committed Nov 6, 2020
2 parents d228fa0 + 1c0a2d5 commit 968842a
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 88 deletions.
135 changes: 74 additions & 61 deletions basil/HL/sensirion_ekh4.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,89 +16,102 @@ class sensirionEKH4(HardwareLayer):
'''Driver for the Sensirion EK-H4 multiplexer box. Can be used to read up to 4 channels of sensirion sensors for humidity and temperature
(http://www.sensirion.com/en/products/humidity-temperature/evaluation-kits/ek-h4/).
A TLV protocoll via serial port is used with 115200 baud rate. The type byte definitions cannot be found online...
The data returned by the device is often too long, especially for the humidity read out. Still it is interpreted.
But to avoid unreasonable values a max_value can be set (e.g. rel. humidity < 100). If this values is exceeded None is set for that channel.
The data returned by the device is often too long. Different cases are handled and not reconsturctable channels are set to None.
'''

def __init__(self, intf, conf):
super(sensirionEKH4, self).__init__(intf, conf)

def init(self):
super(sensirionEKH4, self).init()
self.read() # clear trash
# set readout every second
self.ask(r"7e230200013102010c25010e2601033a7e")
self._write(r"7e230200013102010c25010e2601033a7e") # set update interval to 1Hz
self._intf.read(size=1024) # clear buffer

def write(self, command):
def _write(self, command):
self._intf.write(binascii.a2b_hex(command))

def ask(self, command):
'''Read response to command and convert it to 16-bit integer.
Returns : list of values
'''
self._intf.read(size=1024) # Make sure that buffer is cleared
self.write(command)
def _read(self):
word = ''
data_flg = 0
for _ in range(1024):
b = codecs.encode(self._intf.read(size=1), 'hex_codec').decode('utf-8')

if b == '7e': # delimiter
if data_flg == 0: # data word comes next
data_flg = 1
else: # data word finished
break
elif data_flg == 1:
word += b

return word

def _query(self, command):
self._write(command)
time.sleep(0.1)
return self.read()

def read(self):
answer = []
flg = 0
for _ in range(1024): # data assumed to be less than 1024 words
a = codecs.encode(self._intf.read(size=1), 'hex_codec').decode('utf-8')
if a == '':
break
elif flg == 0 and a == '7e':
flg = 1
elif flg == 1 and a == '7e':
break
elif flg == 1:
answer.append(a)
return answer

def get_temperature(self, channel=None, min_val=-40, max_val=200):
values = []
ret = self.ask(r"7e4700b87e")
for j in range(4):
if ret[2 + 2 * j] == "7f" and ret[2 + 2 * j + 1] == "ff":
values.append(None)
else:
values.append(self.cal_ret(ret[2 + 2 * j] + ret[2 + 2 * j + 1]))
return self._read()

def _calc_value(self, value):
'''
Calculate two's complement of signed binary, if applicable.
'''
bits = 16
value = int(value, 16)
if (value & (1 << (bits - 1))) != 0: # if sign bit is set, e.g., 8bit: 128-255
value = value - (1 << bits) # compute negative value
return float(value) / 100.0

def _get_values(self, cmd):
'''
Retrieve raw values via self._query and do error correction if possible
First 4 characters of data word are unknown ("4608", "4708" or "4808").
Last two characters seem to be some kind of checksum?
In between there should be 16 characters, 4 per channel. Sometimes it's more.
Two cases can be distinguished here:
1. Some kind of common error code "7d31" plus an additional byte.
In this case, the affected channel is set to None,
the extra byte is removed and the other channels are interpreted as usual.
2. Varying amount of extra bytes with no recognizable error code.
In this case, nothing can be reconstructed and all channels are set to None.
Case 1 is reproducable for temperatures between about 43.53C and 46.1C...?
'''
ret = self._query(cmd)[4:-2]
# Cut off extra bytes in case of error
for i in range(0, 16, 4):
if ret[i:i + 4] == '7d31':
ret = ret[:i + 4] + ret[i + 6:]

if len(ret) != 16: # wrong number of bytes despite attempted error correction
values = [None, None, None, None]
else:
values = []
data = [ret[j:j + 4] for j in range(0, len(ret), 4)]
for i, d in enumerate(data):
if d == '7d31' or d == '7fff': # Error or no sensor connected
values.append(None)
else:
values.append(self._calc_value(d))

return values

def get_temperature(self, channel=None):
values = self._get_values(r"7e4700b87e")

if channel is None:
return values
return values[channel]

def get_humidity(self, channel=None, min_val=0, max_val=100):
values = []
ret = self.ask(r"7e4600b97e")
for j in range(4):
if ret[2 + 2 * j] == "7f" and ret[2 + 2 * j + 1] == "ff":
values.append(None)
else:
values.append(self.cal_ret(ret[2 + 2 * j] + ret[2 + 2 * j + 1]))
def get_humidity(self, channel=None):
values = self._get_values(r"7e4600b97e")

if channel is None:
return values
return values[channel]

def get_dew_point(self, channel=None, min_val=-40, max_val=100):
values = []
ret = self.ask(r"7e4800b77e")
for j in range(4):
if ret[2 + 2 * j] == "7f" and ret[2 + 2 * j + 1] == "ff":
values.append(None)
else:
values.append(self.cal_ret(ret[2 + 2 * j] + ret[2 + 2 * j + 1]))
def get_dew_point(self, channel=None):
values = self._get_values(r"7e4800b77e")

if channel is None:
return values
return values[channel]

def cal_ret(self, value):
bits = 16
value = int(value, 16)
# compute the 2's compliment of int value
if (value & (1 << (bits - 1))) != 0: # if sign bit is set, e.g., 8bit: 128-255
value = value - (1 << bits) # compute negative value
return float(value) / 100.0 # return positive value as is
89 changes: 82 additions & 7 deletions basil/HL/weiss_labevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,41 @@

from basil.HL.RegisterHardwareLayer import HardwareLayer

logger = logging.getLogger(__name__)


class weissLabEvent(HardwareLayer):
'''
Driver for Weiss LabEvent T/210/70/5 climate chamber.
Driver for Weiss LabEvent T/210/70/5 climate chamber. Commands extracted from
https://github.com/IzaakWN/ClimateChamberMonitor/blob/master/chamber_commands.py
'''

CHAMBER_TYPE = 'LabEvent T/210/70/5'

RETURN_CODES = {
1: "Command is accepted and executed.",
-5: "Command number transmitted is unidentified!",
-6: "Too few or incorrect parameters entered!",
-8: "Data could not be read!",
}

STATUS_CODES = {
1: 'Test not running (Idle)',
3: 'Test running',
4: 'Warnings present',
8: 'Alarms present'
}

def __init__(self, intf, conf):
super(weissLabEvent, self).__init__(intf, conf)

def init(self):
super(weissLabEvent, self).init()

info = self.get_info()
if info != self.CHAMBER_TYPE:
raise ValueError("Not the expected climatechamber! Expected '{0}', chamber reported '{1}'.".format(self.CHAMBER_TYPE, info))

def query(self, cmd):
ret = self._intf.query(cmd, buffer_size=512)[0]
dat = [d.decode('ascii') for d in ret.split(b'\xb6')]
Expand All @@ -37,18 +53,77 @@ def query(self, cmd):
data = dat[1]
except IndexError:
data = None
logging.debug('Return code {0}: {1}'.format(code, self.RETURN_CODES[code]))
logger.debug('Return code {0}: {1}'.format(code, self.RETURN_CODES[code]))
if code != 1:
logging.error('Return code {0}: {1}'.format(code, self.RETURN_CODES[code]))
logger.error('Return code {0}: {1}'.format(code, self.RETURN_CODES[code]))

return code, data

def get_temperature(self):
return float(self.query(b'11004\xb61\xb61')[1])
def _get_feature_status(self, id):
'''
Installed features:
1 - Condensation protection
2 - Not installed
3 - Not installed
4 - Compressed air / N2
5 - Air Dryer
6 - Not installed
'''

if id not in range(1, 7):
raise ValueError('Invalid feature id!')

feature_name = self.query(b'14010\xb61\xb6' + str(id).encode('ascii'))[1]
feature_status = self.query(b'14003\xb61\xb6' + str(id + 1).encode('ascii'))[1] # For get and set status, id = id + 1
logger.debug('Feature {0} has status {1}'.format(feature_name, feature_status))
return bool(int(feature_status))

def _set_feature_status(self, id, value):
return self.query(b'14001\xb61\xb6' + str(id + 1).encode('ascii') + b'\xb6' + str(int(value)).encode('ascii')) # For get and set status, id = id + 1

def get_info(self):
return self.query(b'99997\xb61\xb61')[1]

def get_status(self):
status_code = int(self.query(b'10012\xb61\xb61')[1])
status = '{0}: {1}'.format(status_code, self.STATUS_CODES[status_code])
return status

def start_manual_mode(self):
if not self.query(b'14001\xb61\xb61\xb61')[0] == 1:
logging.error('Could not start manual mode!')
logger.error('Could not start manual mode!')

def stop_manual_mode(self):
if not self.query(b'14001\xb61\xb61\xb60')[0] == 1:
logger.error('Could not stop manual mode!')

def get_temperature(self):
return float(self.query(b'11004\xb61\xb61')[1])

def set_temperature(self, target):
self.query(b'11001\xb61\xb61\xb6' + str(target).encode('ascii'))
if not self.query(b'11001\xb61\xb61\xb6' + str(target).encode('ascii'))[0] == 1:
logger.error('Could not set temperature!')

def get_temperature_setpoint(self):
return float(self.query(b'11002\xb61\xb61')[1])

def get_condensation_protection(self):
return self._get_feature_status(1)

def set_condensation_protection(self, value):
if not self._set_feature_status(1, value)[0] == 1:
logger.error('Could not set condensation protection!')

def get_compressed_air(self):
return self._get_feature_status(4)

def set_compressed_air(self, value):
if not self._set_feature_status(4, value)[0] == 1:
logger.error('Could not set compressed air / N2!')

def get_air_dryer(self):
return self._get_feature_status(5)

def set_air_dryer(self, value):
if not self._set_feature_status(5, value)[0] == 1:
logger.error('Could not set air dryer!')
26 changes: 6 additions & 20 deletions examples/lab_devices/temperature_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@
# ------------------------------------------------------------
#

''' This script shows how to read temperature/humidity with Sensition sensors and how to set temperature using a Binder MK 53 climate chamber.
The Sensition sensors are read using the Sensirion EK-H4 multiplexer box from the evaluation kit. A serial TL has to be used
(http://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/Humidity/Sensirion_Humidity_EK-H4_Datasheet_V3.pdf).
For the communication with the Binder MK 53 also a serial TL has to be used (http://www.binder-world.com).
'''
This script shows how to set temperature using a Binder MK 53 climate chamber.
For the communication with the Binder MK 53 also a serial TL has to be used (http://www.binder-world.com).
The newer Weiss Labevent climatechamber uses a direct TCP/IP connection.
'''


from basil.dut import Dut

# Sensirion sensor readout
dut = Dut('sensirionEKH4_pyserial.yaml')
dut.init()
print(dut['Thermohygrometer'].get_temperature())
print(dut['Thermohygrometer'].get_humidity())
print(dut['Thermohygrometer'].get_dew_point())

# Binder MK 53 control
dut = Dut('binderMK53_pyserial.yaml')
dut.init()
Expand All @@ -30,17 +23,10 @@
temperature_target = dut['Climatechamber'].get_temperature_target()
dut['Climatechamber'].set_temperature(temperature_target)

# Weiss SB 22 control
dut = Dut('WeissSB22_pyserial.yaml')
dut.init()
print(dut['Climatechamber'].get_temperature())
print(dut['Climatechamber'].get_digital_ch())
temperature_target = dut['Climatechamber'].get_temperature_target()
dut['Climatechamber'].set_temperature(temperature_target)

# New Weiss Labevent control
dut = Dut('WeissLabEvent_socket.yaml')
dut.init()
dut['Climatechamber'].start_manual_mode()
dut['Climatechamber'].set_temperature(-10)
dut['Climatechamber'].set_temperature(20)
dut['Climatechamber'].set_air_dryer(True) # Make sure the air dryer is turned on
print(dut['Climatechamber'].get_temperature())
22 changes: 22 additions & 0 deletions examples/lab_devices/temperature_sensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# ------------------------------------------------------------
# Copyright (c) All rights reserved
# SiLab, Institute of Physics, University of Bonn
# ------------------------------------------------------------
#

'''
This script shows how to read temperature/humidity with Sensition sensors.
The Sensition sensors are read using the Sensirion EK-H4 multiplexer box from the evaluation kit. A serial TL has to be used
(http://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/Humidity/Sensirion_Humidity_EK-H4_Datasheet_V3.pdf).
'''


from basil.dut import Dut

# Sensirion sensor readout
dut = Dut('sensirionEKH4_pyserial.yaml')
dut.init()
print(dut['Thermohygrometer'].get_temperature())
print(dut['Thermohygrometer'].get_humidity())
print(dut['Thermohygrometer'].get_dew_point())

0 comments on commit 968842a

Please sign in to comment.