axis.py 69.2 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
79
80
    def move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
        polling_time=DEFAULT_POLLING_TIME,
    ):
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
134
135
136
                    move_func = "_handle_move"
                task = gevent.spawn(
                    getattr(motion.axis, move_func), motion, polling_time
                )
137
138
                monitor_move[motion] = task
        try:
139
            gevent.joinall(monitor_move.values(), raise_error=True)
140
141
        finally:
            # update the last motor state
Vincent Michel's avatar
Vincent Michel committed
142
            for motion, task in monitor_move.items():
143
144
                try:
                    motion.last_state = task.get(block=False)
145
                except BaseException:
146
                    pass
147
148

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

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

170
    @protect_from_one_kill
171
172
    def _do_backlash_move(self, motions_dict, polling_time):
        backlash_move = []
Vincent Michel's avatar
Vincent Michel committed
173
        for controller, motions in motions_dict.items():
174
175
            for motion in motions:
                if motion.backlash:
176
177
178
179
180
                    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
                        )
181
182
183
184
185
186
187
188
189
190
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
                    backlash_move.append(
                        gevent.spawn(
                            motion.axis._backlash_move, backlash_motion, polling_time
                        )
                    )
191
        gevent.joinall(backlash_move)
192
        gevent.joinall(backlash_move, raise_error=True)
193

194
195
196
197
198
199
200
201
202
    def _move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func,
        started_event,
        polling_time,
    ):
203
        # Set axis moving state
Vincent Michel's avatar
Vincent Michel committed
204
        for motions in motions_dict.values():
205
            for motion in motions:
206
                motion.last_state = None
207
                motion.axis._set_moving_state()
208

Vincent Michel's avatar
Vincent Michel committed
209
                for _, chan in motion.axis._beacon_channels.items():
210
                    chan.unregister_callback(chan._setting_update_cb)
211
        with capture_exceptions(raise_index=0) as capture:
212
213
            try:
                # Spawn start motion for all controllers
214
215
                start = [
                    gevent.spawn(start_motion, controller, motions)
Vincent Michel's avatar
Vincent Michel committed
216
                    for controller, motions in motions_dict.items()
217
                ]
218

219
                # Wait for the controllers to be started
220
                with capture():
221
222
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
223
                    gevent.joinall(start)
224
225
226
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
227

228
229
                    self._stop_wait(motions_dict, capture)
                    return
230

231
232
233
234
235
236
237
                # 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
238
                with capture():
239
240
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
241
242
243
244
245
246
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
247
248
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
249
250
251
252
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
253
                reset_setpos = capture.failed or self._user_stopped
254

255
256
257
258
                # cleanup
                # -------
                # update final state ; in case of exception
                # state is set to FAULT
Vincent Michel's avatar
Vincent Michel committed
259
                for motions in motions_dict.values():
260
                    for motion in motions:
261
262
263
264
                        state = motion.last_state
                        if state is not None:
                            continue

265
                        with capture():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
266
                            state = motion.axis.hw_state
267
268
269
                        if state is None:
                            state = AxisState("FAULT")
                        # update state and update dial pos.
270
271
                        with capture():
                            motion.axis._update_settings(state)
272
273
274
275
276
277
278
279
280

                # 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
281
                if len(motions_dict) == 1:
Vincent Michel's avatar
Vincent Michel committed
282
                    motion = motions_dict[list(motions_dict.keys()).pop()][0]
283
                    if motion.type == "jog":
284
                        reset_setpos = False
285
286
287
288
                        motion.axis._jog_cleanup(
                            motion.saved_velocity, motion.reset_position
                        )
                    elif motion.type == "homing":
289
                        reset_setpos = True
290
                    elif motion.type == "limit_search":
291
292
                        reset_setpos = True
                if reset_setpos:
293
                    with capture():
Vincent Michel's avatar
Vincent Michel committed
294
                        for motions in motions_dict.values():
295
                            for motion in motions:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
296
                                motion.axis._set_position = motion.axis.position
297
298
                                event.send(motion.axis, "sync_hard")

Vincent Michel's avatar
Vincent Michel committed
299
                for motions in motions_dict.values():
300
301
302
303
                    for motion in motions:
                        with capture():
                            motion.axis._Axis__execute_post_move_hook([motion])

Vincent Michel's avatar
Vincent Michel committed
304
                        for _, chan in motion.axis._beacon_channels.items():
305
306
307
308
309
                            chan.register_callback(chan._setting_update_cb)

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

Cyril Guilloud's avatar
Cyril Guilloud committed
311

312
class Modulo:
313
314
315
316
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
317
        dial_pos = axis.dial
318
        axis._Axis__do_set_dial(dial_pos % self.modulo)
319
320


321
class Motion:
322
323
324
325
326
327
328
329
    """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
330

331
332
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
333
    """
Matias Guijarro's avatar
Matias Guijarro committed
334

335
336
337
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
338
        self.__axis = axis
339
        self.__type = motion_type
340
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
341
342
343
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
344

Matias Guijarro's avatar
Matias Guijarro committed
345
346
    @property
    def axis(self):
347
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
348
        return self.__axis
349

350
351
352
353
    @property
    def type(self):
        return self.__type

354
355
356
357
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
358
359
360
361
362
363
            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

364
365
366
367
368
369
370
371
372
373
374
        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
375

376
377
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
378

379
380
381
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
382

Matias Guijarro's avatar
Matias Guijarro committed
383
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
384
385
386
387
388
389
390
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
391
392
393
394
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

395
396
397
398
399
400
401
    @property
    def axis(self):
        return self.__axis

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

403
404
    @property
    def events_positions(self):
405
        return self._events_positions
406

407
408
    @events_positions.setter
    def events_positions(self, events):
409
        self._events_positions = events
410

411
412
413
    def has_events(self):
        return self._events_positions.size

414
415
416
417
418
419
420
    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
        """
421
422
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
423
        pvt = numpy.copy(self.__pvt)
424
425
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
426
        new_obj = self.__class__(self.axis, pvt)
427
        pattern_evts = numpy.copy(self._events_positions)
428
429
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
430
        new_obj._events_positions = pattern_evts
431
        return new_obj
432
433


434
class CyclicTrajectory(Trajectory):
435
436
437
438
439
440
441
442
    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)
443
444
        self.nb_cycles = nb_cycles
        self.origin = origin
445
446
447
448
449

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

450
451
452
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
453

454
455
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
456
457
        self._events_positions = values

458
459
460
461
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
462
463
464
465
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
466
467
468

    @property
    def pvt(self):
469
        """Return the full PVT table. Positions are absolute"""
470
471
472
473
474
475
476
477
478
479
480
481
482
483
        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
484
        for cycle in range(self.nb_cycles):
485
            start = cycle_size * cycle + offset
486
487
            end = start + cycle_size
            pvt[start:end] = raw_pvt
488
489
490
491
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
492
493

        if self.is_closed:
494
495
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
496
497
498

        return pvt

499
500
501
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
502
        time_offset = 0.0
503
        last_time = self.pvt_pattern["time"][-1]
504
        nb_pattern_evts = len(pattern_evts)
505
506
507
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
508
        for i in range(self.nb_cycles):
509
510
511
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
512
            sub_evts[:] = pattern_evts
513
            sub_evts["time"] += time_offset
514
515
516
            time_offset += last_time
        return all_events

517
518
519
520
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
521
522
523
524
        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
525
526


527
528
529
530
531
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
532

533
    return func_wrapper
534

Matias Guijarro's avatar
Matias Guijarro committed
535

536
@with_custom_members
537
class Axis:
538
539
540
541
542
543
544
    """
    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
545
546
547
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
548
        self.__settings = AxisSettings(self)
Matias Guijarro's avatar
Matias Guijarro committed
549
        self.__move_done = gevent.event.Event()
550
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
551
        self.__move_done.set()
552
        self.__move_done_callback.set()
553
554
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
555
            hook._add_axis(self)
556
557
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
558
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
559
        self.__init_config_properties()
560
        self._group_move = GroupMove()
561
        self._beacon_channels = dict()
562
563
564
565
566
        self._move_stop_channel = Channel(
            "axis.%s.move_stop" % self.name,
            default_value=False,
            callback=self._external_stop,
        )
567
        self._lock = gevent.lock.Semaphore()
568
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
569

570
571
572
573
574
575
576
577
578
579
580
581
582
        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)
583
        self._unit = self.config.get("unit", str, None)
584
        global_map.register(self, parents_list=["axes", controller])
585

586
    def __close__(self):
587
588
589
590
591
592
593
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

594
595
596
597
598
599
600
601
    @property
    def no_offset(self):
        return self.__no_offset

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

602
603
604
605
606
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
607
608
    @property
    def name(self):
609
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
610
611
612
613
        return self.__name

    @property
    def controller(self):
614
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
615
616
617
618
        return self.__controller

    @property
    def config(self):
619
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
620
621
622
623
        return self.__config

    @property
    def settings(self):
624
625
626
627
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
628
629
630
631
        return self.__settings

    @property
    def is_moving(self):
632
633
634
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
635
636
        return not self.__move_done.is_set()

Matias Guijarro's avatar
Matias Guijarro committed
637
638
639
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
640
641
        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
642
643
644
645
646
647
648
649
650
651
652
653
654
        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)
655

656
657
    @property
    def steps_per_unit(self):
658
        """Current steps per unit (:obj:`float`)"""
659
        return self.__steps_per_unit
660

661
    @property
Matias Guijarro's avatar
Matias Guijarro committed
662
663
664
665
666
667
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
668
669
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
670
671
672
673
674
675
676
677
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

679
680
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
681
        """Current Axis tolerance in dial units (:obj:`float`)"""
682
        return self.__tolerance
683

Matias Guijarro's avatar
Matias Guijarro committed
684
685
    @property
    def encoder(self):
686
687
688
689
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
690
        return self.__encoder
691

692
693
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
694
695
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
696

697
698
699
700
701
702
703
704
705
706
707
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
    @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)

739
    def set_setting(self, *args):
740
        """Sets the given settings"""
741
742
743
        self.settings.set(*args)

    def get_setting(self, *args):
744
        """Return the values for the given settings"""
745
746
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
747
    def has_tag(self, tag):
748
749
750
751
752
753
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

754
        Return:
755
756
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
757
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
758
759
760
761
762
763
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

764
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
765
    def on(self):
766
        """Turns the axis on"""
767
768
769
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
770
        self.__controller.set_on(self)
771
        state = self.__controller.state(self)
772
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
773

774
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
775
    def off(self):
776
        """Turns the axis off"""
777
778
779
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
780
781
        self.__controller.set_off(self)
        state = self.__controller.state(self)
782
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
783

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
784
785
786
787
788
789
790
791
792
793
794
    @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
795
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
796
    def _set_position(self, new_set_pos):
797
798
799
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
800
        self.settings.set("_set_position", new_set_pos)
801

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
802
    @property
803
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
804
805
    def measured_position(self):
        """
806
        Return the encoder value in user units.
807

808
        Return:
809
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
810
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
811
        return self.dial2user(self.dial_measured_position)
812

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
813
    @property
814
    @lazy_init
815
    def dial_measured_position(self):
816
        """
817
        Return the dial encoder position.
818

819
        Return:
820
            float: dial encoder position
821
        """
822
823
824
825
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
826

827
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
828
        user_pos = self.position
829
        old_dial = self.dial
830

831
832
833
834
        # 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)
835

836
837
        dial_pos = hw_pos / self.steps_per_unit
        self.settings.set("dial_position", dial_pos)
838

839
840
841
842
843
844
        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)
845

846
847
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
848
    @property
849
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
850
    def dial(self):
851
        """
852
        Return current dial position, or set dial
853

854
        Return:
855
            float: current dial position (dimensionless)
856
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
857
858
859
860
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
861

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
862
863
864
    @dial.setter
    @lazy_init
    def dial(self, new_dial):
865
        if self.is_moving:
866
867
868
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
869
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
870
871
872
        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}")
873

874
875
876
877
878
879
    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
880
881
882
883
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
884
885
886
887
888
889
890
        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
891
892
893
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
894
895
896
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed
897

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
898
    @property
899
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
900
    def position(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
901
        """
902
        Return current user position, or set new user position
903

904
        Return:
905
            float: current user position (user units)
Cyril Guilloud's avatar
Cyril Guilloud committed
906
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
907
908
909
910
        pos = self.settings.get("position")
        if pos is None:
            pos = self.dial2user(self.dial)
        return pos
911

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
912
913
914
    @position.setter
    @lazy_init
    def position(self, new_pos):
915
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)
916
        if self.is_moving:
917
918
919
            raise RuntimeError(
                "%s: can't set axis user position " "while moving" % self.name
            )
920
        new_pos = float(new_pos)  # accepts both float or numpy array of 1 element
921
922
923
924
925
926
927
        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})"
            )
928

Vincent Michel's avatar
Vincent Michel committed
929
    @lazy_init
930
    def _update_dial(self, update_user=True):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
931
        dial_pos = self._hw_position
932
        self.settings.set("dial_position", dial_pos)
933
934
        if update_user:
            user_pos = self.dial2user(dial_pos, self.offset)
935
            self.settings.set("position", user_pos)
936
937
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
938
    @property
939
    @lazy_init
Matias's avatar
Matias committed
940
941
    def _hw_position(self):
        try:
942
            curr_pos = self.__controller.read_position(self) / self.steps_per_unit
Matias's avatar
Matias committed
943
944
945
946
947
948
        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
949
    @property
950
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
951
    def state(self):
952
        """
953
        Return the axis state
954
955
956
957

        Keyword Args:
            read_hw (bool): read from hardware [default: False]

958
        Return:
959
960
            AxisState: axis state
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
961
962
963
        if self.is_moving:
            return AxisState("MOVING")
        state = self.settings.get("state")
964
965
        if state is None:
            # really read from hw
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
966
            state = self.hw_state
967
        return state
Matias Guijarro's avatar
Matias Guijarro committed
968

Sebastien Petitdemange's avatar