diff --git a/scpi-server.py b/scpi-server.py new file mode 100644 index 0000000..012a40c --- /dev/null +++ b/scpi-server.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +""" +Run a multi-threaded single-client SCPI Server implemented in Python. +Using a single-client server is sensible for many SCPI servers +where state would need to be shared between the multiple clients +and thus access to it would need to be made thread-safe. +In most cases, this doesn't make sense. Everything is +simply much easier when allowing only one client at a time. +The design choice for a multi-threaded server was made in +order to be able to actively disconnect additional clients +while another one is already connected. +Contains code from https://gist.github.com/pklaus/db709c8c1279348e0638 +""" + +# Make it work on Python 2 and Python 3: +try: + import socketserver +except ImportError: + import SocketServer as socketserver +import socket, threading +import argparse, random, logging +from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL +import peaktech + +logger = logging.getLogger('scpi-server') + +class CmdTCPServer(socketserver.ThreadingTCPServer): + """ + A TCP server made to respond to line based commands. + """ + + #: newline character(s) to be added to string responses + newline = '\n' + #: Ctrl-C will cleanly kill all spawned threads + daemon_threads = True + #: much faster rebinding possible + allow_reuse_address = True + address_family = socket.AF_INET6 + + class CmdRequestHandler(socketserver.StreamRequestHandler): + def handle(self): + if not self.server.lock.acquire(blocking=False): + self.log(DEBUG, 'An additional cliend tried to connect from {client}. Denying...') + return + self.log(DEBUG, 'Connected to {client}.') + try: + while True: + self.single_cmd() + except Disconnected: + pass + self.log(DEBUG, 'The client {client} closed the connection') + finally: + self.server.lock.release() + def read_cmd(self): + return self.rfile.readline().decode('utf-8').strip() + def log(self, level, msg, *args, **kwargs): + if type(level) == str: + level = getattr(logging, level.upper()) + msg = msg.format(client=self.client_address[0]) + logger.log(level, msg, *args, **kwargs) + def send_reply(self, reply): + if type(reply) == str: + if self.server.newline: reply += self.server.newline + reply = reply.encode('utf-8') + self.wfile.write(reply) + def single_cmd(self): + cmd = self.read_cmd() + if not cmd: raise Disconnected + self.log(DEBUG, 'Received a cmd: {}'.format(cmd)) + try: + reply = self.server.process(cmd) + if reply is not None: + self.send_reply(reply) + except: + self.send_reply('ERR') + + def __init__(self, server_address, name=None): + socketserver.TCPServer.__init__(self, server_address, self.CmdRequestHandler) + self.lock = threading.Lock() + self.name = name if name else "{}:{}".format(*server_address) + + def process(self, cmd): + """ + Implement this method to handle command processing. + For each command, this method will be called. + Return a string or bytes as appropriate. + If your the message is only a command (not a query), return None. + """ + raise NotImplemented + +class SCPIServerExample(CmdTCPServer): + + def process(self, cmd): + """ + This is the method to process each SCPI command + received from the client. + """ + if cmd.startswith('*IDN?'): + return self.name + if cmd.startswith('READ?'): + return str(peaktech.read()) + #return '{:+.6E}'.format(peaktech.read()) + #return '{:+.6E}'.format(random.random()) + else: + return 'unknown cmd' + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split('\n')[1]) + parser.add_argument('--port', type=int, default=5025, help='TCP port to listen to.') + parser.add_argument('--host', default='::', help='The host / IP address to listen at.') + parser.add_argument('--loglevel', default='INFO', help='log level', + choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']) + args = parser.parse_args() + logging.basicConfig(format='%(message)s', level=args.loglevel.upper()) + scpi_server = SCPIServerExample((args.host, args.port)) + try: + scpi_server.serve_forever() + except KeyboardInterrupt: + logger.info('Ctrl-C pressed. Shutting down...') + scpi_server.server_close() + +class Disconnected(Exception): pass + +if __name__ == "__main__": + main()