You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
#!/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()
|