axis.py 69.5 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
7
# Distributed under the GNU LGPLv3. See LICENSE for more info.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
Axis related classes (:class:`~bliss.common.axis.Axis`, \
:class:`~bliss.common.axis.AxisState` and :class:`~bliss.common.axis.Motion`)

These classes are part of the bliss motion subsystem.
They are not to be instantiated directly. They are the objects produced
as calls to :meth:`~bliss.config.static.Config.get`. Example::

    >>> from bliss.config.static import get_config

    >>> cfg = get_config()
    >>> energy = cfg.get('energy')
    >>> energy
    <bliss.common.axis.Axis object at 0x7f7baa7f6d10>

    >>> energy.move(120)
    >>> print(energy.position)
    120.0

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
27
    >>> print energy.state
28
29
    READY (Axis is READY)
"""
30
from bliss import global_map
31
from bliss.common.cleanup import capture_exceptions
32
33
from bliss.common.motor_config import StaticConfig
from bliss.common.motor_settings import AxisSettings
34
from bliss.common import event
35
from bliss.common.greenlet_utils import protect_from_one_kill
36
from bliss.common.utils import with_custom_members
37
from bliss.config.channels import Channel
38
from bliss.common.logtools import log_debug, lprint, lprint_disable
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
39
from bliss.common.utils import rounder
40

41
import gevent
42
import re
43
import sys
44
import math
45
import functools
46
import numpy
47
from unittest import mock
48
import warnings
49
50

warnings.simplefilter("once", DeprecationWarning)
51

52

53
#: Default polling time
54
DEFAULT_POLLING_TIME = 0.02
Matias Guijarro's avatar
Matias Guijarro committed
55

56

57
class GroupMove:
58
    def __init__(self, parent=None):
59
60
        self.parent = parent
        self._move_task = None
61
62
63
        self._motions_dict = dict()
        self._stop_motion = None
        self._user_stopped = False
64
65
66
67
68
69
70
71

    # Public API

    @property
    def is_moving(self):
        # A greenlet evaluates to True when not dead
        return bool(self._move_task)

72
73
74
75
76
77
78
    def move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
79
        polling_time=None,
80
    ):
81
82
83
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
        self._user_stopped = False
84
85
        started = gevent.event.Event()
        self._move_task = gevent.spawn(
86
87
88
89
90
91
92
93
            self._move,
            motions_dict,
            start_motion,
            stop_motion,
            move_func,
            started,
            polling_time,
        )
94

95
96
97
98
99
        for _, motions in motions_dict.items():
            for motion_obj in motions:
                msg = motion_obj.user_msg
                if msg:
                    lprint(msg)
100
101
102
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
103
        except BaseException:
104
105
            self.stop()
            raise
106
107
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
108
            self.wait()
109
110
111

    def wait(self):
        if self._move_task is not None:
112
113
            try:
                self._move_task.get()
114
            except BaseException:
115
116
                self.stop()
                raise
117
118

    def stop(self, wait=True):
119
120
121
122
123
124
        with capture_exceptions(raise_index=0) as capture:
            if self._move_task is not None:
                with capture():
                    self._stop_move(self._motions_dict, self._stop_motion)
                if wait:
                    self._move_task.get()
125
126
127

    # Internal methods

128
    def _monitor_move(self, motions_dict, move_func, polling_time):
129
        monitor_move = dict()
Vincent Michel's avatar
Vincent Michel committed
130
        for controller, motions in motions_dict.items():
131
            for motion in motions:
132
                if move_func is None:
133
                    move_func = "_handle_move"
134
135
136
                axis_polling_time = (
                    motion.axis._polling_time if polling_time is None else polling_time
                )
137
                task = gevent.spawn(
138
                    getattr(motion.axis, move_func), motion, axis_polling_time
139
                )
140
141
                monitor_move[motion] = task
        try:
142
            gevent.joinall(monitor_move.values(), raise_error=True)
143
144
        finally:
            # update the last motor state
Vincent Michel's avatar
Vincent Michel committed
145
            for motion, task in monitor_move.items():
146
147
                try:
                    motion.last_state = task.get(block=False)
148
                except BaseException:
149
                    pass
150
151

    def _stop_move(self, motions_dict, stop_motion):
152
        self._user_stopped = True
153
        stop = []
Vincent Michel's avatar
Vincent Michel committed
154
        for controller, motions in motions_dict.items():
155
            stop.append(gevent.spawn(stop_motion, controller, motions))
156
157
158
159
        # Raise exception if any, when all the stop tasks are finished
        for task in gevent.joinall(stop):
            task.get()

160
161
    def _stop_wait(self, motions_dict, exception_capture):
        stop_wait = []
Vincent Michel's avatar
Vincent Michel committed
162
        for controller, motions in motions_dict.items():
163
164
165
            for motion in motions:
                stop_wait.append(gevent.spawn(motion.axis._move_loop))
        gevent.joinall(stop_wait)
166
        task_index = 0
Vincent Michel's avatar
Vincent Michel committed
167
        for controller, motions in motions_dict.items():
168
169
170
171
            for motion in motions:
                with exception_capture():
                    motion.last_state = stop_wait[task_index].get()
                task_index += 1
172

173
    @protect_from_one_kill
174
175
    def _do_backlash_move(self, motions_dict, polling_time):
        backlash_move = []
Vincent Michel's avatar
Vincent Michel committed
176
        for controller, motions in motions_dict.items():
177
178
            for motion in motions:
                if motion.backlash:
179
180
181
182
183
                    if self._user_stopped:
                        # have to recalculate target: do backlash from where it stopped
                        motion.target_pos = (
                            motion.axis.dial * motion.axis.steps_per_unit
                        )
184
185
186
187
188
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
189
190
191
192
193
194
                    axis_polling_time = (
                        motion.axis._polling_time
                        if polling_time is None
                        else polling_time
                    )

195
196
                    backlash_move.append(
                        gevent.spawn(
197
198
199
                            motion.axis._backlash_move,
                            backlash_motion,
                            axis_polling_time,
200
201
                        )
                    )
202
        gevent.joinall(backlash_move)
203
        gevent.joinall(backlash_move, raise_error=True)
204

205
206
207
208
209
210
211
212
213
    def _move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func,
        started_event,
        polling_time,
    ):
214
        # Set axis moving state
Vincent Michel's avatar
Vincent Michel committed
215
        for motions in motions_dict.values():
216
            for motion in motions:
217
                motion.last_state = None
218
                motion.axis._set_moving_state()
219

Vincent Michel's avatar
Vincent Michel committed
220
                for _, chan in motion.axis._beacon_channels.items():
221
                    chan.unregister_callback(chan._setting_update_cb)
222
        with capture_exceptions(raise_index=0) as capture:
223
224
            try:
                # Spawn start motion for all controllers
225
226
                start = [
                    gevent.spawn(start_motion, controller, motions)
Vincent Michel's avatar
Vincent Michel committed
227
                    for controller, motions in motions_dict.items()
228
                ]
229

230
                # Wait for the controllers to be started
231
                with capture():
232
233
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
234
                    gevent.joinall(start)
235
236
237
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
238

239
240
                    self._stop_wait(motions_dict, capture)
                    return
241

242
243
244
245
246
247
248
                # All the controllers are now started
                started_event.set()

                if self.parent:
                    event.send(self.parent, "move_done", False)

                # Spawn the monitoring for all motions
249
                with capture():
250
251
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
252
253
254
255
256
257
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
258
259
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
260
261
262
263
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
264
                reset_setpos = capture.failed or self._user_stopped
265

266
267
268
269
                # cleanup
                # -------
                # update final state ; in case of exception
                # state is set to FAULT
Vincent Michel's avatar
Vincent Michel committed
270
                for motions in motions_dict.values():
271
                    for motion in motions:
272
273
274
275
                        state = motion.last_state
                        if state is not None:
                            continue

276
                        with capture():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
277
                            state = motion.axis.hw_state
278
279
280
                        if state is None:
                            state = AxisState("FAULT")
                        # update state and update dial pos.
281
282
                        with capture():
                            motion.axis._update_settings(state)
283
284
285
286
287
288
289
290
291

                # update set position if motor has been stopped,
                # or if an exception happened or if motion type is
                # home search or hw limit search ;
                # as state update happened just before, this
                # is equivalent to sync_hard -> emit the signal
                # (useful for real motor positions update in case
                # of pseudo axis)
                # -- jog move is a special case
292
                if len(motions_dict) == 1:
Vincent Michel's avatar
Vincent Michel committed
293
                    motion = motions_dict[list(motions_dict.keys()).pop()][0]
294
                    if motion.type == "jog":
295
                        reset_setpos = False
296
297
298
299
                        motion.axis._jog_cleanup(
                            motion.saved_velocity, motion.reset_position
                        )
                    elif motion.type == "homing":
300
                        reset_setpos = True
301
                    elif motion.type == "limit_search":
302
303
                        reset_setpos = True
                if reset_setpos:
304
                    with capture():
Vincent Michel's avatar
Vincent Michel committed
305
                        for motions in motions_dict.values():
306
                            for motion in motions:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
307
                                motion.axis._set_position = motion.axis.position
308
309
                                event.send(motion.axis, "sync_hard")

Vincent Michel's avatar
Vincent Michel committed
310
                for motions in motions_dict.values():
311
312
313
314
                    for motion in motions:
                        with capture():
                            motion.axis._Axis__execute_post_move_hook([motion])

Vincent Michel's avatar
Vincent Michel committed
315
                        for _, chan in motion.axis._beacon_channels.items():
316
317
318
319
320
                            chan.register_callback(chan._setting_update_cb)

                        motion.axis._set_move_done()
                if self.parent:
                    event.send(self.parent, "move_done", True)
321

Cyril Guilloud's avatar
Cyril Guilloud committed
322

323
class Modulo:
324
325
326
327
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
328
        dial_pos = axis.dial
329
        axis._Axis__do_set_dial(dial_pos % self.modulo)
330
331


332
class Motion:
333
334
335
336
337
338
339
340
    """Motion information

    Represents a specific motion. The following members are present:

    * *axis* (:class:`Axis`): the axis to which this motion corresponds to
    * *target_pos* (:obj:`float`): final motion position
    * *delta* (:obj:`float`): motion displacement
    * *backlash* (:obj:`float`): motion backlash
Vincent Michel's avatar
Vincent Michel committed
341

342
343
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
344
    """
Matias Guijarro's avatar
Matias Guijarro committed
345

346
347
348
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
349
        self.__axis = axis
350
        self.__type = motion_type
351
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
352
353
354
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
355

Matias Guijarro's avatar
Matias Guijarro committed
356
357
    @property
    def axis(self):
358
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
359
        return self.__axis
360

361
362
363
364
    @property
    def type(self):
        return self.__type

365
366
367
368
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
369
370
371
372
373
374
            msg = (
                f"Moving {self.axis.name} from {start_} until it is stopped, at constant velocity: {self.target_pos}\n"
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

375
376
377
378
379
380
381
382
383
384
385
        else:
            if self.user_target_pos is None:
                return None
            else:
                if isinstance(self.user_target_pos, str):
                    # can be a string in case of special move like limit search, homing...
                    end_ = self.user_target_pos
                else:
                    end_ = rounder(self.axis.tolerance, self.user_target_pos)
                return f"Moving {self.axis.name} from {start_} to {end_}"

Matias Guijarro's avatar
Matias Guijarro committed
386

387
388
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
389

390
391
392
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
393

Matias Guijarro's avatar
Matias Guijarro committed
394
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
395
396
397
398
399
400
401
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
402
403
404
405
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

406
407
408
409
410
411
412
    @property
    def axis(self):
        return self.__axis

    @property
    def pvt(self):
        return self.__pvt
Matias Guijarro's avatar
Matias Guijarro committed
413

414
415
    @property
    def events_positions(self):
416
        return self._events_positions
417

418
419
    @events_positions.setter
    def events_positions(self, events):
420
        self._events_positions = events
421

422
423
424
    def has_events(self):
        return self._events_positions.size

425
426
427
428
429
430
431
    def __len__(self):
        return len(self.pvt)

    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
432
433
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
434
        pvt = numpy.copy(self.__pvt)
435
436
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
437
        new_obj = self.__class__(self.axis, pvt)
438
        pattern_evts = numpy.copy(self._events_positions)
439
440
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
441
        new_obj._events_positions = pattern_evts
442
        return new_obj
443
444


445
class CyclicTrajectory(Trajectory):
446
447
448
449
450
451
452
453
    def __init__(self, axis, pvt, nb_cycles=1, origin=0):
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
                    point coordinates are in relative space
        """
        super(CyclicTrajectory, self).__init__(axis, pvt)
454
455
        self.nb_cycles = nb_cycles
        self.origin = origin
456
457
458
459
460

    @property
    def pvt_pattern(self):
        return super(CyclicTrajectory, self).pvt

461
462
463
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
464

465
466
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
467
468
        self._events_positions = values

469
470
471
472
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
473
474
475
476
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
477
478
479

    @property
    def pvt(self):
480
        """Return the full PVT table. Positions are absolute"""
481
482
483
484
485
486
487
488
489
490
491
492
493
494
        pvt_pattern = self.pvt_pattern
        if self.is_closed:
            # take first point out because it is equal to the last
            raw_pvt = pvt_pattern[1:]
            cycle_size = raw_pvt.shape[0]
            size = self.nb_cycles * cycle_size + 1
            offset = 1
        else:
            raw_pvt = pvt_pattern
            cycle_size = raw_pvt.shape[0]
            size = self.nb_cycles * cycle_size
            offset = 0
        pvt = numpy.empty(size, dtype=raw_pvt.dtype)
        last_time, last_position = 0, self.origin
495
        for cycle in range(self.nb_cycles):
496
            start = cycle_size * cycle + offset
497
498
            end = start + cycle_size
            pvt[start:end] = raw_pvt
499
500
501
502
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
503
504

        if self.is_closed:
505
506
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
507
508
509

        return pvt

510
511
512
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
513
        time_offset = 0.0
514
        last_time = self.pvt_pattern["time"][-1]
515
        nb_pattern_evts = len(pattern_evts)
516
517
518
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
519
        for i in range(self.nb_cycles):
520
521
522
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
523
            sub_evts[:] = pattern_evts
524
            sub_evts["time"] += time_offset
525
526
527
            time_offset += last_time
        return all_events

528
529
530
531
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
532
533
534
535
        new_obj = super(CyclicTrajectory, self).convert_to_dial()
        new_obj.origin = self.axis.user2dial(self.origin) * self.axis.steps_per_unit
        new_obj.nb_cycles = self.nb_cycles
        return new_obj
536
537


538
539
540
541
542
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
543

544
    return func_wrapper
545

Matias Guijarro's avatar
Matias Guijarro committed
546

547
@with_custom_members
548
class Axis:
549
550
551
552
553
554
555
    """
    Bliss motor axis

    Typical usage goes through the bliss configuration (see this module
    documentation above for an example)
    """

Matias Guijarro's avatar
Matias Guijarro committed
556
557
558
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
559
        self.__settings = AxisSettings(self)
Matias Guijarro's avatar
Matias Guijarro committed
560
        self.__move_done = gevent.event.Event()
561
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
562
        self.__move_done.set()
563
        self.__move_done_callback.set()
564
565
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
566
            hook._add_axis(self)
567
568
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
569
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
570
        self.__init_config_properties()
571
        self._group_move = GroupMove()
572
        self._beacon_channels = dict()
573
574
575
576
577
        self._move_stop_channel = Channel(
            "axis.%s.move_stop" % self.name,
            default_value=False,
            callback=self._external_stop,
        )
578
        self._lock = gevent.lock.Semaphore()
579
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
580

581
582
583
584
585
586
587
588
589
590
591
592
593
        try:
            config.parent
        # some Axis don't have a controller
        # like SoftAxis
        except AttributeError:
            disabled_cache = list()
        else:
            disabled_cache = config.parent.get(
                "disabled_cache", []
            )  # get it from controller (parent)
        disabled_cache.extend(config.get("disabled_cache", []))  # get it for this axis
        for settings_name in disabled_cache:
            self.settings.disable_cache(settings_name)
594
        self._unit = self.config.get("unit", str, None)
595
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
596
        global_map.register(self, parents_list=["axes", controller])
597

598
    def __close__(self):
599
600
601
602
603
604
605
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

606
607
608
609
610
611
612
613
    @property
    def no_offset(self):
        return self.__no_offset

    @no_offset.setter
    def no_offset(self, value):
        self.__no_offset = value

614
615
616
617
618
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
619
620
    @property
    def name(self):
621
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
622
623
624
625
        return self.__name

    @property
    def controller(self):
626
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
627
628
629
630
        return self.__controller

    @property
    def config(self):
631
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
632
633
634
635
        return self.__config

    @property
    def settings(self):
636
637
638
639
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
640
641
642
643
        return self.__settings

    @property
    def is_moving(self):
644
645
646
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
647
648
        return not self.__move_done.is_set()

Matias Guijarro's avatar
Matias Guijarro committed
649
650
651
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
652
653
        self.__steps_per_unit = self.config.get("steps_per_unit", float, 1)
        self.__tolerance = self.config.get("tolerance", float, 1e-4)
Matias Guijarro's avatar
Matias Guijarro committed
654
655
656
657
658
659
660
661
662
663
664
665
666
        if velocity:
            if self.controller.axis_settings.config_setting["velocity"]:
                self.__config_velocity = self.config.get("velocity", float)
        if acceleration:
            if self.controller.axis_settings.config_setting["acceleration"]:
                self.__config_acceleration = self.config.get("acceleration", float)
        if limits:
            self.__config_low_limit = self.config.get("low_limit", float, float("-inf"))
            self.__config_high_limit = self.config.get(
                "high_limit", float, float("+inf")
            )
        if backlash:
            self.__config_backlash = self.config.get("backlash", float, 0)
667

668
669
    @property
    def steps_per_unit(self):
670
        """Current steps per unit (:obj:`float`)"""
671
        return self.__steps_per_unit
672

673
    @property
Matias Guijarro's avatar
Matias Guijarro committed
674
675
676
677
678
679
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
680
681
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
682
683
684
685
686
687
688
689
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

    @backlash.setter
    def backlash(self, backlash):
        self.settings.set("backlash", backlash)
690

691
692
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
693
        """Current Axis tolerance in dial units (:obj:`float`)"""
694
        return self.__tolerance
695

Matias Guijarro's avatar
Matias Guijarro committed
696
697
    @property
    def encoder(self):
698
699
700
701
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
702
        return self.__encoder
703

704
705
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
706
707
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
708

709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
    @property
    @lazy_init
    def offset(self):
        """Current offset in user units (:obj:`float`)"""
        offset = self.settings.get("offset")
        if offset is None:
            return 0
        return offset

    @offset.setter
    def offset(self, new_offset):
        if self.no_offset:
            raise RuntimeError(
                f"{self.name}: cannot change offset, axis has 'no offset' flag"
            )
        self.__do_set_position(offset=new_offset)

    @property
    @lazy_init
    def sign(self):
        """Current motor sign (:obj:`int`) [-1, 1]"""
        sign = self.settings.get("sign")
        if sign is None:
            return 1
        return sign

    @sign.setter
    def sign(self, new_sign):
        new_sign = float(
            new_sign
        )  # works both with single float or numpy array of 1 element
        new_sign = math.copysign(1, new_sign)
        if new_sign != self.sign:
            if self.no_offset:
                raise RuntimeError(
                    f"{self.name}: cannot change sign, axis has 'no offset' flag"
                )
            self.settings.set("sign", new_sign)
            # update pos with new sign, offset stays the same
            # user pos is **not preserved** (like spec)
            self.position = self.dial2user(self.dial)

751
    def set_setting(self, *args):
752
        """Sets the given settings"""
753
754
755
        self.settings.set(*args)

    def get_setting(self, *args):
756
        """Return the values for the given settings"""
757
758
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
759
    def has_tag(self, tag):
760
761
762
763
764
765
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

766
        Return:
767
768
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
769
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
770
771
772
773
774
775
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

776
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
777
    def on(self):
778
        """Turns the axis on"""
779
780
781
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
782
        self.__controller.set_on(self)
783
        state = self.__controller.state(self)
784
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
785

786
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
787
    def off(self):
788
        """Turns the axis off"""
789
790
791
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
792
793
        self.__controller.set_off(self)
        state = self.__controller.state(self)
794
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
795

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
796
797
798
799
800
801
802
803
804
805
806
    @property
    @lazy_init
    def _set_position(self):
        sp = self.settings.get("_set_position")
        if sp is not None:
            return sp
        position = self.position
        self._set_position = position
        return position

    @_set_position.setter
807
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
808
    def _set_position(self, new_set_pos):
809
810
811
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
812
        self.settings.set("_set_position", new_set_pos)
813

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
814
    @property
815
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
816
817
    def measured_position(self):
        """
818
        Return the encoder value in user units.
819

820
        Return:
821
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
822
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
823
        return self.dial2user(self.dial_measured_position)
824

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
825
    @property
826
    @lazy_init
827
    def dial_measured_position(self):
828
        """
829
        Return the dial encoder position.
830

831
        Return:
832
            float: dial encoder position
833
        """
834
835
836
837
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
838

839
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
840
        user_pos = self.position
841
        old_dial = self.dial
842

843
844
845
846
        # Send the new value in motor units to the controller
        # and read back the (atomically) reported position
        new_hw = new_dial * self.steps_per_unit
        hw_pos = self.__controller.set_position(self, new_hw)
847

848
849
        dial_pos = hw_pos / self.steps_per_unit
        self.settings.set("dial_position", dial_pos)
850

851
852
853
854
855
856
        if self.no_offset:
            self.__do_set_position(dial_pos, offset=0)
        else:
            # set user pos, will recalculate offset
            # according to new dial
            self.__do_set_position(user_pos)
857

858
859
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
860
    @property
861
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
862
    def dial(self):
863
        """
864
        Return current dial position, or set dial
865

866
        Return:
867
            float: current dial position (dimensionless)
868
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
869
870
871
872
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
873

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
874
875
876
    @dial.setter
    @lazy_init
    def dial(self, new_dial):
877
        if self.is_moving:
878
879
880
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
881
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
882
883
884
        old_dial = self.dial
        new_dial = self.__do_set_dial(new_dial)
        lprint(f"'{self.name}` dial position reset from {old_dial} to {new_dial}")
885

886
887
888
889
890
891
    def __do_set_position(self, new_pos=None, offset=None):
        dial = self.dial
        curr_offset = self.offset
        if offset is None:
            # calc offset
            offset = new_pos - self.sign * dial
892
893
894
895
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
896
897
898
899
900
901
902
        if math.isclose(offset, 0):
            offset = 0
        if not math.isclose(curr_offset, offset):
            self.settings.set("offset", offset)
        if new_pos is None:
            # calc pos from offset
            new_pos = self.sign * dial + offset
903
904
905
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
906
907
908
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed
909

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
910
    @property
911
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
912
    def position(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
913
        """
914
        Return current user position, or set new user position
915

916
        Return:
917
            float: current user position (user units)
Cyril Guilloud's avatar
Cyril Guilloud committed
918
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
919
920
921
922
        pos = self.settings.get("position")
        if pos is None:
            pos = self.dial2user(self.dial)
        return pos
923

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
924
925
926
    @position.setter
    @lazy_init
    def position(self, new_pos):
927
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)
928
        if self.is_moving:
929
930
931
            raise RuntimeError(
                "%s: can't set axis user position " "while moving" % self.name
            )
932
        new_pos = float(new_pos)  # accepts both float or numpy array of 1 element
933
934
935
936
937
938
939
        curr_pos = self.position
        if self.no_offset:
            self.dial = new_pos
        if self.__do_set_position(new_pos):
            lprint(
                f"'{self.name}` position reset from {curr_pos} to {new_pos} (sign: {self.sign}, offset: {self.offset})"
            )
940

Vincent Michel's avatar
Vincent Michel committed
941
    @lazy_init
942
    def _update_dial(self, update_user=True):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
943
        dial_pos = self._hw_position
944
        self.settings.set("dial_position", dial_pos)
945
946
        if update_user:
            user_pos = self.dial2user(dial_pos, self.offset)
947
            self.settings.set("position", user_pos)
948
949
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
950
    @property
951
    @lazy_init
Matias's avatar
Matias committed
952
953
    def _hw_position(self):
        try:
954
            curr_pos = self.__controller.read_position(self) / self.steps_per_unit
Matias's avatar
Matias committed
955
956
957
958
959
960
        except NotImplementedError:
            # this controller does not have a 'position'
            # (e.g like some piezo controllers)
            curr_pos = 0
        return curr_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
961
    @property
962
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
963
    def state(self):
Jose Tiago Macara Coutinho's avatar