axis.py 77 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
"""
Axis related classes (:class:`~bliss.common.axis.Axis`, \
10
11
:class:`~bliss.common.axis.AxisState`, :class:`~bliss.common.axis.Motion`
and :class:`~bliss.common.axis.GroupMove`)
12
"""
13
from bliss import global_map
14
from bliss.common.cleanup import capture_exceptions
15
16
from bliss.common.motor_config import StaticConfig
from bliss.common.motor_settings import AxisSettings
17
from bliss.common import event
18
from bliss.common.greenlet_utils import protect_from_one_kill
19
from bliss.common.utils import with_custom_members, safe_get
20
from bliss.config.channels import Channel
21
from bliss.common.logtools import log_debug, lprint, lprint_disable
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
22
from bliss.common.utils import rounder
23
from bliss.common.utils import autocomplete_property
24

25
import enum
26
import gevent
27
import re
28
import sys
29
import math
30
import functools
31
import collections
32
import numpy
33
from unittest import mock
34
import warnings
35
36

warnings.simplefilter("once", DeprecationWarning)
37

38

39
#: Default polling time
40
DEFAULT_POLLING_TIME = 0.02
Matias Guijarro's avatar
Matias Guijarro committed
41

42

43
class GroupMove:
44
    def __init__(self, parent=None):
45
46
        self.parent = parent
        self._move_task = None
47
48
49
        self._motions_dict = dict()
        self._stop_motion = None
        self._user_stopped = False
50
51
52
53
54
55
56
57

    # Public API

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

58
59
60
    def move(
        self,
        motions_dict,
61
        prepare_motion,
62
63
64
65
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
66
        polling_time=None,
67
    ):
68
69
70
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
        self._user_stopped = False
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

        hooks = collections.defaultdict(list)
        executed_hooks = dict()
        axes = set()
        hooked_axes = set()
        for motions in motions_dict.values():
            for motion in motions:
                axis = motion.axis
                axes.add(axis)

                # group motion hooks
                for hook in axis.motion_hooks:
                    hooks[hook].append(motion)

        with capture_exceptions(raise_index=0) as capture:
            for hook, motions in hooks.items():
                hooked_axes.union({m.axis for m in motions})

                with capture():
                    hook._init()
                    hook.pre_move(motions)

                executed_hooks[hook] = motions

                if capture.failed:
                    # something wrong happened with this hook:
                    # let's call post_move for all executed hooks so far
                    # (including this one), in reversed order
                    for hook, motions in reversed(list(executed_hooks.items())):
                        with capture():
                            hook.post_move(motions)
                    return

        # now check if axes are ready ;
        # the check happens after pre_move hooks execution,
        # some axes can **become** ready because of the hook
        with capture_exceptions(raise_index=0) as capture:
            for axis in axes:
                with capture():
                    axis._check_ready()
                if capture.failed:
                    # this axis _check_ready() had a problem:
                    # need to ensure post_move hook is called,
                    # if the pre_move was executed
                    for hook in reversed(axis.motion_hooks):
                        motions = executed_hooks.get(hook)
                        if motions:
                            with capture():
                                hook.post_move(motions)
                    return

        for controller, motions in motions_dict.items():
            if prepare_motion is not None:
                prepare_motion(controller, motions)
125

126
            for motion_obj in motions:
127
128
129
130
                target_pos = motion_obj.user_target_pos
                if target_pos is not None and not isinstance(target_pos, str):
                    motion_obj.axis._set_position = target_pos

131
132
133
134
                msg = motion_obj.user_msg
                if msg:
                    lprint(msg)

135
        started = gevent.event.Event()
136

137
        self._move_task = gevent.spawn(
138
139
140
141
142
143
144
145
            self._move,
            motions_dict,
            start_motion,
            stop_motion,
            move_func,
            started,
            polling_time,
        )
146

147
148
149
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
150
        except BaseException:
151
152
            self.stop()
            raise
153
154
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
155
            self.wait()
156
157
158

    def wait(self):
        if self._move_task is not None:
159
160
            try:
                self._move_task.get()
161
            except BaseException:
162
163
                self.stop()
                raise
164
165

    def stop(self, wait=True):
166
167
168
169
170
171
        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()
172
173
174

    # Internal methods

175
    def _monitor_move(self, motions_dict, move_func, polling_time):
176
        monitor_move = dict()
Vincent Michel's avatar
Vincent Michel committed
177
        for controller, motions in motions_dict.items():
178
            for motion in motions:
179
                if move_func is None:
180
                    move_func = "_handle_move"
181
182
183
                axis_polling_time = (
                    motion.axis._polling_time if polling_time is None else polling_time
                )
184
                task = gevent.spawn(
185
                    getattr(motion.axis, move_func), motion, axis_polling_time
186
                )
187
188
                monitor_move[motion] = task
        try:
189
            gevent.joinall(monitor_move.values(), raise_error=True)
190
191
        finally:
            # update the last motor state
Vincent Michel's avatar
Vincent Michel committed
192
            for motion, task in monitor_move.items():
193
194
                try:
                    motion.last_state = task.get(block=False)
195
                except BaseException:
196
                    pass
197
198

    def _stop_move(self, motions_dict, stop_motion):
199
        self._user_stopped = True
200
        stop = []
Vincent Michel's avatar
Vincent Michel committed
201
        for controller, motions in motions_dict.items():
202
            stop.append(gevent.spawn(stop_motion, controller, motions))
203
204
205
206
        # Raise exception if any, when all the stop tasks are finished
        for task in gevent.joinall(stop):
            task.get()

207
208
    def _stop_wait(self, motions_dict, exception_capture):
        stop_wait = []
Vincent Michel's avatar
Vincent Michel committed
209
        for controller, motions in motions_dict.items():
210
211
212
            for motion in motions:
                stop_wait.append(gevent.spawn(motion.axis._move_loop))
        gevent.joinall(stop_wait)
213
        task_index = 0
Vincent Michel's avatar
Vincent Michel committed
214
        for controller, motions in motions_dict.items():
215
216
217
218
            for motion in motions:
                with exception_capture():
                    motion.last_state = stop_wait[task_index].get()
                task_index += 1
219

220
    @protect_from_one_kill
221
222
    def _do_backlash_move(self, motions_dict, polling_time):
        backlash_move = []
Vincent Michel's avatar
Vincent Michel committed
223
        for controller, motions in motions_dict.items():
224
225
            for motion in motions:
                if motion.backlash:
226
227
228
229
230
                    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
                        )
231
232
233
234
235
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
236
237
238
239
240
241
                    axis_polling_time = (
                        motion.axis._polling_time
                        if polling_time is None
                        else polling_time
                    )

242
243
                    backlash_move.append(
                        gevent.spawn(
244
245
246
                            motion.axis._backlash_move,
                            backlash_motion,
                            axis_polling_time,
247
248
                        )
                    )
249
        gevent.joinall(backlash_move)
250
        gevent.joinall(backlash_move, raise_error=True)
251

252
253
254
255
256
257
258
259
260
    def _move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func,
        started_event,
        polling_time,
    ):
261
        # Set axis moving state
Vincent Michel's avatar
Vincent Michel committed
262
        for motions in motions_dict.values():
263
            for motion in motions:
264
                motion.last_state = None
265
                motion.axis._set_moving_state()
266

Vincent Michel's avatar
Vincent Michel committed
267
                for _, chan in motion.axis._beacon_channels.items():
268
                    chan.unregister_callback(chan._setting_update_cb)
269
        with capture_exceptions(raise_index=0) as capture:
270
271
            try:
                # Spawn start motion for all controllers
272
273
                start = [
                    gevent.spawn(start_motion, controller, motions)
Vincent Michel's avatar
Vincent Michel committed
274
                    for controller, motions in motions_dict.items()
275
                ]
276

277
                # Wait for the controllers to be started
278
                with capture():
279
280
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
281
                    gevent.joinall(start)
282
283
284
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
285

286
287
                    self._stop_wait(motions_dict, capture)
                    return
288

289
290
291
292
293
294
295
                # 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
296
                with capture():
297
298
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
299
300
301
302
303
304
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
305
306
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
307
308
309
310
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
311
                reset_setpos = capture.failed or self._user_stopped
312

313
314
315
316
                # cleanup
                # -------
                # update final state ; in case of exception
                # state is set to FAULT
Vincent Michel's avatar
Vincent Michel committed
317
                for motions in motions_dict.values():
318
                    for motion in motions:
319
320
321
322
                        state = motion.last_state
                        if state is not None:
                            continue

323
                        with capture():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
324
                            state = motion.axis.hw_state
325
326
327
                        if state is None:
                            state = AxisState("FAULT")
                        # update state and update dial pos.
328
329
                        with capture():
                            motion.axis._update_settings(state)
330
331
332
333
334
335
336
337
338

                # 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
339
                if len(motions_dict) == 1:
Vincent Michel's avatar
Vincent Michel committed
340
                    motion = motions_dict[list(motions_dict.keys()).pop()][0]
341
                    if motion.type == "jog":
342
                        reset_setpos = False
343
344
345
346
                        motion.axis._jog_cleanup(
                            motion.saved_velocity, motion.reset_position
                        )
                    elif motion.type == "homing":
347
                        reset_setpos = True
348
                    elif motion.type == "limit_search":
349
350
                        reset_setpos = True
                if reset_setpos:
351
                    with capture():
Vincent Michel's avatar
Vincent Michel committed
352
                        for motions in motions_dict.values():
353
                            for motion in motions:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
354
                                motion.axis._set_position = motion.axis.position
355
356
                                event.send(motion.axis, "sync_hard")

357
                hooks = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
358
                for motions in motions_dict.values():
359
                    for motion in motions:
360
361
362
363
364
                        axis = motion.axis

                        # group motion hooks
                        for hook in axis.motion_hooks:
                            hooks[hook].append(motion)
365

366
367
                        # set move done
                        for _, chan in axis._beacon_channels.items():
368
369
370
                            chan.register_callback(chan._setting_update_cb)

                        motion.axis._set_move_done()
371

372
373
374
375
376
377
378
                if self._user_stopped:
                    lprint("")
                    for motion in motions:
                        _axis = motion.axis
                        _axis_pos = safe_get(_axis, "position", on_error="!ERR")
                        lprint(f"Axis {_axis.name} stopped at position {_axis_pos}")

379
380
381
382
383
384
385
                try:
                    if self.parent:
                        event.send(self.parent, "move_done", True)
                finally:
                    for hook, motions in reversed(list(hooks.items())):
                        with capture():
                            hook.post_move(motions)
386

Cyril Guilloud's avatar
Cyril Guilloud committed
387

388
class Modulo:
389
390
391
392
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
393
        dial_pos = axis.dial
394
        axis._Axis__do_set_dial(dial_pos % self.modulo)
395
396


397
class Motion:
398
399
400
401
402
403
404
405
    """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
406

407
408
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
409
    """
Matias Guijarro's avatar
Matias Guijarro committed
410

411
412
413
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
414
        self.__axis = axis
415
        self.__type = motion_type
416
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
417
418
419
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
420

Matias Guijarro's avatar
Matias Guijarro committed
421
422
    @property
    def axis(self):
423
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
424
        return self.__axis
425

426
427
428
429
    @property
    def type(self):
        return self.__type

430
431
432
433
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
434
            msg = (
435
                f"Moving {self.axis.name} from {start_} until it is stopped, at constant velocity in {'positive' if self.delta > 0 else 'negative'} direction: {abs(self.target_pos/self.axis.steps_per_unit)}\n"
Cyril Guilloud's avatar
Cyril Guilloud committed
436
437
438
439
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

440
441
442
443
444
445
446
447
448
449
450
        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
451

452
453
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
454

455
456
457
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
458

Matias Guijarro's avatar
Matias Guijarro committed
459
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
460
461
462
463
464
465
466
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
467
468
469
470
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

471
472
473
474
475
476
477
    @property
    def axis(self):
        return self.__axis

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

479
480
    @property
    def events_positions(self):
481
        return self._events_positions
482

483
484
    @events_positions.setter
    def events_positions(self, events):
485
        self._events_positions = events
486

487
488
489
    def has_events(self):
        return self._events_positions.size

490
491
492
493
494
495
496
    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
        """
497
498
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
499
        pvt = numpy.copy(self.__pvt)
500
501
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
502
        new_obj = self.__class__(self.axis, pvt)
503
        pattern_evts = numpy.copy(self._events_positions)
504
505
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
506
        new_obj._events_positions = pattern_evts
507
        return new_obj
508
509


510
class CyclicTrajectory(Trajectory):
511
512
513
514
515
516
517
518
    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)
519
520
        self.nb_cycles = nb_cycles
        self.origin = origin
521
522
523
524
525

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

526
527
528
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
529

530
531
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
532
533
        self._events_positions = values

534
535
536
537
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
538
539
540
541
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
542
543
544

    @property
    def pvt(self):
545
        """Return the full PVT table. Positions are absolute"""
546
547
548
549
550
551
552
553
554
555
556
557
558
559
        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
560
        for cycle in range(self.nb_cycles):
561
            start = cycle_size * cycle + offset
562
563
            end = start + cycle_size
            pvt[start:end] = raw_pvt
564
565
566
567
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
568
569

        if self.is_closed:
570
571
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
572
573
574

        return pvt

575
576
577
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
578
        time_offset = 0.0
579
        last_time = self.pvt_pattern["time"][-1]
580
        nb_pattern_evts = len(pattern_evts)
581
582
583
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
584
        for i in range(self.nb_cycles):
585
586
587
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
588
            sub_evts[:] = pattern_evts
589
            sub_evts["time"] += time_offset
590
591
592
            time_offset += last_time
        return all_events

593
594
595
596
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
597
598
599
600
        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
601
602


603
604
605
606
607
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
608

609
    return func_wrapper
610

Matias Guijarro's avatar
Matias Guijarro committed
611

612
@with_custom_members
613
class Axis:
614
615
616
617
618
619
620
    """
    Bliss motor axis

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

621
622
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
623
624
625
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
626
        self.__settings = AxisSettings(self)
Matias Guijarro's avatar
Matias Guijarro committed
627
        self.__move_done = gevent.event.Event()
628
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
629
        self.__move_done.set()
630
        self.__move_done_callback.set()
631
632
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
633
            hook._add_axis(self)
634
635
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
636
637
        if self.__encoder is not None:
            self.__encoder.axis = self
638
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
639
        self.__init_config_properties()
640
        self._group_move = GroupMove()
641
        self._beacon_channels = dict()
642
        self._move_stop_channel = Channel(
643
            f"axis.{self.name}.move_stop",
644
645
646
            default_value=False,
            callback=self._external_stop,
        )
647
648
649
650
651
        self._jog_velocity_channel = Channel(
            f"axis.{self.name}.change_jog_velocity",
            default_value=None,
            callback=self._set_jog_velocity,
        )
652
        self._lock = gevent.lock.Semaphore()
653
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
654

655
656
657
658
659
660
661
662
663
664
665
666
667
        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)
668
        self._unit = self.config.get("unit", str, None)
669
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
670
        global_map.register(self, parents_list=["axes", controller])
671

672
    def __close__(self):
673
674
675
676
677
678
679
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

680
681
682
683
684
685
686
687
    @property
    def no_offset(self):
        return self.__no_offset

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

688
689
690
691
692
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
693
694
    @property
    def name(self):
695
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
696
697
        return self.__name

698
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
699
    def controller(self):
700
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
701
702
703
704
        return self.__controller

    @property
    def config(self):
705
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
706
707
708
709
        return self.__config

    @property
    def settings(self):
710
711
712
713
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
714
715
716
717
        return self.__settings

    @property
    def is_moving(self):
718
719
720
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
721
722
        return not self.__move_done.is_set()

Matias Guijarro's avatar
Matias Guijarro committed
723
724
725
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
726
727
        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
728
729
730
        if velocity:
            if self.controller.axis_settings.config_setting["velocity"]:
                self.__config_velocity = self.config.get("velocity", float)
731
732
733
734
            if self.controller.axis_settings.config_setting["jog_velocity"]:
                self.__config_jog_velocity = self.config.get(
                    "jog_velocity", float, self.__config_velocity
                )
Matias Guijarro's avatar
Matias Guijarro committed
735
736
737
738
739
740
741
742
743
744
        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)
745

746
747
    @property
    def steps_per_unit(self):
748
        """Current steps per unit (:obj:`float`)"""
749
        return self.__steps_per_unit
750

751
    @property
Matias Guijarro's avatar
Matias Guijarro committed
752
753
754
755
756
757
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
758
759
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
760
761
762
763
764
765
766
767
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

769
770
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
771
        """Current Axis tolerance in dial units (:obj:`float`)"""
772
        return self.__tolerance
773

Matias Guijarro's avatar
Matias Guijarro committed
774
775
    @property
    def encoder(self):
776
777
778
779
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
780
        return self.__encoder
781

782
783
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
784
785
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
786

787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
    @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)

829
    def set_setting(self, *args):
830
        """Sets the given settings"""
831
832
833
        self.settings.set(*args)

    def get_setting(self, *args):
834
        """Return the values for the given settings"""
835
836
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
837
    def has_tag(self, tag):
838
839
840
841
842
843
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

844
        Return:
845
846
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
847
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
848
849
850
851
852
853
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

854
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
855
    def on(self):
856
        """Turns the axis on"""
857
858
859
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
860
        self.__controller.set_on(self)
861
        state = self.__controller.state(self)
862
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
863

864
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
865
    def off(self):
866
        """Turns the axis off"""
867
868
869
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
870
871
        self.__controller.set_off(self)
        state = self.__controller.state(self)
872
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
873

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
874
875
876
877
878
879
880
881
882
883
884
885
    @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
    def _set_position(self, new_set_pos):
886
887
888
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
889
        self.settings.set("_set_position", new_set_pos)
890

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
891
    @property
892
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
893
894
    def measured_position(self):
        """
895
        Return the encoder value in user units.
896

897
        Return:
898
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
899
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
900
        return self.dial2user(self.dial_measured_position)
901

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
902
    @property
903
    @lazy_init
904
    def dial_measured_position(self):
905
        """
906
        Return the dial encoder position.
907

908
        Return:
909
            float: dial encoder position
910
        """
911
912
913
914
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
915

916
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
917
        user_pos = self.position
918
        old_dial = self.dial
919

920
921
922
923
924
925
926
927
928
        # Set the new dial on the encoder
        if self._read_position_mode == self.READ_POSITION_MODE.ENCODER:
            dial_pos = self.encoder.set(new_dial)
        else:
            # 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)
            dial_pos = hw_pos / self.steps_per_unit
929
        self.settings.set("dial_position", dial_pos)
930

931
932
933
934
935
936
        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)
937

938
939
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
940
    @property
941
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
942
    def dial(self):
943
        """
944
        Return current dial position, or set dial
945

946
        Return:
947
            float: current dial position (dimensionless)
948
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
949
950
951
952
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
953

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
954
955
    @dial.setter
    def dial(self, new_dial):
956
        if self.is_moving:
957
958
959
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
960
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
961
962
963
        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}")
964

965
966
967
968
969
970
    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
971
972
973
974
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
975
976
977
978
979
980
981
        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
982
983
984
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
985
986
987
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed