From d9f2cc3f2c17c933896d1e4f78ced20021a6d77c Mon Sep 17 00:00:00 2001 From: Markus Rosenstihl Date: Sun, 12 Jul 2026 20:00:39 +0200 Subject: [PATCH 1/3] added lakeshore temperature controller --- src/damaris/tools/lakeshore.py | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/damaris/tools/lakeshore.py diff --git a/src/damaris/tools/lakeshore.py b/src/damaris/tools/lakeshore.py new file mode 100644 index 0000000..0b0c8b0 --- /dev/null +++ b/src/damaris/tools/lakeshore.py @@ -0,0 +1,108 @@ +import serial +import re +import operator +import numpy as np + + +DEBUG = False + +reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL) +# example answer '\x02PV279.8\x03/' +# [EOT] = \x04 +# [STX] = \x02 +# [ENQ] = \x05 +# [ETX] = \x03 +# [ACK] = \x06 +# BCC = checksum + +standard_device = '/dev/ttyUSB0' +#EOT = '\x04' +#STX = '\x02' +#ENQ = '\x05' +#ETX = '\x03' +#ACK = '\x06' +#NAK = '\x15' + + +""" +Parameter read example: + +Master: [EOT]0011PV[ENQ] +Instrument: [STX]PV16.4[ETX]{BCC} + +Writing data: + +Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)[ETX](BCC) + +""" + + + +class Lakeshore(object): + def __init__(self, serial_device, baudrate=57600): + self.device = standard_device + # timeout: 110 ms to get all answers. + self.s = serial.Serial(serial_device, + baudrate=baudrate, + bytesize=7, + parity='O', + stopbits=1, + timeout=0.4) + self._expect_len = 50 + + def __del__(self): + self.s.close() + + #def get_current_temperature(self): # Temp A + # self.s.write("KRDG? a\n") + # a = self.s.readline().strip()[1:] + # return float(a) + + def get_temperature_A(self): # Temp A + self.s.write("KRDG? a\n") + a = self.s.readline().strip()[1:] + return float(a) + + def get_temperature_B(self): # Temp B + self.s.write("KRDG? b\n") + b = self.s.readline().strip()[1:] + return float(b) + + def set_temperature(self, temperature): # setze Setpoint + t = temperature + self.s.write(f"SETP 1,{t}\n") + + def set_ramp(self, ramp): + return self.s.write(f'RAMP 1, 1, {ramp}\n') + + def get_set_temperature(self): # Setpoint abfrage + self.s.write("SETP? 1\n") + s = self.s.readline().strip()[1:] + return float(s) + + def get_output(self): # Heater Output + self.s.write("HTR? 1\n") + o = self.s.readline().strip()[1:] + return float(o) + + def get_ramp(self): + self.s.write('RAMP? 1\n') + ret = self.s.readline().strip() + onoff, rate = ret.split(',') + if onoff: + return float(rate) + else: + return 0.0 + + def get_value(self, name): + temp = self.read_param(name) + if temp is None: + return np.nan + else: + try: + return float(temp) + except ValueError: + return np.nan + + def close(self): + self.s.close() \ No newline at end of file From 6f0b090d9dbb46c67be26f0b56c037c09b0946e0 Mon Sep 17 00:00:00 2001 From: Markus Rosenstihl Date: Sun, 12 Jul 2026 20:09:07 +0200 Subject: [PATCH 2/3] added lakeshore to temperature logging facility --- pyproject.toml | 1 + src/damaris/tools/lakeshore.py | 103 +++++++++++++++++++++++++++---- src/damaris/tools/temperature.py | 8 +++ 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2568eb3..d357af0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "matplotlib >= 1.4.1, <= 3.6", "pyxdg", "PyGObject >= 3.14.0, <= 3.51.0", + "pyserial>=3.5", ] authors = [ {name = "Achim Gädke"}, diff --git a/src/damaris/tools/lakeshore.py b/src/damaris/tools/lakeshore.py index 0b0c8b0..09a68b2 100644 --- a/src/damaris/tools/lakeshore.py +++ b/src/damaris/tools/lakeshore.py @@ -2,6 +2,9 @@ import serial import re import operator import numpy as np +from functools import reduce + +from .temperature import TemperatureController, register_controller DEBUG = False @@ -15,20 +18,25 @@ reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL) # [ACK] = \x06 # BCC = checksum -standard_device = '/dev/ttyUSB0' -#EOT = '\x04' -#STX = '\x02' -#ENQ = '\x05' -#ETX = '\x03' -#ACK = '\x06' -#NAK = '\x15' +standard_device = '0011' +EOT = '\x04' +STX = '\x02' +ENQ = '\x05' +ETX = '\x03' +ACK = '\x06' +NAK = '\x15' + + +def checksum(message): + bcc = (reduce(operator.xor, list(map(ord,message)))) + return chr(bcc) """ Parameter read example: Master: [EOT]0011PV[ENQ] -Instrument: [STX]PV16.4[ETX]{BCC} +Instrument: [STX]PV16.4[ETX]{BCC} Writing data: @@ -38,7 +46,7 @@ Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)[ETX](BCC) -class Lakeshore(object): +class Lakeshore(TemperatureController): def __init__(self, serial_device, baudrate=57600): self.device = standard_device # timeout: 110 ms to get all answers. @@ -51,7 +59,9 @@ class Lakeshore(object): self._expect_len = 50 def __del__(self): - self.s.close() + if hasattr(self, 's') and self.s is not None: + if self.s.is_open: + self.s.close() #def get_current_temperature(self): # Temp A # self.s.write("KRDG? a\n") @@ -104,5 +114,76 @@ class Lakeshore(object): except ValueError: return np.nan + def send_read_param(self, param): + self.s.write(EOT + self.device + param + ENQ) + + def read_param(self, param): + self.s.flushInput() + self.send_read_param(param) + answer = self.s.read(self._expect_len) + m = reply_pattern.search(answer) + if m is None: + # Reading _expect_len bytes was not enough... + answer += self.s.read(200) + m = reply_pattern.search(answer) + if m is not None: + self._expect_len = len(answer) + return m.group(1) + else: + print("received:", repr(answer)) + return None + + def write_param(self, mnemonic, data): + if len(mnemonic) > 2: + raise ValueError + bcc = checksum(mnemonic + data + ETX) + mes = EOT+self.device+STX+mnemonic+data+ETX+bcc + if DEBUG: + for i in mes: + print(i,hex(ord(i))) + self.s.flushInput() + self.s.write(mes) + answer = self.s.read(1) + # print "received:", repr(answer) + if answer == "": + # raise IOError("No answer from device") + return None + return answer[-1] == ACK + + # TemperatureController interface implementation + + def get_temperature(self) -> float: + """Read current temperature in Celsius.""" + temp = self.get_temperature_A() + if temp is None: + return 0.0 + try: + return float(temp) + except (ValueError, TypeError): + return 0.0 + + def get_setpoint(self) -> float: + """Read target setpoint temperature in Celsius.""" + temp = self.get_set_temperature() + if temp is None: + return 0.0 + try: + return float(temp) + except (ValueError, TypeError): + return 0.0 + + def set_setpoint(self, temperature: float) -> bool: + """Set target temperature in Celsius.""" + self.set_temperature(temperature) + return True + def close(self): - self.s.close() \ No newline at end of file + """Close the serial connection.""" + if hasattr(self, 's') and self.s is not None: + if self.s.is_open: + self.s.close() + self.s = None + + +# Register Lakeshore with the factory +register_controller('lakeshore', Lakeshore) \ No newline at end of file diff --git a/src/damaris/tools/temperature.py b/src/damaris/tools/temperature.py index 5ab04b8..a50c01d 100644 --- a/src/damaris/tools/temperature.py +++ b/src/damaris/tools/temperature.py @@ -44,6 +44,7 @@ def create_controller(name: str, *args, **kwargs): module_map = { 'simulated': 'damaris.tools.temperature_simulator', 'eurotherm': 'damaris.tools.eurotherm', + 'lakeshore': 'damaris.tools.lakeshore', } if name in module_map: try: @@ -79,6 +80,13 @@ except ImportError as e: # That's OK, it will be imported on-demand if needed pass +try: + from . import lakeshore +except ImportError as e: + # Lakeshore may require serial module which might not be installed + # That's OK, it will be imported on-demand if needed + pass + try: from . import temperature_simulator except ImportError as e: From 75863ffc1970be1f270fb4b4f5d68aaa618ce34f Mon Sep 17 00:00:00 2001 From: Markus Rosenstihl Date: Sun, 12 Jul 2026 20:15:50 +0200 Subject: [PATCH 3/3] fixed lakeshore to keep using the simple ASCII commands --- src/damaris/tools/lakeshore.py | 66 +++++++--------------------------- 1 file changed, 13 insertions(+), 53 deletions(-) diff --git a/src/damaris/tools/lakeshore.py b/src/damaris/tools/lakeshore.py index 09a68b2..561947d 100644 --- a/src/damaris/tools/lakeshore.py +++ b/src/damaris/tools/lakeshore.py @@ -1,8 +1,6 @@ import serial import re -import operator import numpy as np -from functools import reduce from .temperature import TemperatureController, register_controller @@ -18,18 +16,7 @@ reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL) # [ACK] = \x06 # BCC = checksum -standard_device = '0011' -EOT = '\x04' -STX = '\x02' -ENQ = '\x05' -ETX = '\x03' -ACK = '\x06' -NAK = '\x15' - - -def checksum(message): - bcc = (reduce(operator.xor, list(map(ord,message)))) - return chr(bcc) +standard_device = '/dev/ttyUSB0' """ @@ -104,6 +91,15 @@ class Lakeshore(TemperatureController): else: return 0.0 + def read_param(self, param): + """Read a parameter using simple ASCII commands.""" + self.s.write(f"{param}?\n") + answer = self.s.readline().strip() + if answer and len(answer) > 1: + # Remove the first character (command echo) and return the value + return answer[1:] + return None + def get_value(self, name): temp = self.read_param(name) if temp is None: @@ -114,47 +110,11 @@ class Lakeshore(TemperatureController): except ValueError: return np.nan - def send_read_param(self, param): - self.s.write(EOT + self.device + param + ENQ) - - def read_param(self, param): - self.s.flushInput() - self.send_read_param(param) - answer = self.s.read(self._expect_len) - m = reply_pattern.search(answer) - if m is None: - # Reading _expect_len bytes was not enough... - answer += self.s.read(200) - m = reply_pattern.search(answer) - if m is not None: - self._expect_len = len(answer) - return m.group(1) - else: - print("received:", repr(answer)) - return None - - def write_param(self, mnemonic, data): - if len(mnemonic) > 2: - raise ValueError - bcc = checksum(mnemonic + data + ETX) - mes = EOT+self.device+STX+mnemonic+data+ETX+bcc - if DEBUG: - for i in mes: - print(i,hex(ord(i))) - self.s.flushInput() - self.s.write(mes) - answer = self.s.read(1) - # print "received:", repr(answer) - if answer == "": - # raise IOError("No answer from device") - return None - return answer[-1] == ACK - # TemperatureController interface implementation def get_temperature(self) -> float: """Read current temperature in Celsius.""" - temp = self.get_temperature_A() + temp = self.read_param('KRDG') if temp is None: return 0.0 try: @@ -164,7 +124,7 @@ class Lakeshore(TemperatureController): def get_setpoint(self) -> float: """Read target setpoint temperature in Celsius.""" - temp = self.get_set_temperature() + temp = self.read_param('SETP') if temp is None: return 0.0 try: @@ -174,7 +134,7 @@ class Lakeshore(TemperatureController): def set_setpoint(self, temperature: float) -> bool: """Set target temperature in Celsius.""" - self.set_temperature(temperature) + self.s.write(f"SETP {temperature}\n") return True def close(self):