diff --git a/bin/stringio-server b/bin/stringio-server new file mode 100755 index 0000000..3b7d9c2 --- /dev/null +++ b/bin/stringio-server @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# pylint: disable=invalid-name +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""server for a string communicator + +Usage: + + bin/stringio-server + +open a server on to communicate with the string based over TCP/IP. + +Use cases, mainly for test purposes: +- as a T, if the hardware allows only one connection, and more than one is needed +- relay to a communicator not using TCP/IP, if Frappy should run on an other host +- relay to a hardware simulation written as a communicator +""" + +import sys +from os import path +import asyncore +import socket + +# Add import path for inplace usage +sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..'))) + +from secop.lib import get_class, formatException + +class LineHandler(asyncore.dispatcher_with_send): + + def __init__(self, sock): + self.buffer = b"" + asyncore.dispatcher_with_send.__init__(self, sock) + self.crlf = 0 + + def handle_read(self): + data = self.recv(8192) + if data: + parts = data.split(b"\n") + if len(parts) == 1: + self.buffer += data + else: + self.handle_line((self.buffer + parts[0]).decode('latin_1')) + for part in parts[1:-1]: + if part[-1] == b"\r": + self.crlf = True + part = part[:-1] + else: + self.crlf = False + self.handle_line(part.decode('latin_1')) + self.buffer = parts[-1] + + def send_line(self, line): + self.send((line + ("\r\n" if self.crlf else "\n")).encode('latin_1')) + + +class LineServer(asyncore.dispatcher): + + def __init__(self, host, port, lineHandlerClass): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((host, port)) + self.listen(5) + self.lineHandlerClass = lineHandlerClass + + def handle_accept(self): + pair = self.accept() + if pair is not None: + sock, addr = pair + print ("Incoming connection from %s" % repr(addr)) + self.lineHandlerClass(sock) + + def loop(self): + asyncore.loop() + + +class Server(LineServer): + + class Dispatcher: + def announce_update(self, *_): + pass + + def announce_update_error(self, *_): + pass + + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + self.dispatcher = self.Dispatcher() + + +class Handler(LineHandler): + def handle_line(self, line): + try: + reply = module.do_communicate(line.strip()) + print('%-40s | %s' % (line, reply)) + except Exception: + print(formatException(verbose=True)) + self.send_line(reply) + + + + +class Logger: + def debug(self, *args): + print(*args) + info = exception = debug + + +if len(sys.argv) < 2: + sys.argv.append('secop_psi.ls370sim.Ls370Sim') +if len(sys.argv) < 3: + sys.argv.append('4567') +communicatorname = sys.argv[1] +serverport = int(sys.argv[2]) +opts = {'.description':'simulator'} +for arg in sys.argv[3:]: + k, v = arg.split('=',1) + opts[k] = v + +srv = Server('localhost', serverport, Handler) +module = get_class(communicatorname)(communicatorname, Logger(), opts, srv) +module.earlyInit() +srv.loop() diff --git a/cfg/ls370sim.cfg b/cfg/ls370sim.cfg new file mode 100644 index 0000000..66ce0ca --- /dev/null +++ b/cfg/ls370sim.cfg @@ -0,0 +1,24 @@ +[node LscSIM.psi.ch] +description = Lsc Simulation at PSI + +[interface tcp] +type = tcp +bindto = 0.0.0.0 +bindport = 5000 + +[module res] +class = secop_psi.ls370res.ResChannel +.channel = 3 +.description = resistivity +.main = lsmain +.iodev = lscom + +[module lsmain] +class = secop_psi.ls370res.Main +.description = main control of Lsc controller +.iodev = lscom + +[module lscom] +class = secop_psi.ls370sim.Ls370Sim +.description = simulated serial communicator to a LS 370 +.visibility = 3 diff --git a/cfg/ls370test.cfg b/cfg/ls370test.cfg new file mode 100644 index 0000000..1004f36 --- /dev/null +++ b/cfg/ls370test.cfg @@ -0,0 +1,26 @@ +[node LscSIM.psi.ch] +description = Lsc370 Test + +[interface tcp] +type = tcp +bindto = 0.0.0.0 +bindport = 5000 + +[module res] +class = secop_psi.ls370res.ResChannel +vexc = '2mV' +.channel = 3 +.description = resistivity +.main = lsmain +.iodev = lscom + +[module lsmain] +class = secop_psi.ls370res.Main +.description = main control of Lsc controller +.iodev = lscom + +[module lscom] +class = secop_psi.ls370res.StringIO +.uri=localhost:4567 +.description = serial communicator to an LS 370 +.visibility = 3 diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py new file mode 100644 index 0000000..9a124f8 --- /dev/null +++ b/secop_psi/ls370res.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""LakeShore Model 370 resistance channel""" + +import time + +from secop.modules import Module, Readable, Drivable, Parameter, Override, Property, Attached +from secop.metaclass import Done +from secop.datatypes import FloatRange, IntRange, EnumType, BoolType +from secop.stringio import HasIodev +from secop.poller import Poller, REGULAR +import secop.commandhandler + +Status = Drivable.Status + + +class CmdHandler(secop.commandhandler.CmdHandler): + CMDARGS = ['channel'] + CMDSEPARATOR = ';' + +rdgrng = CmdHandler('rdgrng', 'RDGRNG?%(channel)d', '%d,%d,%d,%d,%d') +inset = CmdHandler('inset', 'INSET?%(channel)d', '%d,%d,%d,%d,%d') +filterhdl = CmdHandler('filt', 'FILTER?%(channel)d', '%d,%d,%d') +scan = CmdHandler('scan', 'SCAN?', '%d,%d') + + +STATUS_TEXT = {0: ''} +for bit, text in enumerate('CS_OVL VCM_OVL VMIX_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()): + for i in range(1 << bit, 2 << bit): + STATUS_TEXT[i] = text + + +class StringIO(secop.stringio.StringIO): + identification = [('*IDN?', 'LSCI,MODEL370,.*')] + + +class Main(HasIodev, Module): + parameters = { + 'channel': + Parameter('the current channel', poll=REGULAR, datatype=IntRange(), readonly=False, handler=scan), + 'autoscan': + Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, handler=scan), + 'pollinterval': Parameter('sleeptime between polls', default=5, + readonly=False, + datatype=FloatRange(0.1, 120), + ), + } + + pollerClass = Poller + + def analyze_scan(self, channel, autoscan): + self.channel, self.autoscan = channel, autoscan + + def change_scan(self, new, *args): + return new.channel, new.autoscan + + +class ResChannel(HasIodev, Readable): + '''temperature channel on Lakeshore 336''' + + RES_RANGE = {key: i+1 for i, key in list( + enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm'] + for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]} + RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7,16)] # RES_SCALE[0] is not used + CUR_RANGE = {key: i + 1 for i, key in list( + enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA'] + for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]} + VOLT_RANGE = {key: i + 1 for i, key in list( + enumerate(mag % val for mag in ['%guV', '%gmV'] + for val in [2, 6.32, 20, 63.2, 200, 632]))} + + pollerClass = Poller + + properties = { + 'channel': + Property('the Lakeshore channel', datatype=IntRange(), export=False), + 'main': + Attached() + } + + parameters = { + 'value': + Override(datatype=FloatRange(unit='Ohm')), + 'pollinterval': + Override(visibility=3), + 'range': + Parameter('reading range', readonly=False, + datatype=EnumType(**RES_RANGE), handler=rdgrng), + 'minrange': + Parameter('minimum range for software autorange', readonly=False, default=1, + datatype=EnumType(**RES_RANGE)), + 'autorange': + Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2), + readonly=False, handler=rdgrng, default=2), + 'iexc': + Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng), + 'vexc': + Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng), + 'enable': + Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset), + 'pause': + Parameter('pause after channel change', datatype=IntRange(), readonly=False, handler=inset), + 'dwell': + Parameter('dwell time with autoscan', datatype=IntRange(), readonly=False, handler=inset), + 'filter': + Parameter('filter time', datatype=IntRange(), readonly=False, handler=filterhdl), + } + + def startModule(self, started_callback): + self._last_range_change = 0 + self._main = self.DISPATCHER.get_module(self.main) + super().startModule(started_callback) + + def read_value(self): + if self.channel != self._main.channel: + return Done + result = self.sendRecv('RDGR?%d' % self.channel) + result = float(result) + if self.autorange == 'soft': + now = time.time() + if now > self._last_range_change + self.pause: + rng = int(max(self.minrange, self.range)) # convert from enum to int + if self.status[1] == '': + if abs(result) > self.RES_SCALE[rng]: + if rng < 22: + rng += 1 + self.log.info('chan %d: increased range to %.3g' % + (self.channel, self.RES_SCALE[rng])) + else: + lim = 0.2 + while rng > self.minrange and abs(result) < lim * self.RES_SCALE[rng]: + rng -= 1 + lim -= 0.05 # not more than 4 steps at once + # effectively: <0.16 %: 4 steps, <1%: 3 steps, <5%: 2 steps, <20%: 1 step + if lim != 0.2: + self.log.info('chan %d: lowered range to %.3g' % + (self.channel, self.RES_SCALE[rng])) + elif rng < 22: + rng = min(22, rng + 1) + self.log.info('chan: %d, %s, increased range to %.3g' % + (self.channel, self.status[1], self.RES_SCALE[rng])) + if rng != self.range: + self.write_range(rng) + self._last_range_change = now + return result + + def read_status(self): + if self.channel != self._main.channel: + return Done + result = int(self.sendRecv('RDGST?%d' % self.channel)) + result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities) + statustext = STATUS_TEXT[result] + if statustext: + return [self.Status.ERROR, statustext] + return [self.Status.IDLE, ''] + + def analyze_rdgrng(self, iscur, exc, rng, autorange, excoff): + if excoff: + self.iexc, self.vexc = 0,0 + elif iscur: + self.iexc, self.vexc = exc, 0 + else: + self.iexc, self.vexc = 0, exc + if autorange: + self.autorange = 'hard' + else: + if self.autorange == 'hard': + self.autorange = 'soft' + else: + self.autorange = self.autorange + self.range = rng + + def change_rdgrng(self, new, iscur, exc, rng, autorange, excoff): + if new.vexc != self.vexc: # in case vext is changed, do not consider iexc + new.iexc = 0 + if new.iexc != 0: # we need '!= 0' here, as bool(enum) is always True! + iscur = 1 + exc = new.iexc + excoff = 0 + elif new.vexc != 0: # we need '!= 0' here, as bool(enum) is always True! + iscur = 0 + exc = new.vexc + excoff = 0 + else: + excoff = 1 + rng = new.range + if new.autorange == 'hard': + autorange = 1 + else: + autorange = 0 + if new.autorange == 'soft': + if rng < new.minrange: + rng = new.minrange + return iscur, exc, rng, autorange, excoff + + def analyze_inset(self, on, dwell, pause, curve, tempco): + self.enabled, self.dwell, self.pause = on, dwell, pause + + def change_inset(self, new, on, dwell, pause, curve, tempco): + return new.enable, new.dwell, new.pause, curve, tempco + + def analyze_filt(self, on, settle, window): + self.filter = settle if on else 0 + + def change_filt(self, new, on, settle, window): + if new.filter: + return 1, new.filter, 80 # always use 80% filter + return 0, settle, window diff --git a/secop_psi/ls370sim.py b/secop_psi/ls370sim.py new file mode 100644 index 0000000..8fc0ece --- /dev/null +++ b/secop_psi/ls370sim.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""a very simple simulator for a LakeShore Model 370""" + +from secop.modules import Communicator +#from secop.lib import mkthread + +class Ls370Sim(Communicator): + CHANNEL_COMMANDS = [ + ('RDGR?%d', '1.0'), + ('RDGST?%d', '0'), + ('RDGRNG?%d', '0,5,5,0,0'), + ('INSET?%d', '1,5,5,0,0'), + ('FILTER?%d', '1,5,80'), + ] + OTHER_COMMANDS = [ + ('*IDN?', 'LSCI,MODEL370,370184,05302003'), + ('SCAN?', '3,1'), + ] + def earlyInit(self): + self._data = dict(self.OTHER_COMMANDS) + for fmt, v in self.CHANNEL_COMMANDS: + for chan in range(1,17): + self._data[fmt % chan] = v + # mkthread(self.run) + + def do_communicate(self, command): + # simulation part, time independent + for channel in range(1,17): + _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') + if excoff == '1': + self._data['RDGST?%d' % channel] = '4' + else: + self._data['RDGST?%d' % channel] = '0' + + chunks = command.split(';') + reply = [] + for chunk in chunks: + if '?' in chunk: + reply.append(self._data[chunk]) + else: + for nqarg in (1,0): + if nqarg == 0: + qcmd, arg = chunk.split(' ', 1) + qcmd += '?' + else: + qcmd, arg = chunk.split(',', nqarg) + qcmd = qcmd.replace(' ', '?', 1) + if qcmd in self._data: + self._data[qcmd] = arg + break + #if command.startswith('R'): + # print('> %s\t< %s' % (command, reply)) + return ';'.join(reply) + + #def run(self): + # # time dependent simulation + # while True: + # time.sleep(1)