axis.py 76.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
"""
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
125
126
127
128
129

        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)

130
        started = gevent.event.Event()
131

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

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

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

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

    # Internal methods

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                        motion.axis._set_move_done()
366

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

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

Cyril Guilloud's avatar
Cyril Guilloud committed
382

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

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


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

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

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

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

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

425
426
427
428
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
429
            msg = (
430
                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
431
432
433
434
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

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

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

450
451
452
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
453

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

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

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

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

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

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

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


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

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

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

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

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

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

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

        return pvt

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

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


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

604
    return func_wrapper
605

Matias Guijarro's avatar
Matias Guijarro committed
606

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

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

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

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

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

667
    def __close__(self):
668
669
670
671
672
673
674
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

675
676
677
678
679
680
681
682
    @property
    def no_offset(self):
        return self.__no_offset

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

683
684
685
686
687
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
688
689
    @property
    def name(self):
690
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
691
692
        return self.__name

693
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
694
    def controller(self):
695
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
696
697
698
699
        return self.__controller

    @property
    def config(self):
700
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
701
702
703
704
        return self.__config

    @property
    def settings(self):
705
706
707
708
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
709
710
711
712
        return self.__settings

    @property
    def is_moving(self):
713
714
715
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
716
717
        return not self.__move_done.is_set()

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

741
742
    @property
    def steps_per_unit(self):
743
        """Current steps per unit (:obj:`float`)"""
744
        return self.__steps_per_unit
745

746
    @property
Matias Guijarro's avatar
Matias Guijarro committed
747
748
749
750
751
752
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
753
754
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
755
756
757
758
759
760
761
762
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

764
765
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
766
        """Current Axis tolerance in dial units (:obj:`float`)"""
767
        return self.__tolerance
768

Matias Guijarro's avatar
Matias Guijarro committed
769
770
    @property
    def encoder(self):
771
772
773
774
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
775
        return self.__encoder
776

777
778
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
779
780
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
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
814
815
816
817
818
819
820
821
822
823
    @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)

824
    def set_setting(self, *args):
825
        """Sets the given settings"""
826
827
828
        self.settings.set(*args)

    def get_setting(self, *args):
829
        """Return the values for the given settings"""
830
831
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
832
    def has_tag(self, tag):
833
834
835
836
837
838
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

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

849
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
850
    def on(self):
851
        """Turns the axis on"""
852
853
854
        if self.is_moving:
            return

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

859
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
860
    def off(self):
861
        """Turns the axis off"""
862
863
864
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
865
866
        self.__controller.set_off(self)
        state = self.__controller.state(self)
867
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
868

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
886
    @property
887
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
888
889
    def measured_position(self):
        """
890
        Return the encoder value in user units.
891

892
        Return:
893
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
894
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
895
        return self.dial2user(self.dial_measured_position)
896

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
897
    @property
898
    @lazy_init
899
    def dial_measured_position(self):
900
        """
901
        Return the dial encoder position.
902

903
        Return:
904
            float: dial encoder position
905
        """
906
907
908
909
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
910

911
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
912
        user_pos = self.position
913
        old_dial = self.dial
914

915
916
917
918
919
920
921
922
923
        # 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
924
        self.settings.set("dial_position", dial_pos)
925

926
927
928
929
930
931
        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)
932

933
934
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
935
    @property
936
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
937
    def dial(self):
938
        """
939
        Return current dial position, or set dial
940

941
        Return:
942
            float: current dial position (dimensionless)
943
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
944
945
946
947
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
948

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

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
984
    @property
985
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
986
    def position(self):
Cyril Guilloud's avatar
Cyril Guilloud committed