move to a more standard python packaging structure
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m3s
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m46s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled

This commit is contained in:
2026-04-07 17:09:10 +02:00
parent 1322fd3835
commit 6abb338c4a
43 changed files with 245 additions and 83 deletions
View File
+120
View File
@@ -0,0 +1,120 @@
import serial
import re
import operator
from functools import reduce
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='0011'
EOT = '\x04'
STX = '\x02'
ENQ = '\x05'
ETX = '\x03'
ACK = '\x06'
NAK = '\x15'
"""
Paramter read example:
Master: [EOT]0011PV[ENQ]
Instrument: [STX]PV16.4[ETX]{BCC}
Writing data:
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
"""
def checksum(message):
bcc = (reduce(operator.xor, list(map(ord,message))))
return chr(bcc)
class Eurotherm(object):
def __init__(self, serial_device, baudrate = 19200):
self.device = standard_device
# timeout: 110 ms to get all answers.
self.s = serial.Serial(serial_device,
baudrate = baudrate,
bytesize=7,
parity='E',
stopbits=1,
timeout=0.11)
self._expect_len = 50
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
def get_current_temperature(self):
temp = self.read_param('PV')
if temp is None:
temp = "0"
return temp
def set_temperature(self, temperature):
return self.write_param('SL', str(temperature))
def get_setpoint_temperature(self):
return self.read_param('SL')
if __name__ == '__main__':
import time
delta=5
date = time.strftime('%Y-%m-%d')
f = open('templog_%s'%date,'w')
f.write('# Start time: %s\n#delta t : %.1f s\n'%(time.asctime(), delta))
et = Eurotherm("/dev/ttyUSB0")
while True:
for i in range(120):
time.sleep(delta)
#t = time.strftime()
T = et.get_current_temperature()
l = '%f %s\n'%(time.time(),T)
print(time.asctime(), T)
f.write(l)
f.flush()
f.write('# MARK -- %s --\n'%(time.asctime()))
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python2
# -*- encoding: utf-8 -*-
import optparse
import os, sys
import serial
import numpy as N
import struct
import binascii
import time
DEBUG = False
crc_test = "\x0d\x01\x00\x62\x00\x33"
crc_expected = 0xddd
def crc(message):
"""
(from "Modbus_over_serial_line_V1_02.pdf" at http://www.modbus.org)
6.2.2 CRC Generation
====================
The Cyclical Redundancy Checking (CRC) field is two bytes, containing a 16bit binary value. The CRC value is calculated by the
transmitting device, which appends the CRC to the message. The device that receives recalculates a CRC during receipt of the
message, and compares the calculated value to the actual value it received in the CRC field. If the two values are not equal, an error
results.
The CRC is started by first preloading a 16bit register to all 1s. Then a process begins of applying successive 8bit bytes of the
message to the current contents of the register. Only the eight bits of data in each character are used for generating the CRC. Start
and stop bits and the parity bit, do not apply to the CRC.
During generation of the CRC, each 8bit character is exclusive ORed with the register contents. Then the result is shifted in the
direction of the least significant bit (LSB), with a zero filled into the most significant bit (MSB) position. The LSB is extracted and
examined. If the LSB was a 1, the register is then exclusive ORed with a preset, fixed value. If the LSB was a 0, no exclusive OR takes
place.
This process is repeated until eight shifts have been performed. After the last (eighth) shift, the next 8bit character is exclusive ORed
with the registers current value, and the process repeats for eight more shifts as described above. The final content of the register,
after all the characters of the message have been applied, is the CRC value.
A procedure for generating a CRC is:
1. Load a 16bit register with FFFF hex (all 1s). Call this the CRC register.
2. Exclusive OR the first 8bit byte of the message with the loworder byte of the 16bit CRC register, putting the result in the
CRC register.
3. Shift the CRC register one bit to the right (toward the LSB), zerofilling the MSB. Extract and examine the LSB.
4. (If the LSB was 0): Repeat Step 3 (another shift).
(If the LSB was 1): Exclusive OR the CRC register with the polynomial value 0xA001 (1010 0000 0000 0001).
5. Repeat Steps 3 and 4 until 8 shifts have been performed. When this is done, a complete 8bit byte will have been
processed.
6. Repeat Steps 2 through 5 for the next 8bit byte of the message. Continue doing this until all bytes have been processed.
7. The final content of the CRC register is the CRC value.
8. When the CRC is placed into the message, its upper and lower bytes must be swapped as described below.
Placing the CRC into the Message
When the 16bit CRC (two 8bit bytes) is transmitted in the message, the low-order byte will be transmitted first, followed by the high-
order byte.
"""
crc = 0xffff # step 1
for byte in message:
if type(byte) == type(str):
crc ^= ord(byte) # step 2: xor with lower byte of crc
else:
crc ^= byte # step 2: xor with lower byte of crc
for i in range(8): # step 5: repeat 2-4 until 8 shifts were performed
if (crc & 0x1) == 0: # step 4: check LSB
mask = 0x0
else:
mask = 0xa001
crc >>= 1 # step 3: shift one bit to the right
crc ^= mask
if DEBUG: print("Message:", [i for i in message], " CRC:", hex(crc))
return chr(crc & 0xff) + chr(
(crc >> 8) & 0xff) # return lower byte, upper byte (shift to the right, return last byte) i
class Flow:
def __init__(self, device="/dev/ttyUSB0"):
self.s = serial.Serial(port=device, timeout=0.02)
self.s.readline()
self.s.timeout = 0.2
self.address = bytearray([0x3])
self.rcommands = {
"flow": [bytearray([0x3, 0x0, 0x0, 0x0, 0x2]), ">f"],
"temperature": [bytearray([0x3, 0x0, 0x2, 0x0, 0x2]), ">f"],
"setflow": [bytearray([0x3, 0x0, 0x6, 0x0, 0x2]), ">f"],
"total": [bytearray([0x3, 0x0, 0x8, 0x0, 0x2]), ">f"],
"alarm": [bytearray([0x3, 0x0, 0xc, 0x0, 0x1]), ">H"],
"hwerror": [bytearray([0x3, 0x0, 0xd, 0x0, 0x1]), ">H"],
"unit_flow": [bytearray([0x3, 0x0, 0x16, 0x0, 0x4]), ">8s"],
"medium": [bytearray([0x3, 0x00, 0x1a, 0x0, 0x4]), ">8s"],
"device": [bytearray([0x3, 0x00, 0x23, 0x0, 0x4]), ">8s"],
"unit_total": [bytearray([0x3, 0x40, 0x48, 0x0, 0x4]), ">8s"],
}
self.wcommands = {
"flow": [bytearray([0x06, 0x0, 0x6]), ">f"]
}
def send_data(self, data_and_type):
"""
send data to the device at self.address.
"""
data, type = data_and_type
if DEBUG: print("send", data_and_type, "data:", binascii.hexlify(data), "type:", type, self.address)
msg = self.address + data
msg_crc = msg + crc(msg)
if DEBUG: print("send:", repr(str(msg_crc)))
nbytes_sent = self.s.write(str(msg_crc))
if DEBUG: print("send:", self.s)
if DEBUG: print("Sent bytes:", nbytes_sent)
time.sleep(0.1) # this is necessary, otherwise no data can be received
return msg_crc
def recv_data(self):
"""
Reading Data
============
Function: 0x3
Format of the resulting string
Addr Func NoBytes Byte1...N CRCHi CRCLo
"""
message = bytearray(self.s.read(3))
if DEBUG: print("recv_data: header:", binascii.hexlify(message))
if message == "":
return
addr, func, nbytes = message
if func == 0x03:
data = self.s.read(nbytes)
if DEBUG: print("recv_data: data: %i %s" % (nbytes, binascii.hexlify(data)))
message += data
crc_data = self.s.read(2)
if DEBUG: print("recv_data: crc", repr(str(crc_data)))
if crc(message) != crc_data:
print("mismatch", crc(message), crc_data)
return data
def get_var(self, named_type="flow"):
"""
get variable from device, variables are definde in self.rcommands as list [bytearray[hiAd, loAd, numHi, numLo], type]
"""
cmd,fmt = self.rcommands[named_type]
if DEBUG: print("get_var", cmd,fmt)
self.send_data([cmd,fmt])
d = self.recv_data()
if d:
d = struct.unpack(fmt, d)[0]
return d
# msg = self.recv_data()
# print str(msg)
# float struct.unpack('>f', "4byte string")
# ushort struct.unpack('>H', "2byte string")
def current_flow(self):
# start register 0x0000
# 2 bytes
cmd = self.address + bytearray([0x3, 0x0, 0x0, 0x0, 0x2])
return cmd + crc(cmd)
def current_temperature(self):
# start register 0x0002
#
#
cmd = self.address + bytearray([0x3, 0x0, 0x2, 0x0, 0x2])
return cmd + crc(cmd)
def set_flow(self, flow):
"""
write multiple registers: 0x10
startHi
startLo
numregHi
numregLo
numbytes
"""
_flow = struct.pack(">f", flow)
# cmd = address + bytearray([0x10,0x0,0x6, 0x0, 0x2, 0x2]) + _flow
cmd1 = self.address + bytearray([0x06, 0x0, 0x6]) + _flow
self.s.write(cmd1 + crc(cmd1))
time.sleep(0.01)
# print self.s.readline()
# self.recv_data()
def cmd(self, data):
cmds = self.address + data
for i in cmds:
print(hex(ord(i)), end=' ')
return cmds + crc(cmds)
if __name__ == "__main__":
"""
This is the command line program to control the red-y flow control series.
Usage: ./flow_control.py [FLOW]
Several parameters are printed on every execution
The default serial device is /dev/ttyUSB0. If you want to change that create a
file called "~/.flow" with the first line containing an alternate path, i.e. /dev/ttyUSB1
If you want to enable debug output edit the script and set DEBUG=True.
Sample output:
user@pfg # flow
Device: GSCC9SA
Medium: N2B
Current Flow: 0.00
Current Set Flow: 0.00
Flow Unit: ln/min
Total Gas: 0.00
Total Unit: lnF
Current Temperature: 22.38 C
Alarm: 0
HW Error: 0
"""
conf = os.path.expanduser('~/.flow')
if os.path.exists(conf):
dev = open(conf).readlines()[0].strip()
redy = Flow(device=dev)
print("Device: %6s" % (redy.get_var("device")))
print("Medium: %6s" % (redy.get_var("medium")))
print("Current Flow: %6.2f" % (redy.get_var("flow")))
print("Current Set Flow: %6.2f" % (redy.get_var("setflow")))
print("Flow Unit: %6s" % (redy.get_var("unit_flow")))
print("Total Gas: %6.2f" % (redy.get_var("total")))
print("Total Unit: %6s" % (redy.get_var("unit_total")))
print("Current Temperature: %6.2f C" % (redy.get_var("temperature")))
print("Alarm: %8i" % (redy.get_var("alarm")))
print("HW Error: %8i" % (redy.get_var("hwerror")))
if len(sys.argv) > 1:
try:
float(sys.argv[1])
except:
raise SyntaxError("usage: %s <flow> # to set flow or empty to get current flow\nfirst line in ~/.flow defines serial tty, default: /dev/ttyUSB0" % (sys.argv[0]))
print("Set flow: %6.2f"%(float(sys.argv[1])))
redy.set_flow(float(sys.argv[1]))
+125
View File
@@ -0,0 +1,125 @@
__author__ = 'Markus Rosenstihl <markus.rosenstihl@physik.tu-darmstadt.de>'
import re
import serial
import time
import logging
import os
logfile = os.path.expanduser("~/.goniometer.log")
logging.basicConfig(filename=logfile,level=logging.INFO)
logger = logging.getLogger()
logger.name = "goniometer"
class goniometer:
"""
This class is meant to control the goniometer probe head from Marco Braun (AXYs)
"""
def __init__(self, dev="/dev/ttyUSB0",
baudrate=38400,
timeout=1,
return_to_origin=True,
loglevel=2,
logangle=False):
"""
Upon creating an instance the goniometer will be initialised and the current angle defined as being 0.
The goniometer log can be found in `~/.goniometer.log`.
:param return_to_origin: Set to true if you want the stepper to return to the beginning when
the class instance is deleted (for example with "del" ). Default: True
:type return_to_origin: bool
:param loglevel: This sets the log level 0=DEBUG,1=INFO,2=WARNING. Default: 2
:type loglevel: int
:param logangle: Should a history of the angles be kept? The log can be accessed in the `_angle_history`
variable. Default: False
:type logangle: bool
"""
self.serial = serial.Serial(port=dev, baudrate=baudrate, timeout=timeout)
logger.setLevel([logging.DEBUG,logging.INFO,logging.WARNING][loglevel])
self.serial.readlines() # empty serial buffer
self.current_angle = 0.0
self._angle_history = []
self.initialise()
self.return_to_origin = return_to_origin
self.logangel = logangle
def __del__(self):
if self.return_to_origin:
logging.info("Return to origin")
self._send("g1")
self._wait()
self.serial.close()
def _send(self, string_to_send):
formatted_string = "%s>\r\n"%string_to_send
logging.debug("send: %s"%(repr(formatted_string)))
self.serial.write(formatted_string)
def _recv(self):
retstr = self.serial.readline().strip().replace("\x00","")
search_result = re.search(r"<(\d+)>", retstr)
if search_result != None:
angle = float(search_result.group(1))/36.0
self.current_angle = angle
if self.logangle:
self._angle_history.append(angle)
logging.debug("%.2f"%angle)
return angle
else:
return None
def _wait(self):
logging.debug("Wait until rotation finnished (current: %.2f)"%(self.current_angle))
time.sleep(4)
while True:
retstr = self._recv()
if not retstr:
logging.debug("Rotation finnished (current: %.2f)"%(self.current_angle))
break
def initialise(self):
logging.info("Initialize goniometer")
self._send("R000")
self.current_angle = 0.0
def stop(self):
"""
Stops the current movement, not possible while doing :func:`step` and :func:`angle`
"""
logging.info("Stop goniometer")
self._send("S456")
def step(self, sample_rotation_degree=1.0):
"""
How many degrees should we step further. Negative values are not allowed, the stepper would return to 0.
A ValueError exception is raised in this case.
:param sample_rotation_degree: How many degrees to rotate. Default: 1
:type sample_rotation_degree: float
"""
if sample_rotation_degree <= 0:
raise ValueError("This does not what you expect, would move to the start position whatever the value")
logging.info("Step goniometer: %.2f"%sample_rotation_degree)
direction = "g" if (sample_rotation_degree < 0) else "G"
sample_rotation_degree = -1*sample_rotation_degree if sample_rotation_degree < 0 else sample_rotation_degree
steps = int(sample_rotation_degree%360)
self._send("%s%i"%(direction, steps ))
self._wait()
def angle(self, angle_degree):
"""
To what angle, in degrees, should we rotate. Mod 360 is calculated for `angle_degree`.
If angle is smaller than current angle an exception will be raised.
(From the underlying :func:`step` function)
TODO: rotate either back or over to the wanted position.
:param angle_degree: position in degree
:type angle_degree: float
"""
angle_degree %= 360
delta_degree = angle_degree - self.current_angle
logging.info("Rotate tp %.2f (from %.2f, i.e. delta=%.2f)"%(angle_degree, self.current_angle, delta_degree))
self.step(sample_rotation_degree=delta_degree)
+127
View File
@@ -0,0 +1,127 @@
import numpy as N
import sys
if sys.version_info > (2,6,0):
import numbers
else:
pass
if sys.version_info > (2,6,0):
def lin_range(start,stop,step):
if isinstance(step, numbers.Integral):
return N.linspace(start,stop,step)
else:
return N.arange(start,stop,step)
else:
def lin_range(start,stop,step):
return N.arange(start,stop,step)
def log_range(start, stop, stepno):
if (start<=0 or stop<=0 or stepno<1):
raise ValueError("start, stop must be positive and stepno must be >=1")
return N.logspace(N.log10(start),N.log10(stop), num=stepno)
def staggered_range(some_range, size=3):
m=0
if isinstance(some_range, N.ndarray):
is_numpy = True
some_range = list(some_range)
else:
is_numpy = False
new_list=[]
for k in range(len(some_range)):
for i in range(size):
try:
index = (m*size)
new_list.append(some_range.pop(index))
except IndexError:
break
m+=1
if is_numpy:
new_list = N.asarray(new_list+some_range)
else:
new_list+=some_range
return new_list
def combine_ranges(*ranges):
new_list = []
for r in ranges:
new_list.extend(r)
return new_list
combined_ranges=combine_ranges
def interleaved_range(some_list, left_out):
"""
in first run, do every n-th, then do n-1-th of the remaining values and so on...
"""
m=0
new_list = []
for j in range(left_out):
for i in range(len(some_list)):
if (i*left_out+m) < len(some_list):
new_list.append(some_list[i*left_out+m])
else:
m+=1
break
if isinstance(some_list, N.ndarray):
new_list = N.array(new_list)
return new_list
# These are the generators
def lin_range_iter(start,stop, step):
this_one=float(start)+0.0
if step>0:
while (this_one<=float(stop)):
yield this_one
this_one+=float(step)
else:
while (this_one>=float(stop)):
yield this_one
this_one+=float(step)
def log_range_iter(start, stop, stepno):
if (start<=0 or stop<=0 or stepno<1):
raise ValueError("start, stop must be positive and stepno must be >=1")
if int(stepno)==1:
factor=1.0
else:
factor=(stop/start)**(1.0/int(stepno-1))
for i in range(int(stepno)):
yield start*(factor**i)
def staggered_range_iter(some_range, size = 1):
"""
size=1: do one, drop one, ....
size=n: do 1 ... n, drop n+1 ... 2*n
in a second run the dropped values were done
"""
left_out=[]
try:
while True:
for i in range(size):
yield next(some_range)
for i in range(size):
left_out.append(next(some_range))
except StopIteration:
pass
# now do the droped ones
for i in left_out:
yield i
def combined_ranges_iter(*ranges):
"""
iterate over one range after the other
"""
for r in ranges:
for i in r:
yield i
combine_ranges_iter=combined_ranges_iter
+30
View File
@@ -0,0 +1,30 @@
import math
__all__ = ['rotate_signal']
def rotate_signal(timesignal, angle):
"Rotate <timesignal> by <angle> degrees"
# implicit change to float arrays!
if timesignal.get_number_of_channels()!=2:
raise Exception("rotation defined only for 2 channels")
# simple case 0, 90, 180, 270 degree
reduced_angle=divmod(angle, 90)
if abs(reduced_angle[1])<1e-6:
reduced_angle=reduced_angle[0]%4
if reduced_angle==0:
return
elif reduced_angle==1:
timesignal.y[1]*=-1
timesignal.y=[timesignal.y[1],timesignal.y[0]]
elif reduced_angle==2:
timesignal.y[0]*=-1
timesignal.y[1]*=-1
elif reduced_angle==3:
timesignal.y[0]*=-1
timesignal.y=[timesignal.y[1],timesignal.y[0]]
else:
sin_angle=math.sin(angle/180.0*math.pi)
cos_angle=math.cos(angle/180.0*math.pi)
timesignal.y=[cos_angle*timesignal.y[0]-sin_angle*timesignal.y[1],
sin_angle*timesignal.y[0]+cos_angle*timesignal.y[1]]