Commit e78752f5 authored by Alejandro Homs Puron's avatar Alejandro Homs Puron
Browse files

Merge branch 'emulator' into 'master'

Emulator

A list of emulators for TCP and serial line.

See merge request !246
parents ca6386dc 598ec7ea
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from bliss.controllers.emulator import main
main()
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
"""
Device Server Emulator
To create a server use the following configuration as a starting point:
.. code-block:: yaml
name: my_emulator
devices:
- class: SCPI
transports:
- type: tcp
url: :25000
To start the server you can do something like:
$ python -m bliss.controllers.emulator my_emulator
A simple *nc* client can be used to connect to the instrument:
$ nc 0 25000
*idn?
Bliss Team, Generic SCPI Device, 0, 0.1.0
"""
from __future__ import print_function
import os
import pty
import sys
import logging
import weakref
import gevent
from gevent.baseserver import BaseServer
from gevent.server import StreamServer
from gevent.fileobject import FileObject
_log = logging.getLogger('emulator')
class EmulatorServerMixin(object):
"""
Mixin class for TCP/Serial servers to handle line based commands.
Internal usage only
"""
def __init__(self, device=None, newline=None, baudrate=None):
self.device = device
self.baudrate = baudrate
self.newline = device.newline if newline is None else newline
self.special_messages = set(device.special_messages)
self.connections = {}
name = '{0}({1}, device={2})'.format(type(self).__name__, self.address,
device.name)
self._log = logging.getLogger('{0}.{1}'.format(_log.name, name))
self._log.info('listening on %s (newline=%r)', self.address,
self.newline)
def handle(self, sock, addr):
file_obj = sock.makefile(mode='rb')
self.connections[addr] = file_obj, sock
try:
return self.__handle(sock, file_obj)
finally:
file_obj.close()
del self.connections[addr]
def __handle(self, sock, file_obj):
"""
Handle new connection and requests
Arguments:
sock (gevent.socket.socket): new socket resulting from an accept
addr tuple): address (tuple of host, port)
"""
if self.newline == '\n' and not self.special_messages:
for line in file_obj:
self.handle_line(sock, line)
else:
# warning: in this mode read will block even if client
# disconnects. Need to find a better way to handle this
buff = ''
finish = False
while not finish:
readout = file_obj.read(1)
if not readout:
return
buff += readout
if buff in self.special_messages:
lines = buff,
buff = ''
else:
lines = buff.split(self.newline)
buff, lines = lines[-1], lines[:-1]
for line in lines:
if not line:
return
self.handle_line(sock, line)
def handle_line(self, sock, line):
"""
Handle a single command line. Emulates a delay if baudrate is defined
in the configuration.
Arguments:
sock (gevent.socket.socket): new socket resulting from an accept
addr (tuple): address (tuple of host, port)
line (str): line to be processed
Returns:
str: response to give to client or None if no response
"""
self.pause(len(line))
response = self.device.handle_line(line)
if response is not None:
self.pause(len(response))
sock.sendall(response)
return response
def pause(self, nb_bytes):
"""
Emulate a delay simulating the transport of the given number of bytes,
correspoding to the baudrate defined in the configuration
Arguments:
nb_bytes (int): number of bytes to transport
"""
# emulate baudrate
if not self.baudrate:
return
byterate = self.baudrate / 10.0
sleep = nb_bytes / byterate
gevent.sleep(sleep)
def broadcast(self, msg):
for _, (_, sock) in self.connections.items():
try:
sock.sendall(msg)
except:
self._log.exception('error in broadcast')
class SerialServer(BaseServer, EmulatorServerMixin):
"""
Serial line emulation server. It uses :ref:`pty.opentpy` to open a
pseudo-terminal simulating a serial line.
.. note::
Since :ref:`pty.opentpy` opens a non configurable file descriptor, it
is impossible to predict which /dev/pts/<N> will be used.
You have to be attentive to the first logging info messages when the
server is started. They indicate which device is in use :-(
"""
def __init__(self, *args, **kwargs):
device = kwargs.pop('device')
e_kwargs = dict(baudrate=kwargs.pop('baudrate', None),
newline=kwargs.pop('newline', None))
BaseServer.__init__(self, None, *args, **kwargs)
EmulatorServerMixin.__init__(self, device, **e_kwargs)
def set_listener(self, listener):
"""
Override of :ref:`BaseServer.set_listener` to initialize
a pty and properly fill the address
"""
if listener is None:
self.master, self.slave = pty.openpty()
else:
self.master, self.slave = listener
self.address = os.ttyname(self.slave)
self.fileobj = FileObject(self.master, mode='rb')
@property
def socket(self):
"""
Override of :ref:`BaseServer.socket` to return a socket
object for the pseudo-terminal file object
"""
return self.fileobj._sock
def _do_read(self):
# override _do_read to properly handle pty
try:
self.do_handle(self.socket, self.address)
except:
self.loop.handle_error(([self.address], self), *sys.exc_info())
if self.delay >= 0:
self.stop_accepting()
self._timer = self.loop.timer(self.delay)
self._timer.start(self._start_accepting_if_started)
self.delay = min(self.max_delay, self.delay * 2)
class TCPServer(StreamServer, EmulatorServerMixin):
"""
TCP emulation server
"""
def __init__(self, *args, **kwargs):
listener = kwargs.pop('url')
device = kwargs.pop('device')
e_kwargs = dict(baudrate=kwargs.pop('baudrate', None),
newline=kwargs.pop('newline', None))
StreamServer.__init__(self, listener, *args, **kwargs)
EmulatorServerMixin.__init__(self, device, **e_kwargs)
def handle(self, sock, addr):
info = self._log.info
info('new connection from %s', addr)
EmulatorServerMixin.handle(self, sock, addr)
info('client disconnected %s', addr)
class BaseDevice(object):
"""
Base intrument class. Override to implement an emulator for a specific
device
"""
DEFAULT_NEWLINE='\n'
special_messages = set()
def __init__(self, name, newline=DEFAULT_NEWLINE):
self.name = name
self.newline = newline
self._log = logging.getLogger('{0}.{1}'.format(_log.name, name))
self.__transports = weakref.WeakKeyDictionary()
@property
def transports(self):
"""the list of registered transports"""
return self.__transports.keys()
@transports.setter
def transports(self, transports):
self.__transports.clear()
for transport in transports:
self.__transports[transport] = None
def handle_line(self, line):
"""
To be implemented by the device.
Raises: NotImplementedError
"""
raise NotImplementedError
def broadcast(self, msg):
"""
broadcast the given message to all the transports
Arguments:
msg (str): message to be broadcasted
"""
for transport in self.transports:
transport.broadcast(msg)
class Server(object):
"""
The emulation server
Handles a set of devices
"""
def __init__(self, name='', devices=(), backdoor=None):
self.name = name
self._log = logging.getLogger('{0}.{1}'.format(_log.name, name))
self._log.info('Bootstraping server')
if backdoor:
from gevent.backdoor import BackdoorServer
banner = 'Welcome to Bliss emulator server console.\n' \
'My name is {0!r}. You can access me through the ' \
'\'server()\' function. Have fun!'.format(name)
self._log.info('Opening backdoor at %r', backdoor)
self.backdoor = BackdoorServer(backdoor, banner=banner,
locals=dict(server=weakref.ref(self)))
self.backdoor.start()
self.devices = {}
for device in devices:
try:
self.create_device(device)
except Exception as error:
dname = device.get('name', device.get('class', 'unknown'))
self._log.error('error creating device %s (will not be available): %s',
dname, error)
self._log.debug('details: %s', error, exc_info=1)
def create_device(self, device_info):
klass_name = device_info.get('class')
name = device_info.get('name', klass_name)
self._log.info('Creating device %s (%r)', name, klass_name)
device, transports = create_device(device_info)
self.devices[device] = transports
return device, transports
def get_device_by_name(self, name):
for device in self.devices:
if device.name == name:
return device
def start(self):
for device in self.devices:
for interface in self.devices[device]:
interface.start()
def stop(self):
for device in self.devices:
for interface in self.devices[device]:
interface.stop()
def serve_forever(self):
stop_events = []
for device in self.devices:
for interface in self.devices[device]:
stop_events.append(interface._stop_event)
self.start()
try:
gevent.joinall(stop_events)
finally:
self.stop()
def __str__(self):
return '{0}({1})'.format(self.__class__.__name__, self.name)
def find_device_class(device_info):
klass_name = device_info.get('class')
if 'package' in device_info:
package_name = device_info['package']
else:
module_name = device_info.get('module', klass_name.lower())
package_name = 'bliss.controllers.emulators.' + module_name
__import__(package_name)
package = sys.modules[package_name]
return getattr(package, klass_name)
def create_device(device_info):
device_info = dict(device_info)
klass = find_device_class(device_info)
klass_name = device_info.pop('class')
name = device_info.pop('name', klass_name)
transports_info = device_info.pop('transports', ())
device = klass(name, **device_info)
transports = []
for interface_info in transports_info:
ikwargs = dict(interface_info)
itype = ikwargs.pop('type', 'tcp')
if itype == 'tcp':
iklass = TCPServer
elif itype == 'serial':
iklass = SerialServer
ikwargs['device'] = device
transports.append(iklass(**ikwargs))
device.transports = transports
return device, transports
def create_server_from_config(config, name):
cfg = config.get_config(name)
backdoor, devices = cfg.get('backdoor', None), cfg.get('devices', ())
return Server(name=name, devices=devices, backdoor=backdoor)
def main():
import argparse
from bliss.config.static import get_config
parser = argparse.ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('name',
help='server name as defined in the static configuration')
parser.add_argument('--log-level', default='WARNING', help='log level',
choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'])
args = parser.parse_args()
fmt = '%(asctime)-15s %(levelname)-5s %(name)s: %(message)s'
level = getattr(logging, args.log_level.upper())
logging.basicConfig(format=fmt, level=level)
config = get_config()
server = create_server_from_config(config, args.name)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nCtrl-C Pressed. Bailing out...")
if __name__ == "__main__":
main()
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
__all__ = ['IcePAP', 'IcePAPError']
import re
import enum
import inspect
import logging
import weakref
import functools
from bliss.controllers.emulator import BaseDevice
MAX_AXIS = 128
def iter_axis(start=1, stop=MAX_AXIS+1, step=1):
start, stop = max(start, 1), min(stop, MAX_AXIS+1)
for i in xrange(start, stop, step):
if i % 10 > 8:
continue
yield i
VALID_AXES = list(iter_axis())
def default_parse_args(icepap, query=True, broadcast=False, args=()):
if query:
if broadcast:
axes = sorted(icepap._axes.keys())
else:
axes, args = args, ()
else:
if broadcast:
axes = sorted(icepap._axes.keys())
args = len(axes)*[args[0]]
else:
axes, args = args[::2], args[1::2]
return axes, args
axis_value_parse_args = default_parse_args
def value_axes_parse_args(icepap, query=True, broadcast=False, args=()):
if query:
if broadcast:
axes = sorted(icepap._axes.keys())
else:
axes, args = args, ()
else:
if broadcast:
axes = sorted(icepap._axes.keys())
else:
axes = args[1:]
args = len(axes)*[args[0]]
return axes, args
def args_axes_parse_args(icepap, query=True, broadcast=False, args=()):
if not query and not broadcast:
axes, args = args[1:], args[:1]
else:
axes, args = default_parse_args(icepap, query, broadcast, args)
return axes, args
class IcePAPError(enum.Enum):
CommandNotRecognized = 'Command not recognised'
CannotBroadCastQuery = 'Cannot broadcast a query'
CannotAcknowledgeBroadcast = 'Cannot acknowledge a broadcast'
WrongParameters = 'Wrong parameter(s)'
WrongNumberParameters = 'Wrong number of parameter(s)'
TooManyParameters = 'Too many parameters'
InvalidControllerBoardCommand = 'Command or option not valid in controller boards'
BadBoardAddress = 'Bad board address'
BoardNotPresent = 'Board is not present in the system'
def axis_command(func_or_name=None, mode='rw', default=None, cfg_info=None):
if func_or_name is None:
# used as a decorator with no arguments
return functools.partial(axis_command, default=default, mode=mode)
if callable(func_or_name):
# used directly as a decorator
func, name = func_or_name, func_or_name.__name__
if default is not None:
ValueError("Cannot give 'default' in method '{0}' "
"decorator".format(name))
else:
name = func_or_name
attr_name = "_" + name
if default is None:
raise ValueError('Must give default string value')
def func(self, value=None):
if value is None:
return getattr(self, attr_name, default)
setattr(self, attr_name, value)
return 'OK'
func._name = name
func._mode = mode
func._cfg_info = cfg_info
return func
axis_read_command = functools.partial(axis_command, mode='r')
class DriverPresence(enum.Enum):
NotPresent = 0
NotResponsive = 1
ConfigurationMode = 2
Alive = 3
class DriverMode(enum.Enum):
Oper = 0 << 2
Prog = 1 << 2
Test = 2 << 2
Fail = 3 << 2
class DriverDisable(enum.Enum):
PowerEnabled = 0 << 4
NotActive = 1 << 4
Alarm = 2 << 4
RemoteRackDisableInputSignal = 3 << 4
LocalRackDisableSwitch = 4 << 4
RemoteAxisDisableInputSignal = 5 << 4
LocalAxisDisableSwitch = 6 << 4
SoftwareDisable = 7 << 4
class DriverIndexer(enum.Enum):
Internal = 0 << 7
InSystem = 1 << 7
External = 2 << 7
Linked = 3 << 7
class DriverReady(enum.Enum):
NotReady = 0 << 9
Ready = 1 << 9
class DriverMoving(enum.Enum):
NotMoving = 0 << 10
Moving = 1 << 10
class DriverSettling(enum.Enum):
NotSettling = 0 << 11
Settling = 1 << 11
class DriverOutOfWindow(enum.Enum):
NotOutOfWindow = 0 << 12
OutOfWindow = 1 << 12
class DriverWarning(enum.Enum):
NotWarning = 0 << 13
Warning = 1 << 13
class DriverStopCode(enum.Enum):
EndOfMotion = 0 << 14
Stop = 1 << 14
Abort = 2 << 14
LimitPos = 3 << 14
LimitNeg = 4 << 14
ConfiguredStop = 5 << 14
Disabled = 6 << 14
InternalFailure = 8 << 14
MotorFailure = 9 << 14
PowerOverload = 10 << 14
DriverOverheading = 11 << 14
CloseLoopError = 12 << 14
ControlEncoderError = 13 << 14
ExternalAlarm = 14 << 14
class LimitPos(enum.Enum):
NotActive = 0 << 18