diff --git a/bin/sim-server b/bin/sim-server index 217d6ca..6b66daf 100755 --- a/bin/sim-server +++ b/bin/sim-server @@ -22,22 +22,35 @@ Usage: - bin/stringio-server + bin/sim-server -p [-o = =] 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 + + > bin/sim-server frappy_psi.ls370sim.Ls370Sim + +- relay to a communicator not using TCP/IP, if Frappy should run on an other host + + > bin/sim-server frappy.io.StringIO -o uri=serial:///dev/tty... + +- as a T, if the hardware allows only one connection, and more than one is needed: + + > bin/sim-server frappy.io.StringIO -o uri=tcp://: + + typically using communicator class frappy.io.StringIO """ import sys import argparse from pathlib import Path -import asyncore import socket import time +import os +from ast import literal_eval +from socketserver import BaseRequestHandler, ThreadingTCPServer # Add import path for inplace usage sys.path.insert(0, str(Path(__file__).absolute().parents[1])) @@ -45,92 +58,6 @@ sys.path.insert(0, str(Path(__file__).absolute().parents[1])) from frappy.lib import get_class, formatException, mkthread -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_line(self, line): - raise NotImplementedError - - 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, port, line_handler_cls, handler_args): - asyncore.dispatcher.__init__(self) - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.set_reuse_addr() - self.bind(('0.0.0.0', port)) - self.listen(5) - print('accept connections at port', port) - self.line_handler_cls = line_handler_cls - self.handler_args = handler_args - - def handle_accept(self): - pair = self.accept() - if pair is not None: - sock, addr = pair - print("Incoming connection from %s" % repr(addr)) - self.line_handler_cls(sock, self.handler_args) - - 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.secnode = None - self.dispatcher = self.Dispatcher() - - -class Handler(LineHandler): - def __init__(self, sock, handler_args): - super().__init__(sock) - self.module = handler_args['module'] - self.verbose = handler_args['verbose'] - - def handle_line(self, line): - try: - reply = self.module.communicate(line.strip()) - if self.verbose: - print('%-40s | %s' % (line, reply)) - except Exception: - print(formatException(verbose=True)) - return - self.send_line(reply) - - class Logger: def debug(self, *args): pass @@ -144,43 +71,126 @@ class Logger: exception = error = warn = info +class TcpRequestHandler(BaseRequestHandler): + def setup(self): + print(f'connection opened from {self.client_address}') + self.running = True + self.request.settimeout(1) + self.data = b'' + + def finish(self): + """called when handle() terminates, i.e. the socket closed""" + # close socket + try: + self.request.shutdown(socket.SHUT_RDWR) + except Exception: + pass + finally: + print(f'connection closed from {self.client_address}') + self.request.close() + + def poller(self): + while True: + time.sleep(1.0) + self.module.doPoll() + + def handle(self): + """handle a new connection""" + # do a copy of the options, as they are consumed + self.module = self.server.modulecls( + 'mod', Logger(), dict(self.server.options), self.server) + self.module.earlyInit() + + mkthread(self.poller) + while self.running: + try: + newdata = self.request.recv(1024) + if not newdata: + return + except socket.timeout: + # no new data during read, continue + continue + self.data += newdata + while self.running: + message, sep, self.data = self.data.partition(b'\n') + if not sep: + break + cmd = message.decode('latin-1') + try: + reply = self.module.communicate(cmd.strip()) + if self.server.verbose: + print('%-40s | %s' % (cmd, reply)) + except Exception: + print(formatException(verbose=True)) + return + outdata = reply.encode('latin-1') + b'\n' + try: + self.request.sendall(outdata) + except Exception as e: + print(repr(e)) + self.running = False + + +class Server(ThreadingTCPServer): + allow_reuse_address = os.name != 'nt' # False on Windows systems + + class Dispatcher: + def announce_update(self, *_): + pass + + def announce_update_error(self, *_): + pass + + def __init__(self, port, modulecls, options, verbose=False): + super().__init__(('', port), TcpRequestHandler, + bind_and_activate=True) + self.secnode = None + self.dispatcher = self.Dispatcher() + self.verbose = verbose + self.modulecls = get_class(modulecls) + self.options = options + print(f'started sim-server listening on port {port}') + + def parse_argv(argv): - parser = argparse.ArgumentParser(description="Simulate HW with a serial interface") + parser = argparse.ArgumentParser(description="Relay to a communicator (simulated HW or other)") parser.add_argument("-v", "--verbose", help="output full communication", action='store_true', default=False) parser.add_argument("cls", type=str, - help="simulator class.\n",) + help="communicator class.\n",) parser.add_argument('-p', '--port', action='store', help='server port or uri', default=2089) + parser.add_argument('-o', + '--options', + action='store', + nargs='*', + help='options in the form key=value', + default=None) return parser.parse_args(argv) -def poller(pollfunc): - while True: - time.sleep(1.0) - pollfunc() - - def main(argv=None): if argv is None: argv = sys.argv args = parse_argv(argv[1:]) - - opts = {'description': 'simulator'} - - handler_args = {'verbose': args.verbose} - srv = Server(int(args.port), Handler, handler_args) - module = get_class(args.cls)(args.cls, Logger(), opts, srv) - handler_args['module'] = module - module.earlyInit() - mkthread(poller, module.doPoll) - srv.loop() + options = {'description': ''} + for item in args.options or (): + key, eq, value = item.partition('=') + if not eq: + raise ValueError(f"missing '=' in {item}") + try: + value = literal_eval(value) + except Exception: + pass + options[key] = value + srv = Server(int(args.port), args.cls, options, args.verbose) + srv.serve_forever() if __name__ == '__main__': diff --git a/frappy_psi/ls370res.py b/frappy_psi/ls370res.py index 32313b5..c30406a 100644 --- a/frappy_psi/ls370res.py +++ b/frappy_psi/ls370res.py @@ -63,7 +63,12 @@ def parse_result(reply): class LakeShoreIO(HasIO): def set_param(self, cmd, *args): - head = ','.join([cmd] + [f'{a:g}' for a in args]) + args = [f'{a:g}' for a in args] + if ' ' in cmd.strip(): + args.insert(0, cmd) + else: + args[0] = cmd + args[0] + head = ','.join(args) tail = cmd.replace(' ', '?') reply = self.io.communicate(f'{head};{tail}') return parse_result(reply) @@ -99,7 +104,7 @@ class Switcher(LakeShoreIO, ChannelSwitcher): if channelno is None: self.status = 'ERROR', 'no enabled channel' return - self.set_param(f'SCAN {channelno},0') + self.set_param('SCAN ', channelno, 0) def doPoll(self): """poll buttons @@ -160,7 +165,7 @@ class Switcher(LakeShoreIO, ChannelSwitcher): self.measure_delay = chan.dwell def set_active_channel(self, chan): - self.set_param(f'SCAN {chan.channel},0') + self.set_param('SCAN ', chan.channel, 0) chan._last_range_change = time.monotonic() self.set_delays(chan) @@ -227,7 +232,7 @@ class ResChannel(LakeShoreIO, Channel): now = time.monotonic() if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause: return None - result = self.get_param(f'RDGR{self.channel}') + result = self.get_param(f'RDGR?{self.channel}') if self.autorange: self.fix_autorange() if now + 0.5 > self._last_range_change + self.pause: @@ -251,7 +256,7 @@ class ResChannel(LakeShoreIO, Channel): def read_value(self): if self.channel == self.switcher.value == self.switcher.target: - value = self._read_value() + value = self.get_value() if value is not None: return value return self.value # return previous value @@ -264,7 +269,7 @@ class ResChannel(LakeShoreIO, Channel): @CommonReadHandler(rdgrng_params) def read_rdgrng(self): - iscur, exc, rng, autorange, excoff = self.get_param(f'RDGRNG{self.channel}') + iscur, exc, rng, autorange, excoff = self.get_param(f'RDGRNG?{self.channel}') self._prev_rdgrng = iscur, exc if autorange: # pressed autorange button if not self._toggle_autorange: @@ -293,8 +298,7 @@ class ResChannel(LakeShoreIO, Channel): excoff = 1 rng = change['range'] if self.autorange: - if rng < self.minrange: - rng = self.minrange + rng = max(rng, self.minrange) self.set_param(f'RDGRNG {self.channel}', iscur, exc, rng, 0, excoff) self.read_range() diff --git a/frappy_psi/ls370sim.py b/frappy_psi/ls370sim.py index a74f449..4ebaf65 100644 --- a/frappy_psi/ls370sim.py +++ b/frappy_psi/ls370sim.py @@ -16,84 +16,38 @@ # Module authors: # Markus Zolliker # ***************************************************************************** -"""a very simple simulator for a LakeShore Model 370""" +"""a very simple simulator for LakeShore Models 370 and 372 +reduced to the functionality actually used in e.g. frappy_psi.ls370res +""" + +import time from frappy.modules import Communicator -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'), - ('*OPC?', '1'), - ] - - def earlyInit(self): - super().earlyInit() - self._data = dict(self.OTHER_COMMANDS) - for fmt, v in self.CHANNEL_COMMANDS: - for chan in range(1,17): - self._data[fmt % chan] = v - - def communicate(self, command): - self.comLog('> %s' % 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] = '6' - 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 - reply = ';'.join(reply) - self.comLog('< %s' % reply) - return reply - - -class Ls372Sim(Communicator): +class _Ls37xSim(Communicator): + # commands containing %d for the channel number CHANNEL_COMMANDS = [ ('RDGR?%d', '1.0'), ('RDGK?%d', '1.5'), ('RDGST?%d', '0'), ('RDGRNG?%d', '0,5,5,0,0'), - ('INSET?%d', '1,5,5,0,0'), + ('INSET?%d', '1,3,3,0,0'), ('FILTER?%d', '1,5,80'), ] + # commands not related to a channel OTHER_COMMANDS = [ - ('*IDN?', 'LSCI,MODEL372,372184,05302003'), ('SCAN?', '3,1'), - ('PID?1', '10,10,0'), ('*OPC?', '1'), ] def earlyInit(self): super().earlyInit() + self._res = {} + self._start = time.time() self._data = dict(self.OTHER_COMMANDS) for fmt, v in self.CHANNEL_COMMANDS: - for chan in range(1,17): + for chan in range(1, 17): self._data[fmt % chan] = v def communicate(self, command): @@ -105,6 +59,10 @@ class Ls372Sim(Communicator): self._data['RDGST?%d' % channel] = '6' else: self._data['RDGST?%d' % channel] = '0' + channel = int(self._data['SCAN?'].split(',', 1)[0]) + self._res[channel] = channel + (time.time() - self._start) / 3600 + strvalue = f'{self._res[channel]:g}' + self._data['RDGR?%d' % channel] = self._data['RDGK?%d' % channel] = strvalue chunks = command.split(';') reply = [] @@ -112,7 +70,7 @@ class Ls372Sim(Communicator): if '?' in chunk: reply.append(self._data[chunk]) else: - for nqarg in (1,0): + for nqarg in (1, 0): if nqarg == 0: qcmd, arg = chunk.split(' ', 1) qcmd += '?' @@ -125,3 +83,16 @@ class Ls372Sim(Communicator): reply = ';'.join(reply) self.comLog('< %s' % reply) return reply + + +class Ls370Sim(_Ls37xSim): + OTHER_COMMANDS = _Ls37xSim.OTHER_COMMANDS + [ + ('*IDN?', 'LSCI,MODEL370,370184,05302003'), + ] + + +class Ls372Sim(_Ls37xSim): + OTHER_COMMANDS = _Ls37xSim.OTHER_COMMANDS + [ + ('*IDN?', 'LSCI,MODEL372,372184,05302003'), + ('PID?1', '10,10,0'), + ]