pi_e753.py 16.4 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
Benoit Formet's avatar
Benoit Formet committed
5
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
6
# Distributed under the GNU LGPLv3. See LICENSE for more info.
7
8
import contextlib
import numpy
9

10
from bliss.controllers.motor import Controller
11
from bliss.common.utils import object_attribute_get, object_attribute_set, object_method
12
from bliss.common.axis import AxisState
13
from bliss.common import axis as axis_module
14
from bliss.common.logtools import log_debug, log_info
Cyril Guilloud's avatar
Cyril Guilloud committed
15

Vincent Michel's avatar
Vincent Michel committed
16
from . import pi_gcs
17
import gevent.lock
Cyril Guilloud's avatar
Cyril Guilloud committed
18

19

20
21
"""
Bliss controller for ethernet PI E753 piezo controller.
22
Model PI E754 should be compatible.. to be tested.
23
24
"""

25
26
27
28
29
30
31
32
33
34
35
"""
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
* #9: Get Wave Generator Status
* #24: Stop All Motion
"""

36

37
class PI_E753(pi_gcs.Communication, pi_gcs.Recorder, Controller):
38
    def __init__(self, *args, **kwargs):
39
40
        pi_gcs.Communication.__init__(self)
        pi_gcs.Recorder.__init__(self)
41
        Controller.__init__(self, *args, **kwargs)
Cyril Guilloud's avatar
Cyril Guilloud committed
42

43
44
    # Init of controller.
    def initialize(self):
zorro@lid269's avatar
zorro@lid269 committed
45
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
46
        Controller intialization: open a single socket for all 3 axes.
zorro@lid269's avatar
zorro@lid269 committed
47
        """
48
49
        self.com_initialize()

50
51
52
        # acceleration is not mandatory in config
        self.axis_settings.config_setting["acceleration"] = False

Cyril Guilloud's avatar
Cyril Guilloud committed
53
54
        # Check model.
        try:
55
            idn_ans = self.command("*IDN?")
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
56
            log_info(self, "IDN?: %s", idn_ans)
Cyril Guilloud's avatar
Cyril Guilloud committed
57
58
59
60
61
62
63
64
65
            if idn_ans.find("E-753") > 0:
                self.model = "E-753"
            elif idn_ans.find("E-754") > 0:
                self.model = "E-754"
            else:
                self.model = "UNKNOWN"
        except:
            self.model = "UNKNOWN"

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
66
        log_debug(self, "model=%s", self.model)
Cyril Guilloud's avatar
Cyril Guilloud committed
67

68
    def initialize_axis(self, axis):
69
        log_debug(self, "axis initialization")
Cyril Guilloud's avatar
Cyril Guilloud committed
70

71
        # Enables the closed-loop.
72
        # Can be dangerous ??? test diff between target and position before ???
73
        # self._set_closed_loop(axis, True)
74
        self._add_recoder_enum_on_axis(axis)
Cyril Guilloud's avatar
Cyril Guilloud committed
75

cyril@lid16na1's avatar
cyril@lid16na1 committed
76
77
78
    def initialize_encoder(self, encoder):
        pass

Cyril Guilloud's avatar
Cyril Guilloud committed
79
    """ ON / OFF """
80

cyril@lid16na1's avatar
cyril@lid16na1 committed
81
82
83
84
85
86
    def set_on(self, axis):
        pass

    def set_off(self, axis):
        pass

Cyril Guilloud's avatar
Cyril Guilloud committed
87
88
    """ Position """

89
    def read_position(self, axis):
90
        _ans = self._get_target_pos(axis)
91
        log_debug(self, "read_position = %f" % _ans)
92
        return _ans
Cyril Guilloud's avatar
Cyril Guilloud committed
93

94
95
    def read_encoder(self, encoder):
        _ans = self._get_pos()
Cyril Guilloud's avatar
Cyril Guilloud committed
96
97
98

        # log_info(self, "read encodeer")
        # log_warning(self, "read encod")
99
        log_debug(self, "read_position measured = %f" % _ans)
100
        return _ans
Cyril Guilloud's avatar
Cyril Guilloud committed
101

zorro@lid269's avatar
zorro@lid269 committed
102
    """ VELOCITY """
103

104
105
    def read_velocity(self, axis):
        return self._get_velocity(axis)
106

107
    def set_velocity(self, axis, new_velocity):
108
109
110
        """
        - <new_velocity>: 'float'
        """
111
        log_debug(self, "set_velocity new_velocity = %f" % new_velocity)
112
        self.command("VEL 1 %f" % new_velocity)
113

114
        return self.read_velocity(axis)
115

zorro@lid269's avatar
zorro@lid269 committed
116
    """ STATE """
117

118
    def state(self, axis):
119
        # check if WAV motion is active  #9
120
121
122
        if self.sock.write_readline(chr(9).encode()) != b"0":
            return AxisState("MOVING")

123
124
        if self._get_closed_loop_status(axis):
            if self._get_on_target_status(axis):
125
                return AxisState("READY")
126
            else:
127
                return AxisState("MOVING")
128
        else:
129
130
            # open-loop => always ready.
            return AxisState("READY")
131

132
133
134
    def check_ready_to_move(self, axis, state):
        return True  # Can always move

zorro@lid269's avatar
zorro@lid269 committed
135
    """ MOVEMENTS """
136

137
    def prepare_move(self, motion):
138
        pass
139
140

    def start_one(self, motion):
141
142
143
144
145
146
        """
        - Sends 'MOV' or 'SVA' depending on closed loop mode.

        Args:
            - <motion> : Bliss motion object.

Cyril Guilloud's avatar
Cyril Guilloud committed
147
        Return:
148
149
150
151
            - None
        """
        if self._get_closed_loop_status(motion.axis):
            # Command in position.
152
            self.command("MOV 1 %g" % motion.target_pos)
153
154
        else:
            # Command in voltage.
155
            self.command("SVA 1 %g" % motion.target_pos)
156
157

    def stop(self, axis):
158
159
160
161
        """
        * STP -> stop asap
        * 24    -> stop asap
        * to check : copy of current position into target position ???
162
        * NB: 'HLT' command does not exist for pi e-753
163
        """
164
165
        if self._get_closed_loop_status(axis):
            self.command("STP")
zorro@lid269's avatar
zorro@lid269 committed
166

167
    """
zorro@lid269's avatar
zorro@lid269 committed
168
    E753 specific
169
    """
170

Cyril Guilloud's avatar
Cyril Guilloud committed
171
    @object_method(types_info=("None", "float"))
172
    def get_voltage(self, axis):
Cyril Guilloud's avatar
Cyril Guilloud committed
173
        """ Return voltage read from controller."""
174
        return float(self.command("SVA? 1"))
175

Cyril Guilloud's avatar
Cyril Guilloud committed
176
177
178
    @object_method(types_info=("None", "float"))
    def get_output_voltage(self, axis):
        """ Return output voltage read from controller. """
179
        return float(self.command("VOL? 1"))
Cyril Guilloud's avatar
Cyril Guilloud committed
180

181
182
    def set_voltage(self, axis, new_voltage):
        """ Sets Voltage to the controller."""
183
        self.command("SVA 1 %g" % new_voltage)
184

185
    def _get_velocity(self, axis):
Cyril Guilloud's avatar
Cyril Guilloud committed
186
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
187
        Return velocity taken from controller.
Cyril Guilloud's avatar
Cyril Guilloud committed
188
        """
189
        return float(self.command("VEL? 1"))
190

191
    def _get_pos(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
192
        """
193
194
195
196
        - no axis parameter as _get_pos() is also used by encoder object.

        Returns : float'
            Real position of axis read by capacitive sensor.
Cyril Guilloud's avatar
Cyril Guilloud committed
197
        """
198
        return float(self.command("POS? 1"))
199

200
    def _get_target_pos(self, axis):
Cyril Guilloud's avatar
Cyril Guilloud committed
201
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
202
        Return last target position (MOV?/SVA?/VOL? command) (setpoint value).
203
204
            - SVA? : Query the commanded output voltage (voltage setpoint).
            - VOL? : Query the current output voltage (real voltage).
Cyril Guilloud's avatar
Cyril Guilloud committed
205
            - MOV? : Return the last valid commanded target position.
Cyril Guilloud's avatar
Cyril Guilloud committed
206
        """
207
        if self._get_closed_loop_status(axis):
208
            return float(self.command("MOV? 1"))
209
        else:
210
            return float(self.command("SVA? 1"))
211

212
    def _get_on_target_status(self, axis):
213
        _status = self.command("ONT? 1")
214
        if _status == "1":
215
            return True
216
        elif _status == "0":
217
218
219
220
            return False
        else:
            return -1

221
    """ CLOSED LOOP"""
222

223
    def _get_closed_loop_status(self, axis):
224
        _status = self.command("SVO? 1")
225
        if _status == "1":
226
            return True
227
        elif _status == "0":
228
229
230
231
            return False
        else:
            return -1

232
    def _set_closed_loop(self, axis, state):
Cyril Guilloud's avatar
Cyril Guilloud committed
233
234
235
        """
        Activate closed-loop if <state> is True.
        """
236
        self.command("SVO 1 %d" % bool(state))
237

238
    @object_method(types_info=("None", "None"))
239
    def open_loop(self, axis):
240
        self._set_closed_loop(axis, False)
241

242
    @object_method(types_info=("None", "None"))
243
    def close_loop(self, axis):
244
        self._set_closed_loop(axis, True)
245

246
247
248
249
250
251
252
253
254
    @object_attribute_get(type_info="bool")
    def get_closed_loop(self, axis):
        return self._get_closed_loop_status(axis)

    @object_attribute_set(type_info="bool")
    def set_closed_loop(self, axis, value):
        print("set_closed_loop DISALBED FOR SECURITY ... ")
        # self._set_closed_loop(axis, True)

Cyril Guilloud's avatar
Cyril Guilloud committed
255
256
257
258
    @object_attribute_get(type_info="str")
    def get_model(self, axis):
        return self.model

259
    def _stop(self, axis):
Vincent Michel's avatar
Vincent Michel committed
260
        print("????????? PI_E753.py received _stop ???")
261
        self.command("STP")
Cyril Guilloud's avatar
Cyril Guilloud committed
262

263
264
265
266
267
268
    """
    ID/INFO
    """

    def get_id(self, axis):
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
269
        Return controller identifier.
270
        """
271
        return self.command("*IDN?")
272

Cyril Guilloud's avatar
Cyril Guilloud committed
273
274
275
    def get_axis_info(self, axis):
        """Return Controller specific info about <axis>
        """
276
        info_str = "PI AXIS INFO:\n"
Cyril Guilloud's avatar
Cyril Guilloud committed
277
278
279
        info_str += f"     voltage (SVA) = {self.get_voltage(axis)}\n"
        info_str += f"     output voltage (VOL) = {self.get_output_voltage(axis)}\n"
        info_str += f"     closed loop = {self.get_closed_loop(axis)}\n"
Cyril Guilloud's avatar
Cyril Guilloud committed
280
281
282

        return info_str

Cyril Guilloud's avatar
Cyril Guilloud committed
283
    def __info__(self):
284
285
286
287
288
289
290
291
292
293
294
295
296
297
        """

        IPSTART:
        If a DHCP server is present in the network, the
        IPSTART setting is ignored and the IP address is
        always obtained from the DHCP server.
        If the E-753 is directly connected to the Ethernet card in
        the PC (no DHCP server is present), the current IP
        address of the E-753 will be as follows:
        for IPSTART = 0, the IPADR setting will be used
        for IPSTART = 1, the default value 192.168.0.1 will be
        used.
        """

298
        idn = self.command("*IDN?")
299
300
301
        # ifc = self.command("IFC? IPADR MACADR IPSTART", 3)
        ifc = [
            bs.decode()
302
            for bs in self.sock.write_readlines(b"IFC?  IPADR MACADR IPSTART\n", 3)
303
304
        ]

305
        info_str = "CONTROLLER:\n"
306
307
308
309
310
        info_str += f"     ID: {idn}\n"
        info_str += f"     MAC address: {ifc[1]}\n"
        info_str += f"     IP address: {ifc[0]}\n"
        _start_mode = "use IPADR" if ifc[2] == "0" else "use default -> 192.168.0.1"
        info_str += f"     IP start: IPSTART={ifc[2]}({_start_mode})\n"
311
        info_str += "COMMUNICATION CONFIG:\n     "
312
        info_str += self.sock.__info__()
Cyril Guilloud's avatar
Cyril Guilloud committed
313
314
        return info_str

Cyril Guilloud's avatar
Cyril Guilloud committed
315
    @object_method(types_info=("None", "string"))
316
    def get_info(self, axis):
Cyril Guilloud's avatar
Cyril Guilloud committed
317
318
319
        return self.get_hw_info()

    def get_hw_info(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
320
        """
321
        Helpful parameter to tune the device.
322
323
324

        Args:
            None
325
326
        Return: str
            information about controller.
Cyril Guilloud's avatar
Cyril Guilloud committed
327
328
329
330
331
332
333
334

        IDN? for e753:
             Physik Instrumente, E-753.1CD, 111166712, 08.00.02.00

        IDN? for e754:
             (c)2016 Physik Instrumente (PI) GmbH & Co. KG, E-754.1CD, 117045756, 1.01

        0xffff000* parameters are not valid for 754
Cyril Guilloud's avatar
Cyril Guilloud committed
335
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
336

337
        _infos = [
338
339
            ("Identifier                 ", "*IDN?"),
            ("Com level                  ", "CCL?"),
340
341
342
343
344
345
            ("Real Position              ", "POS? 1"),
            ("Setpoint Position          ", "MOV? 1"),
            ("Position low limit         ", "SPA? 1 0X07000000"),
            ("Position High limit        ", "SPA? 1 0X07000001"),
            ("Velocity                   ", "VEL? 1"),
            ("On target                  ", "ONT? 1"),
346
347
            ("Target tolerance           ", "SPA? 1 0X07000900"),
            ("Settling time              ", "SPA? 1 0X07000901"),
348
349
350
351
352
353
354
            ("Sensor Offset              ", "SPA? 1 0X02000200"),
            ("Sensor Gain                ", "SPA? 1 0X02000300"),
            ("Closed loop status         ", "SVO? 1"),
            ("Auto Zero Calibration ?    ", "ATZ? 1"),
            ("Analog input setpoint      ", "AOS? 1"),
            ("Voltage Low Limit          ", "SPA? 1 0X07000A00"),
            ("Voltage High Limit         ", "SPA? 1 0X07000A01"),
355
356
        ]

Cyril Guilloud's avatar
Cyril Guilloud committed
357
358
359
360
361
362
363
        if self.model == "E-753":
            _infos.append(("Firmware name              ", "SEP? 1 0xffff0007"))
            _infos.append(("Firmware version           ", "SEP? 1 0xffff0008"))
            _infos.append(("Firmware description       ", "SEP? 1 0xffff000d"))
            _infos.append(("Firmware date              ", "SEP? 1 0xffff000e"))
            _infos.append(("Firmware developer         ", "SEP? 1 0xffff000f"))

364
        (error_nb, err_str) = self.get_error()
365
366
        _txt = '      ERR nb=%d  : "%s"\n' % (error_nb, err_str)

Cyril Guilloud's avatar
Cyril Guilloud committed
367
        # Reads pre-defined infos (1-line answers only)
368
        for i in _infos:
369
            _ans = self.command(i[1])
Cyril Guilloud's avatar
Cyril Guilloud committed
370
            _txt += f"        {i[0]} {_ans} \n"
371

372
        # Reads multi-lines infos.
Cyril Guilloud's avatar
Cyril Guilloud committed
373
374

        # IFC
375
        _ans = [bs.decode() for bs in self.sock.write_readlines(b"IFC?\n", 5)]
376
377
        _txt = _txt + "        %s    \n%s\n" % (
            "Communication parameters",
378
            "\n".join(_ans),
379
        )
380

Cyril Guilloud's avatar
Cyril Guilloud committed
381
382
383
384
385
386
387
388
389
        if self.model == "E-753":
            _tad_nb_lines = 2
            _tsp_nb_lines = 2
        elif self.model == "E-754":
            _tad_nb_lines = 3
            _tsp_nb_lines = 3

        # TSP
        _ans = [
390
            bs.decode() for bs in self.sock.write_readlines(b"TSP?\n", _tsp_nb_lines)
Cyril Guilloud's avatar
Cyril Guilloud committed
391
        ]
392
        _txt = _txt + "        %s    \n%s\n" % ("Analog setpoints", "\n".join(_ans))
393

Cyril Guilloud's avatar
Cyril Guilloud committed
394
395
        # TAD
        _ans = [
396
            bs.decode() for bs in self.sock.write_readlines(b"TAD?\n", _tad_nb_lines)
Cyril Guilloud's avatar
Cyril Guilloud committed
397
        ]
398
399
        _txt = _txt + "        %s    \n%s\n" % (
            "ADC value of analog input",
400
            "\n".join(_ans),
401
        )
402

403
404
        # ###  TAD[1]==131071  => broken cable ??
        # 131071 = pow(2,17)-1
405

406
        return _txt
407

408
409
410
411
412
413
414
415
    @object_method
    def start_wave(
        self, axis, wavetype, offset, amplitude, nb_cycles, wavelen, wait=True
    ):
        """
        Start a simple wav trajectory,
        -- wavetype can be LIN for a Linear  or
           SIN for a sinusoidal.
416
        -- offset: 
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
        -- amplitude motor displacement
        -- nb_cycles the number of time the motion is repeated.
        -- wavelen the time in second that should last the motion
        -- wait if you want to wait the end of the motion
        """

        # check wavetype can be "LIN" or "SIN"
        if wavetype not in ("LIN", "SIN"):
            raise ValueError('wavetype can only be "SIN" or "LIN"')

        offset *= axis.steps_per_unit
        amplitude *= axis.steps_per_unit
        servo_cycle = float(self.command("SPA? 1 0xe000200"))
        number_of_points = int(self.command("SPA? 1 0x13000004"))
        freq_faction = int(numpy.ceil((wavelen / number_of_points) / servo_cycle))
        wavelen = round(wavelen / (servo_cycle * freq_faction))

        if wavetype == "SIN":
            if amplitude < 0:  # cycle starting from max
                cmd = b"WAV 1 X SIN_P %d %d %d %d %d %d" % (
                    wavelen,
                    -amplitude,
                    offset - amplitude,
                    wavelen,
                    0,
                    (0.5 * wavelen),
                )
            else:
445
                cmd = b"WAV 1 X SIN_P %d %f %f %d %d %d" % (
446
447
                    wavelen,
                    +amplitude,
448
                    offset - amplitude / 2,
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
                    wavelen,
                    0,
                    (0.5 * wavelen),
                )
        else:
            cmd = b"WAV 1 X LIN %d %d %d %d %d %d" % (
                wavelen,
                amplitude,
                offset,
                wavelen,
                0,
                2,
            )

        if not axis._is_cache_position_disable:
            # This to be able to read the position
            # during the trajectory
            axis.settings.disable_cache("position")
        if not axis._is_cache_state_disable:
            axis.settings.disable_cache("state")
        commands = [
            b"WSL 1 1\n",
            cmd + b"\n",
            b"WTR 0 %d 1\n" % freq_faction,
            b"WGC 1 %d\n" % nb_cycles,
            b"WGO 1 1\n",
            b"ERR?\n",
        ]

        err = self.sock.write_readline(b"".join(commands))
        log_debug(self, "Error code %s", err)

        if wait:
            try:
                while self.state(axis) == AxisState("MOVING"):
                    gevent.sleep(0.1)
            except:
                self.stop_wave(axis)
                raise

    @object_method
    def stop_wave(self, axis):
        try:
            self.sock.write(b"WGO 1 0\n")
        finally:
            if not axis._is_cache_position_disable:
                axis.settings.disable_cache("position", flag=False)
            if not axis._is_cache_state_disable:
                axis.settings.disable_cache("state", flag=False)

499
500
501
502
503

class Axis(axis_module.Axis):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.channel = 1
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
        self._is_cache_position_disable = "position" in self.settings._disabled_settings
        self._is_cache_state_disable = "state" in self.settings._disabled_settings

    @contextlib.contextmanager
    @axis_module.lazy_init
    def run_wave(self, wavetype, offset, amplitude, nb_cycles, wavelen):
        """
        Helper to run a wave (trajectory) during a scan or something like this.
        And stop the trajectory at the end
        """
        self.controller.start_wave(
            self, wavetype, offset, amplitude, nb_cycles, wavelen, wait=False
        )
        try:
            yield
        finally:
            self.controller.stop_wave(self)
521
522
523
524
525
526
527
528

    def start_wave(self, wavetype, offset, amplitude, nb_cycles, wavelen, wait=False):
        self.controller.start_wave(
            self, wavetype, offset, amplitude, nb_cycles, wavelen, wait=wait
        )

    def stop_wave(self):
        self.controller.stop_wave(self)