add sim-server again based on socketserver

- fix ls370test config file
+ fix issues with frappy_psi.ls370res
+ add frappy_psi.ls370sim

Change-Id: Ie61e3ea01c4b9c7c1286426504e50acf9413a8ba
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34957
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
This commit is contained in:
zolliker 2024-11-14 11:42:29 +01:00
parent c2673952f4
commit 142add9109
3 changed files with 160 additions and 175 deletions

View File

@ -22,22 +22,35 @@
Usage: Usage:
bin/stringio-server <communciator> <server port> bin/sim-server <communicator class> -p <server port> [-o <option1>=<value> <option2>=<value>]
open a server on <server port> to communicate with the string based <communicator> over TCP/IP. open a server on <server port> to communicate with the string based <communicator> over TCP/IP.
Use cases, mainly for test purposes: 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 - 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://<host>:<port>
typically using communicator class frappy.io.StringIO
""" """
import sys import sys
import argparse import argparse
from pathlib import Path from pathlib import Path
import asyncore
import socket import socket
import time import time
import os
from ast import literal_eval
from socketserver import BaseRequestHandler, ThreadingTCPServer
# Add import path for inplace usage # Add import path for inplace usage
sys.path.insert(0, str(Path(__file__).absolute().parents[1])) 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 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: class Logger:
def debug(self, *args): def debug(self, *args):
pass pass
@ -144,43 +71,126 @@ class Logger:
exception = error = warn = info 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): 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", parser.add_argument("-v", "--verbose",
help="output full communication", help="output full communication",
action='store_true', default=False) action='store_true', default=False)
parser.add_argument("cls", parser.add_argument("cls",
type=str, type=str,
help="simulator class.\n",) help="communicator class.\n",)
parser.add_argument('-p', parser.add_argument('-p',
'--port', '--port',
action='store', action='store',
help='server port or uri', help='server port or uri',
default=2089) default=2089)
parser.add_argument('-o',
'--options',
action='store',
nargs='*',
help='options in the form key=value',
default=None)
return parser.parse_args(argv) return parser.parse_args(argv)
def poller(pollfunc):
while True:
time.sleep(1.0)
pollfunc()
def main(argv=None): def main(argv=None):
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
args = parse_argv(argv[1:]) args = parse_argv(argv[1:])
options = {'description': ''}
opts = {'description': 'simulator'} for item in args.options or ():
key, eq, value = item.partition('=')
handler_args = {'verbose': args.verbose} if not eq:
srv = Server(int(args.port), Handler, handler_args) raise ValueError(f"missing '=' in {item}")
module = get_class(args.cls)(args.cls, Logger(), opts, srv) try:
handler_args['module'] = module value = literal_eval(value)
module.earlyInit() except Exception:
mkthread(poller, module.doPoll) pass
srv.loop() options[key] = value
srv = Server(int(args.port), args.cls, options, args.verbose)
srv.serve_forever()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -63,7 +63,12 @@ def parse_result(reply):
class LakeShoreIO(HasIO): class LakeShoreIO(HasIO):
def set_param(self, cmd, *args): 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(' ', '?') tail = cmd.replace(' ', '?')
reply = self.io.communicate(f'{head};{tail}') reply = self.io.communicate(f'{head};{tail}')
return parse_result(reply) return parse_result(reply)
@ -99,7 +104,7 @@ class Switcher(LakeShoreIO, ChannelSwitcher):
if channelno is None: if channelno is None:
self.status = 'ERROR', 'no enabled channel' self.status = 'ERROR', 'no enabled channel'
return return
self.set_param(f'SCAN {channelno},0') self.set_param('SCAN ', channelno, 0)
def doPoll(self): def doPoll(self):
"""poll buttons """poll buttons
@ -160,7 +165,7 @@ class Switcher(LakeShoreIO, ChannelSwitcher):
self.measure_delay = chan.dwell self.measure_delay = chan.dwell
def set_active_channel(self, chan): 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() chan._last_range_change = time.monotonic()
self.set_delays(chan) self.set_delays(chan)
@ -227,7 +232,7 @@ class ResChannel(LakeShoreIO, Channel):
now = time.monotonic() now = time.monotonic()
if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause: if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
return None return None
result = self.get_param(f'RDGR{self.channel}') result = self.get_param(f'RDGR?{self.channel}')
if self.autorange: if self.autorange:
self.fix_autorange() self.fix_autorange()
if now + 0.5 > self._last_range_change + self.pause: if now + 0.5 > self._last_range_change + self.pause:
@ -251,7 +256,7 @@ class ResChannel(LakeShoreIO, Channel):
def read_value(self): def read_value(self):
if self.channel == self.switcher.value == self.switcher.target: if self.channel == self.switcher.value == self.switcher.target:
value = self._read_value() value = self.get_value()
if value is not None: if value is not None:
return value return value
return self.value # return previous value return self.value # return previous value
@ -264,7 +269,7 @@ class ResChannel(LakeShoreIO, Channel):
@CommonReadHandler(rdgrng_params) @CommonReadHandler(rdgrng_params)
def read_rdgrng(self): 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 self._prev_rdgrng = iscur, exc
if autorange: # pressed autorange button if autorange: # pressed autorange button
if not self._toggle_autorange: if not self._toggle_autorange:
@ -293,8 +298,7 @@ class ResChannel(LakeShoreIO, Channel):
excoff = 1 excoff = 1
rng = change['range'] rng = change['range']
if self.autorange: if self.autorange:
if rng < self.minrange: rng = max(rng, self.minrange)
rng = self.minrange
self.set_param(f'RDGRNG {self.channel}', iscur, exc, rng, 0, excoff) self.set_param(f'RDGRNG {self.channel}', iscur, exc, rng, 0, excoff)
self.read_range() self.read_range()

View File

@ -16,84 +16,38 @@
# Module authors: # Module authors:
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# ***************************************************************************** # *****************************************************************************
"""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 from frappy.modules import Communicator
class Ls370Sim(Communicator): class _Ls37xSim(Communicator):
CHANNEL_COMMANDS = [ # commands containing %d for the channel number
('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):
CHANNEL_COMMANDS = [ CHANNEL_COMMANDS = [
('RDGR?%d', '1.0'), ('RDGR?%d', '1.0'),
('RDGK?%d', '1.5'), ('RDGK?%d', '1.5'),
('RDGST?%d', '0'), ('RDGST?%d', '0'),
('RDGRNG?%d', '0,5,5,0,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'), ('FILTER?%d', '1,5,80'),
] ]
# commands not related to a channel
OTHER_COMMANDS = [ OTHER_COMMANDS = [
('*IDN?', 'LSCI,MODEL372,372184,05302003'),
('SCAN?', '3,1'), ('SCAN?', '3,1'),
('PID?1', '10,10,0'),
('*OPC?', '1'), ('*OPC?', '1'),
] ]
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self._res = {}
self._start = time.time()
self._data = dict(self.OTHER_COMMANDS) self._data = dict(self.OTHER_COMMANDS)
for fmt, v in self.CHANNEL_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 self._data[fmt % chan] = v
def communicate(self, command): def communicate(self, command):
@ -105,6 +59,10 @@ class Ls372Sim(Communicator):
self._data['RDGST?%d' % channel] = '6' self._data['RDGST?%d' % channel] = '6'
else: else:
self._data['RDGST?%d' % channel] = '0' 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(';') chunks = command.split(';')
reply = [] reply = []
@ -112,7 +70,7 @@ class Ls372Sim(Communicator):
if '?' in chunk: if '?' in chunk:
reply.append(self._data[chunk]) reply.append(self._data[chunk])
else: else:
for nqarg in (1,0): for nqarg in (1, 0):
if nqarg == 0: if nqarg == 0:
qcmd, arg = chunk.split(' ', 1) qcmd, arg = chunk.split(' ', 1)
qcmd += '?' qcmd += '?'
@ -125,3 +83,16 @@ class Ls372Sim(Communicator):
reply = ';'.join(reply) reply = ';'.join(reply)
self.comLog('< %s' % reply) self.comLog('< %s' % reply)
return 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'),
]