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

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

warnings.simplefilter("once", DeprecationWarning)
36

37

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

41

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

    # Public API

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

57
58
59
    def move(
        self,
        motions_dict,
60
        prepare_motion,
61
62
63
64
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
65
        polling_time=None,
66
    ):
67
68
69
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
        self._user_stopped = False
70
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
125
126
127
128

        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)
            for motion_obj in motions:
                msg = motion_obj.user_msg
                if msg:
                    lprint(msg)

129
        started = gevent.event.Event()
130

131
        self._move_task = gevent.spawn(
132
133
134
135
136
137
138
139
            self._move,
            motions_dict,
            start_motion,
            stop_motion,
            move_func,
            started,
            polling_time,
        )
140

141
142
143
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
144
        except BaseException:
145
146
            self.stop()
            raise
147
148
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
149
            self.wait()
150
151
152

    def wait(self):
        if self._move_task is not None:
153
154
            try:
                self._move_task.get()
155
            except BaseException:
156
157
                self.stop()
                raise
158
159

    def stop(self, wait=True):
160
161
162
163
164
165
        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()
166
167
168

    # Internal methods

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

    def _stop_move(self, motions_dict, stop_motion):
193
        self._user_stopped = True
194
        stop = []
Vincent Michel's avatar
Vincent Michel committed
195
        for controller, motions in motions_dict.items():
196
            stop.append(gevent.spawn(stop_motion, controller, motions))
197
198
199
200
        # Raise exception if any, when all the stop tasks are finished
        for task in gevent.joinall(stop):
            task.get()

201
202
    def _stop_wait(self, motions_dict, exception_capture):
        stop_wait = []
Vincent Michel's avatar
Vincent Michel committed
203
        for controller, motions in motions_dict.items():
204
205
206
            for motion in motions:
                stop_wait.append(gevent.spawn(motion.axis._move_loop))
        gevent.joinall(stop_wait)
207
        task_index = 0
Vincent Michel's avatar
Vincent Michel committed
208
        for controller, motions in motions_dict.items():
209
210
211
212
            for motion in motions:
                with exception_capture():
                    motion.last_state = stop_wait[task_index].get()
                task_index += 1
213

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

236
237
                    backlash_move.append(
                        gevent.spawn(
238
239
240
                            motion.axis._backlash_move,
                            backlash_motion,
                            axis_polling_time,
241
242
                        )
                    )
243
        gevent.joinall(backlash_move)
244
        gevent.joinall(backlash_move, raise_error=True)
245

246
247
248
249
250
251
252
253
254
    def _move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func,
        started_event,
        polling_time,
    ):
255
        # Set axis moving state
Vincent Michel's avatar
Vincent Michel committed
256
        for motions in motions_dict.values():
257
            for motion in motions:
258
                motion.last_state = None
259
                motion.axis._set_moving_state()
260

Vincent Michel's avatar
Vincent Michel committed
261
                for _, chan in motion.axis._beacon_channels.items():
262
                    chan.unregister_callback(chan._setting_update_cb)
263
        with capture_exceptions(raise_index=0) as capture:
264
265
            try:
                # Spawn start motion for all controllers
266
267
                start = [
                    gevent.spawn(start_motion, controller, motions)
Vincent Michel's avatar
Vincent Michel committed
268
                    for controller, motions in motions_dict.items()
269
                ]
270

271
                # Wait for the controllers to be started
272
                with capture():
273
274
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
275
                    gevent.joinall(start)
276
277
278
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
279

280
281
                    self._stop_wait(motions_dict, capture)
                    return
282

283
284
285
286
287
288
289
                # 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
290
                with capture():
291
292
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
293
294
295
296
297
298
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
299
300
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
301
302
303
304
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
305
                reset_setpos = capture.failed or self._user_stopped
306

307
308
309
310
                # cleanup
                # -------
                # update final state ; in case of exception
                # state is set to FAULT
Vincent Michel's avatar
Vincent Michel committed
311
                for motions in motions_dict.values():
312
                    for motion in motions:
313
314
315
316
                        state = motion.last_state
                        if state is not None:
                            continue

317
                        with capture():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
318
                            state = motion.axis.hw_state
319
320
321
                        if state is None:
                            state = AxisState("FAULT")
                        # update state and update dial pos.
322
323
                        with capture():
                            motion.axis._update_settings(state)
324
325
326
327
328
329
330
331
332

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

351
                hooks = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
352
                for motions in motions_dict.values():
353
                    for motion in motions:
354
355
356
357
358
                        axis = motion.axis

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

360
361
                        # set move done
                        for _, chan in axis._beacon_channels.items():
362
363
364
                            chan.register_callback(chan._setting_update_cb)

                        motion.axis._set_move_done()
365

366
367
368
369
370
371
372
                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}")

373
374
375
376
377
378
379
                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)
380

Cyril Guilloud's avatar
Cyril Guilloud committed
381

382
class Modulo:
383
384
385
386
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
387
        dial_pos = axis.dial
388
        axis._Axis__do_set_dial(dial_pos % self.modulo)
389
390


391
class Motion:
392
393
394
395
396
397
398
399
    """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
400

401
402
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
403
    """
Matias Guijarro's avatar
Matias Guijarro committed
404

405
406
407
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
408
        self.__axis = axis
409
        self.__type = motion_type
410
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
411
412
413
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
414

Matias Guijarro's avatar
Matias Guijarro committed
415
416
    @property
    def axis(self):
417
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
418
        return self.__axis
419

420
421
422
423
    @property
    def type(self):
        return self.__type

424
425
426
427
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
428
429
430
431
432
433
            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

434
435
436
437
438
439
440
441
442
443
444
        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
445

446
447
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
448

449
450
451
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
452

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

465
466
467
468
469
470
471
    @property
    def axis(self):
        return self.__axis

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

473
474
    @property
    def events_positions(self):
475
        return self._events_positions
476

477
478
    @events_positions.setter
    def events_positions(self, events):
479
        self._events_positions = events
480

481
482
483
    def has_events(self):
        return self._events_positions.size

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


504
class CyclicTrajectory(Trajectory):
505
506
507
508
509
510
511
512
    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)
513
514
        self.nb_cycles = nb_cycles
        self.origin = origin
515
516
517
518
519

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

520
521
522
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
523

524
525
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
526
527
        self._events_positions = values

528
529
530
531
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
532
533
534
535
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
536
537
538

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

        if self.is_closed:
564
565
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
566
567
568

        return pvt

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

587
588
589
590
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
591
592
593
594
        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
595
596


597
598
599
600
601
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
602

603
    return func_wrapper
604

Matias Guijarro's avatar
Matias Guijarro committed
605

606
@with_custom_members
607
class Axis:
608
609
610
611
612
613
614
    """
    Bliss motor axis

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

615
616
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
617
618
619
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
620
        self.__settings = AxisSettings(self)
Matias Guijarro's avatar
Matias Guijarro committed
621
        self.__move_done = gevent.event.Event()
622
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
623
        self.__move_done.set()
624
        self.__move_done_callback.set()
625
626
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
627
            hook._add_axis(self)
628
629
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
630
631
        if self.__encoder is not None:
            self.__encoder.axis = self
632
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
633
        self.__init_config_properties()
634
        self._group_move = GroupMove()
635
        self._beacon_channels = dict()
636
637
638
639
640
        self._move_stop_channel = Channel(
            "axis.%s.move_stop" % self.name,
            default_value=False,
            callback=self._external_stop,
        )
641
        self._lock = gevent.lock.Semaphore()
642
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
643

644
645
646
647
648
649
650
651
652
653
654
655
656
        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)
657
        self._unit = self.config.get("unit", str, None)
658
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
659
        global_map.register(self, parents_list=["axes", controller])
660

661
    def __close__(self):
662
663
664
665
666
667
668
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

669
670
671
672
673
674
675
676
    @property
    def no_offset(self):
        return self.__no_offset

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

677
678
679
680
681
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
682
683
    @property
    def name(self):
684
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
685
686
        return self.__name

687
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
688
    def controller(self):
689
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
690
691
692
693
        return self.__controller

    @property
    def config(self):
694
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
695
696
697
698
        return self.__config

    @property
    def settings(self):
699
700
701
702
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
703
704
705
706
        return self.__settings

    @property
    def is_moving(self):
707
708
709
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
710
711
        return not self.__move_done.is_set()

Matias Guijarro's avatar
Matias Guijarro committed
712
713
714
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
715
716
        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
717
718
719
720
721
722
723
724
725
726
727
728
729
        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)
730

731
732
    @property
    def steps_per_unit(self):
733
        """Current steps per unit (:obj:`float`)"""
734
        return self.__steps_per_unit
735

736
    @property
Matias Guijarro's avatar
Matias Guijarro committed
737
738
739
740
741
742
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
743
744
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
745
746
747
748
749
750
751
752
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

754
755
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
756
        """Current Axis tolerance in dial units (:obj:`float`)"""
757
        return self.__tolerance
758

Matias Guijarro's avatar
Matias Guijarro committed
759
760
    @property
    def encoder(self):
761
762
763
764
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
765
        return self.__encoder
766

767
768
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
769
770
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
771

772
773
774
775
776
777
778
779
780
781
782
783
784
785
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
    @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)

814
    def set_setting(self, *args):
815
        """Sets the given settings"""
816
817
818
        self.settings.set(*args)

    def get_setting(self, *args):
819
        """Return the values for the given settings"""
820
821
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
822
    def has_tag(self, tag):
823
824
825
826
827
828
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

829
        Return:
830
831
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
832
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
833
834
835
836
837
838
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

839
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
840
    def on(self):
841
        """Turns the axis on"""
842
843
844
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
845
        self.__controller.set_on(self)
846
        state = self.__controller.state(self)
847
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
848

849
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
850
    def off(self):
851
        """Turns the axis off"""
852
853
854
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
855
856
        self.__controller.set_off(self)
        state = self.__controller.state(self)
857
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
858

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
859
860
861
862
863
864
865
866
867
868
869
870
    @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):
871
872
873
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
874
        self.settings.set("_set_position", new_set_pos)
875

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
876
    @property
877
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
878
879
    def measured_position(self):
        """
880
        Return the encoder value in user units.
881

882
        Return:
883
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
884
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
885
        return self.dial2user(self.dial_measured_position)
886

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
887
    @property
888
    @lazy_init
889
    def dial_measured_position(self):
890
        """
891
        Return the dial encoder position.
892

893
        Return:
894
            float: dial encoder position
895
        """
896
897
898
899
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
900

901
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
902
        user_pos = self.position
903
        old_dial = self.dial
904

905
906
907
908
909
910
911
912
913
        # 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
914
        self.settings.set("dial_position", dial_pos)
915

916
917
918
919
920
921
        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)
922

923
924
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
925
    @property
926
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
927
    def dial(self):
928
        """
929
        Return current dial position, or set dial
930

931
        Return:
932
            float: current dial position (dimensionless)
933
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
934
935
936
937
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
938

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
939
940
    @dial.setter
    def dial(self, new_dial):
941
        if self.is_moving:
942
943
944
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
945
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
946
947
948
        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}")
949

950
951
952
953
954
955
    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
956
957
958
959
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
960
961
962
963
964
965
966
        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
967
968
969
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
970
971
972
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed
973

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
974
    @property
975
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
976
    def position(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
977
        """
978
        Return current user position, or set new user position
979

980
        Return:
981
            float: current user position (user units)
Cyril Guilloud's avatar
Cyril Guilloud committed
982
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
983
984
985
986
        pos = self.settings.get("position")
        if pos is None:
            pos = self.dial2user(self.dial)
        return pos
987

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
988
989
    @position.setter
    def position(self, new_pos):
990
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)