Commit 04939dc4 authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch 'pi_e753_wave' into 'master'

pi-e753: manage low level wave.

See merge request !3265
parents b1b71941 1ab2e27b
Pipeline #47126 failed with stages
in 105 minutes and 30 seconds
......@@ -57,6 +57,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved management of current_configuration / default_configuration
- Project: Remove `-conda` suffix from requirement files
- PI E712 E753
- uniformization of communication and recorder.
- "wave" motion generator.
### Fixed
- Flint
......
......@@ -28,6 +28,21 @@ from bliss.common import event
from . import pi_gcs
"""
Special commands, e.g. fast polling commands, consist only of one
character. The 24th ASCII character e.g. is called #24. Note that
these commands are not followed by a termination character (but the
responses to them are).
* #5: Request Motion Status
* #6: Query If Position Has Changed Since Last POS? Command
* #7: Request Controller Ready Status
* #8: Query If Macro Is Running
* #9: Get Wave Generator Status
* #24: Stop All Motion
"""
class PI_E51X(Controller):
""" Base class for E517 and E518
"""
......
......@@ -6,24 +6,19 @@
# Distributed under the GNU LGPLv3. See LICENSE for more info.
import time
import re
import numpy
import weakref
import gevent
from bliss.controllers.motor import Controller
from bliss.common.utils import object_method
from bliss.common.utils import grouped
from bliss.common.utils import add_property
from bliss import global_map
from bliss.common.axis import AxisState, Motion, CyclicTrajectory
from bliss.config.channels import Cache
from bliss.common.switch import Switch as BaseSwitch
from bliss.common.logtools import *
from bliss.common.logtools import log_info, log_debug, log_error
from . import pi_gcs
from bliss.comm.util import TCP
from bliss.common.event import connect, disconnect
"""
Bliss controller for ethernet PI E712 piezo controller.
......@@ -55,35 +50,17 @@ config example:
"""
class PI_E712(Controller):
# POSSIBLE DATA TRIGGER SOURCE
WAVEFORM = 0
MOTION = 1
EXTERNAL = 3
IMMEDIATELY = 4
class PI_E712(pi_gcs.Communication, pi_gcs.Recorder, Controller):
def __init__(self, *args, **kwargs):
pi_gcs.Communication.__init__(self)
pi_gcs.Recorder.__init__(self)
Controller.__init__(self, *args, **kwargs)
self.sock = None
self.cname = "E712"
self.__axis_closed_loop = weakref.WeakKeyDictionary()
def initialize(self):
"""
Controller intialization : opens a single socket for all 3 axes.
"""
self.sock = pi_gcs.get_pi_comm(self.config, TCP)
global_map.register(self, children_list=[self.sock])
connect(self.sock, "connect", self._clear_error)
def finalize(self):
"""
Closes the controller socket.
"""
if self.sock:
self.sock.close()
disconnect(self.sock, "connect", self._clear_error)
self.com_initialize()
def initialize_axis(self, axis):
"""
......@@ -121,26 +98,7 @@ class PI_E712(Controller):
log_debug(self, "axis = %r" % axis.name)
# POSSIBLE DATA RECORDER TYPE
axis.TARGET_POSITION_OF_AXIS = 1
axis.CURRENT_POSITION_OF_AXIS = 2
axis.POSITION_ERROR_OF_AXIS = 3
axis.CONTROL_VOLTAGE_OF_OUTPUT_CHAN = 7
axis.DDL_OUTPUT_OF_AXIS = 13
axis.OPEN_LOOP_CONTROL_OF_AXIS = 14
axis.CONTROL_OUTPUT_OF_AXIS = 15
axis.VOLTAGE_OF_OUTPUT_CHAN = 16
axis.SENSOR_NORMALIZED_OF_INPUT_CHAN = 17
axis.SENSOR_FILTERED_OF_INPUT_CHAN = 18
axis.SENSOR_ELECLINEAR_OF_INPUT_CHAN = 19
axis.SENSOR_MECHLINEAR_OF_INPUT_CHAN = 20
axis.SLOWED_TARGET_OF_AXIS = 22
# POSSIBLE DATA TRIGGER SOURCE
axis.WAVEFORM = 0
axis.MOTION = 1
axis.EXTERNAL = 3
axis.IMMEDIATELY = 4
self._add_recoder_enum_on_axis(axis)
# supposed that we are on target on init
axis._last_on_target = True
......@@ -313,14 +271,6 @@ class PI_E712(Controller):
""" RAW COMMANDS """
def raw_write(self, axis, com):
com = com.encode()
self.sock.write(b"%s\n" % com)
def raw_write_read(self, axis, com):
com = com.encode()
return self.sock.write_readline(b"%s\n" % com)
def get_identifier(self, axis):
"""
Returns Identification information (`*IDN?` command).
......@@ -338,259 +288,6 @@ class PI_E712(Controller):
)
return info_str
def command(self, cmd, nb_line=1):
"""
Method to send a command to the controller.
Read answer if needed (ie. `cmd` contains a `?`).
- Encode `cmd` string.
- Add `\\n` terminator.
"""
with self.sock.lock:
cmd = cmd.strip()
need_reply = cmd.find("?") > -1
cmd = cmd.encode()
if need_reply:
if nb_line > 1:
reply = self.sock.write_readlines(cmd + b"\n", nb_line)
else:
reply = self.sock.write_readline(cmd + b"\n")
if not reply: # it's an error
errors = [self.name] + list(self.get_error())
raise RuntimeError(
"Device {0} error nb {1} => ({2})".format(*errors)
)
if nb_line > 1:
parsed_reply = list()
commands = cmd.split(b"\n")
if len(commands) == nb_line: # one reply per command
for cmd, rep in zip(commands, reply):
space_pos = cmd.find(b" ")
if space_pos > -1:
args = cmd[space_pos + 1 :]
parsed_reply.append(self._parse_reply(rep, args))
else:
parsed_reply.append(rep)
else: # a command with several replies
space_pos = cmd.find(b" ")
if space_pos > -1:
args = cmd[space_pos + 1 :]
for arg, rep in zip(args.split(), reply):
parsed_reply.append(self._parse_reply(rep, arg))
reply = parsed_reply
else:
space_pos = cmd.find(b" ")
if space_pos > -1:
reply = self._parse_reply(reply, cmd[space_pos + 1 :])
else:
reply = reply.decode()
return reply
else:
self.sock.write(cmd + b"\n")
errno, error_message = self.get_error()
if errno:
errors = [self.name, cmd] + [errno, error_message]
raise RuntimeError(
"Device {0} command {1} error nb {2} => ({3})".format(*errors)
)
def get_data_len(self):
"""
return how many point you can get from recorder
"""
return int(self.command("DRL? 1"))
def get_data_max_len(self):
"""
return the maximum number of records
"""
return int(self.command("SPA? 1 0x16000200"))
def get_data(self, from_event_id=0, npoints=None, rec_table_id=None):
"""
retrieved store data as a numpy structured array,
struct name will be the data_type + motor name.
i.e:
Target_Position_of_<motor_name> or Current_Position_of_<motor_name>
Args:
- from_event_id from which point id you want to read
- rec_table_id list of table you want to read, None means all
"""
if rec_table_id is None: # All table
# just ask the first table because they have the same synchronization
nb_availabe_points = int(self.command("DRL? 1"))
nb_availabe_points -= from_event_id
if npoints is None:
npoints = nb_availabe_points
else:
npoints = min(nb_availabe_points, npoints)
cmd = b"DRR? %d %d\n" % ((from_event_id + 1), npoints)
else:
rec_tables = " ".join((str(x) for x in rec_table_id))
nb_points = self.command("DRL? %s" % rec_tables, len(rec_table_id))
if isinstance(nb_points, list):
nb_points = min([int(x) for x in nb_points])
else:
nb_points = int(nb_points)
point_2_read = nb_points - from_event_id
if point_2_read < 0:
point_2_read = 0
elif npoints is not None and point_2_read > npoints:
point_2_read = npoints
cmd = b"DRR? %d %d %s\n" % (from_event_id + 1, point_2_read, rec_tables)
try:
exception_occurred = False
with self.sock.lock:
self.sock._write(cmd)
# HEADER
header = dict()
while 1:
line = self.sock.readline()
if not line:
return # no data available
if line.find(b"END_HEADER") > -1:
break
key, value = (x.strip().decode() for x in line[1:].split(b"="))
header[key] = value
ndata = int(header["NDATA"])
separator = chr(int(header["SEPARATOR"])).encode()
sample_time = float(header["SAMPLE_TIME"])
dim = int(header["DIM"])
column_info = dict()
keep_axes = {
x.channel: x for x in self.axes.values() if hasattr(x, "channel")
}
for name_id in range(8):
try:
desc = header["NAME%d" % name_id]
except KeyError:
break
else:
axis_pos = desc.find("axis")
if axis_pos < 0:
axis_pos = desc.find("chan")
axis_id = int(desc[axis_pos + len("axis") :])
if axis_id in keep_axes:
new_desc = desc[:axis_pos] + keep_axes[axis_id].name
column_info[name_id] = new_desc.replace(" ", "_")
dtype = [("timestamp", "f8")]
dtype += [(name, "f8") for name in column_info.values()]
data = numpy.zeros(ndata, dtype=dtype)
data["timestamp"] = (
numpy.arange(from_event_id, from_event_id + ndata) * sample_time
)
for line_id in range(ndata):
line = self.sock.readline().strip()
values = line.split(separator)
for column_id, name in column_info.items():
data[name][line_id] = values[column_id]
return data
except:
exception_occurred = True
try:
errno, error_message = self.get_error()
except:
pass
self.sock.close() # safe in case of ctrl-c
raise
finally:
if not exception_occurred:
errno, error_message = self.get_error()
# If we ask data in advance, ** Out of range **
# error is return.
# in that case it's not an error
if errno > 0 and errno != 17:
errors = [self.name, "get_data"] + [errno, error_message]
raise RuntimeError(
"Device {0} command {1} error nb {2} => ({3})".format(*errors)
)
def set_recorder_data_type(self, *motor_data_type):
"""
Configure the data recorder
Args:
motor_data_type should be a list of tuple with motor and datatype
i.e: motor_data_type=[px,px.CURRENT_POSITION_OF_AXIS,
py,py.CURRENT_POSITION_OF_AXIS]
"""
nb_recorder_table = len(motor_data_type) / 2
if nb_recorder_table * 2 != len(motor_data_type):
raise RuntimeError(
"Argument must be grouped by 2 "
"(motor1,data_type1,motor2,data_type2...)"
)
self.command("SPA 1 0x16000300 %d" % nb_recorder_table)
max_nb_recorder = int(self.command("TNR?"))
if nb_recorder_table > max_nb_recorder:
raise RuntimeError(
"Device %s too many recorder data, can only record %d"
% (self.name, max_nb_recorder)
)
cmd = "DRC "
cmd += " ".join(
(
"%d %s %d" % (rec_id + 1, motor.channel, data_type)
for rec_id, (motor, data_type) in enumerate(grouped(motor_data_type, 2))
)
)
self.command(cmd)
def start_recording(self, trigger_source, value=0, recorder_rate=None):
"""
start recording data according to what was asked to record.
@see set_recorder_data_type
Args:
- trigger_source could be WAVEFORM,MOTION,EXTERNAL,IMMEDIATELY
- value for EXTERNAL value is the trigger input line (0 mean all)
- recorder_rate if None max speed otherwise the period in seconds
"""
if trigger_source not in (
self.WAVEFORM,
self.MOTION,
self.EXTERNAL,
self.IMMEDIATELY,
):
raise RuntimeError(
"Device %s trigger source can only be:"
"WAVEFORM,MOTION,EXTERNAL or IMMEDIATELY"
)
if recorder_rate is not None:
cycle_time = float(self.command("SPA? 1 0xe000200"))
rate = int(recorder_rate / cycle_time) # should be faster than asked
else:
rate = 1
self.command("RTR %d" % rate)
nb_recorder = int(self.command("TNR?"))
cmd = "DRT "
cmd += " ".join(
(
"%d %d %d" % (rec_id, trigger_source, value)
for rec_id in range(1, nb_recorder + 1)
)
)
self.command(cmd)
def get_recorder_data_rate(self):
"""
return the rate of the data recording in seconds
"""
cycle_time, rtr = self.command("SPA? 1 0xe000200\nRTR?", 2)
return float(cycle_time) * int(rtr)
def output_position_gate(self, axis, position_1, position_2, output=1):
"""
This program an external gate on the specified output.
......@@ -756,14 +453,6 @@ class PI_E712(Controller):
axes_str = " ".join(["%d 0" % t.axis.channel for t in trajectories])
self.command("WGO " + axes_str)
def _parse_reply(self, reply, args):
args_pos = reply.find(b"=")
if reply[:args_pos] != args: # weird
print("Weird thing happens with connection of %s" % self.name)
return reply.decode()
else:
return reply[args_pos + 1 :].decode()
def _get_pos(self, channel):
"""
Args:
......@@ -864,16 +553,6 @@ class PI_E712(Controller):
axis._last_on_target = last_on_target
return last_on_target
def get_error(self):
_error_number = int(self.sock.write_readline(b"ERR?\n"))
_error_str = pi_gcs.get_error_str(_error_number)
return (_error_number, _error_str)
def _clear_error(self, connected):
if connected:
self.get_error() # read and clear any error
@object_method(types_info=("None", "string"))
def get_info(self, axis):
""" Return hw info
......
This diff is collapsed.
......@@ -6,11 +6,18 @@
# Distributed under the GNU LGPLv3. See LICENSE for more info.
# Distributed under the GNU LGPLv3. See LICENSE.txt for more info.
# PI GCS
"""
This module is a common base for PI controllers:
* for communication
* fro Wave generator
"""
from warnings import warn
import numpy
from bliss.comm.util import get_comm, get_comm_type, TCP
from bliss.common.event import connect, disconnect
from bliss.common.utils import grouped
from bliss import global_map
def get_pi_comm(config, ctype=None, **opts):
......@@ -415,3 +422,390 @@ pi_gcs_errors = {
-1080: "Parameter DAT file does not have correct revision.",
-1081: "User stages DAT file does not have correct revision.",
}
class Communication:
def __init__(self):
self.sock = None
def com_initialize(self):
self.sock = get_pi_comm(self.config, TCP)
global_map.register(self, children_list=[self.sock])
# ???
connect(self.sock, "connect", self._clear_error)
def finalize(self):
"""
Closes the controller socket.
"""
self.close()
disconnect(self.sock, "connect", self._clear_error)
def close(self):
if self.sock:
self.sock.close()
def get_error(self):
_error_number = int(self.sock.write_readline(b"ERR?\n"))
_error_str = get_error_str(_error_number)
return (_error_number, _error_str)
def _clear_error(self, connected):
if connected:
self.get_error() # read and clear any error
def command(self, cmd, nb_line=1):
"""
Method to send a command to the controller.
Read answer if needed (ie. `cmd` contains a `?`).
Parameters:
<cmd>: str
Command. Not encoded; Without terminator character.
[<nb_line>]: int
Number of lines expected in answer.
For multi-lines commands (ex: IFC?) or multiple commands.
Returns: str ; list of str ; tuple of str
Usage:
* id = self.command("*IDN?")
* ont = self.command("ONT? 1")
* ans = self.command("SPA? 1 0x07000A00")
* com_pars_list = self.command("IFC?", 5)
* pos, vel = self.command("POS? 1\nVEL? 1", 2)
Note:
Does not work for single char commands (#5 #9 #24 etc.)
"""
with self.sock.lock:
cmd = cmd.strip()
need_reply = cmd.find("?") > -1
cmd = cmd.encode()
if need_reply:
if nb_line > 1:
reply = self.sock.write_readlines(cmd + b"\n", nb_line)
else:
reply = self.sock.write_readline(cmd + b"\n")
if not reply: # it's an error
errors = [self.name] + list(self.get_error())
raise RuntimeError(
"PI Device {0} error nb {1} => ({2})".format(*errors)
)
if nb_line > 1:
# Multi-lines answer or multiple commands
parsed_reply = list()
commands = cmd.split(b"\n")
if len(commands) == nb_line:
# Many queries, one reply per query
# Return a tuple of str
for cmd, rep in zip(commands, reply):
space_pos = cmd.find(b" ")
if space_pos > -1:
args = cmd[space_pos + 1 :]
parsed_reply.append(self._parse_reply(rep, args, cmd))
else:
# No space in cmd => no param to parse. ex: "*IDN?" "CCL?"
parsed_reply.append(rep)
else:
# One command with reply in several lines
# Return a list of str
space_pos = cmd.find(b" ")
if space_pos > -1:
args = cmd[space_pos + 1 :]
for arg, rep in zip(args.split(), reply):
parsed_reply.append(self._parse_reply(rep, arg, cmd))
else:
# TSP? TAD? IFC? etc.
for ans in reply:
parsed_reply.append(ans.decode())
reply = parsed_reply
else:
# Single line answer.
# Example: cmd = "VEL? 1"
space_pos = cmd.find(b" ")
# print(f"cmd={cmd} space_pos={space_pos} reply={reply} ")
if space_pos > -1:
axes_arg = cmd[
space_pos + 1 :
] # 2nd part of the command -> axes id.
reply = self._parse_reply(reply, axes_arg, cmd)
else:
reply = reply.decode()
return reply
else:
# no reply expected.
self.sock.write(cmd + b"\n")
errno, error_message = self.get_error()
if errno:
errors = [self.name, cmd] + [errno, error_message]
raise RuntimeError(
"Device {0} command {1} error nb {2} => ({3})".format(*errors)
)
def raw_write(self, axis, com):
com = com.encode()
self.sock.write(b"%s\n" % com)
def raw_write_read(self, axis, com):
com = com.encode()
return self.sock.write_readline(b"%s\n" % com)
def _parse_reply(self, reply, args, cmd):
"""
Extract pertinent value in controller's answer.
<reply>: answer of the controller.