axis.py 72.8 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

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

warnings.simplefilter("once", DeprecationWarning)
35

36

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

40

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

    # Public API

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

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

        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)

128
        started = gevent.event.Event()
129

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

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

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

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

    # Internal methods

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                        motion.axis._set_move_done()
364

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

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

Cyril Guilloud's avatar
Cyril Guilloud committed
380

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

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


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

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

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

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

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

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

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

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

448
449
450
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
451

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

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

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

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

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

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

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


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

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

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

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

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

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

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

        return pvt

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

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


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

602
    return func_wrapper
603

Matias Guijarro's avatar
Matias Guijarro committed
604

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
    @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)

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

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

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

        Args:
            tag (str): tag name

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

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

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

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

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

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

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

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

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

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

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

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

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

922
923
        return dial_pos

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

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

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

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

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

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
987
988
    @position.setter
    def position(self, new_pos):
989
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)
990
        if self.is_moving:
991
992
993
            raise RuntimeError(
                "%s: can't set axis user position " "while moving" % self.name
            )