Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Benoit Rousselle
bliss
Commits
e78752f5
Commit
e78752f5
authored
Oct 27, 2016
by
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
Changes
6
Hide whitespace changes
Inline
Side-by-side
bin/bliss-emulator
0 → 100755
View file @
e78752f5
#!/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
()
bliss/controllers/emulator.py
0 → 100644
View file @
e78752f5
# -*- 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
(
"
\n
Ctrl-C Pressed. Bailing out..."
)
if
__name__
==
"__main__"
:
main
()
bliss/controllers/emulators/__init__.py
0 → 100644
View file @
e78752f5
# -*- 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.
bliss/controllers/emulators/icepap.py
0 → 100644
View file @
e78752f5
# -*- 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