axis.py 77.4 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
231
232
233
234
235
                        # Adjust the difference between encoder and motor controller indexer
                        if (
                            motion.axis._read_position_mode
                            == Axis.READ_POSITION_MODE.ENCODER
                        ):
                            controller_position = controller.read_position(motion.axis)
                            enc_position = motion.target_pos
                            delta_pos = controller_position - enc_position
                            motion.target_pos += delta_pos

236
237
238
239
240
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
241
242
243
244
245
246
                    axis_polling_time = (
                        motion.axis._polling_time
                        if polling_time is None
                        else polling_time
                    )

247
248
                    backlash_move.append(
                        gevent.spawn(
249
250
251
                            motion.axis._backlash_move,
                            backlash_motion,
                            axis_polling_time,
252
253
                        )
                    )
254
        gevent.joinall(backlash_move)
255
        gevent.joinall(backlash_move, raise_error=True)
256

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

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

282
                # Wait for the controllers to be started
283
                with capture():
284
285
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
286
                    gevent.joinall(start)
287
288
289
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
290

291
292
                    self._stop_wait(motions_dict, capture)
                    return
293

294
295
296
297
298
299
300
                # 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
301
                with capture():
302
303
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
304
305
306
307
308
309
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
310
311
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
312
313
314
315
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
316
                reset_setpos = capture.failed or self._user_stopped
317

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

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

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

362
                hooks = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
363
                for motions in motions_dict.values():
364
                    for motion in motions:
365
366
367
368
369
                        axis = motion.axis

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

371
372
                        # set move done
                        for _, chan in axis._beacon_channels.items():
373
374
375
                            chan.register_callback(chan._setting_update_cb)

                        motion.axis._set_move_done()
376

377
378
379
380
381
382
383
                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}")

384
385
386
387
388
389
390
                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)
391

Cyril Guilloud's avatar
Cyril Guilloud committed
392

393
class Modulo:
394
395
396
397
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
398
        dial_pos = axis.dial
399
        axis._Axis__do_set_dial(dial_pos % self.modulo)
400
401


402
class Motion:
403
404
405
406
407
408
409
410
    """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
411

412
413
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
414
    """
Matias Guijarro's avatar
Matias Guijarro committed
415

416
417
418
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
419
        self.__axis = axis
420
        self.__type = motion_type
421
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
422
423
424
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
425

Matias Guijarro's avatar
Matias Guijarro committed
426
427
    @property
    def axis(self):
428
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
429
        return self.__axis
430

431
432
433
434
    @property
    def type(self):
        return self.__type

435
436
437
438
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
439
            msg = (
440
                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
441
442
443
444
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

445
446
447
448
449
450
451
452
453
454
455
        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
456

457
458
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
459

460
461
462
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
463

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

476
477
478
479
480
481
482
    @property
    def axis(self):
        return self.__axis

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

484
485
    @property
    def events_positions(self):
486
        return self._events_positions
487

488
489
    @events_positions.setter
    def events_positions(self, events):
490
        self._events_positions = events
491

492
493
494
    def has_events(self):
        return self._events_positions.size

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


515
class CyclicTrajectory(Trajectory):
516
517
518
519
520
521
522
523
    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)
524
525
        self.nb_cycles = nb_cycles
        self.origin = origin
526
527
528
529
530

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

531
532
533
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
534

535
536
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
537
538
        self._events_positions = values

539
540
541
542
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
543
544
545
546
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
547
548
549

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

        if self.is_closed:
575
576
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
577
578
579

        return pvt

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

598
599
600
601
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
602
603
604
605
        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
606
607


608
609
610
611
612
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
613

614
    return func_wrapper
615

Matias Guijarro's avatar
Matias Guijarro committed
616

617
@with_custom_members
618
class Axis:
619
620
621
622
623
624
625
    """
    Bliss motor axis

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

626
627
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

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

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

677
    def __close__(self):
678
679
680
681
682
683
684
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

685
686
687
688
689
690
691
692
    @property
    def no_offset(self):
        return self.__no_offset

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

693
694
695
696
697
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
698
699
    @property
    def name(self):
700
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
701
702
        return self.__name

703
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
704
    def controller(self):
705
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
706
707
708
709
        return self.__controller

    @property
    def config(self):
710
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
711
712
713
714
        return self.__config

    @property
    def settings(self):
715
716
717
718
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
719
720
721
722
        return self.__settings

    @property
    def is_moving(self):
723
724
725
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
726
727
        return not self.__move_done.is_set()

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

751
752
    @property
    def steps_per_unit(self):
753
        """Current steps per unit (:obj:`float`)"""
754
        return self.__steps_per_unit
755

756
    @property
Matias Guijarro's avatar
Matias Guijarro committed
757
758
759
760
761
762
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
763
764
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
765
766
767
768
769
770
771
772
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

774
775
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
776
        """Current Axis tolerance in dial units (:obj:`float`)"""
777
        return self.__tolerance
778

Matias Guijarro's avatar
Matias Guijarro committed
779
780
    @property
    def encoder(self):
781
782
783
784
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
785
        return self.__encoder
786

787
788
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
789
790
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
791

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

834
    def set_setting(self, *args):
835
        """Sets the given settings"""
836
837
838
        self.settings.set(*args)

    def get_setting(self, *args):
839
        """Return the values for the given settings"""
840
841
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
842
    def has_tag(self, tag):
843
844
845
846
847
848
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

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

859
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
860
    def on(self):
861
        """Turns the axis on"""
862
863
864
        if self.is_moving:
            return

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

869
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
870
    def off(self):
871
        """Turns the axis off"""
872
873
874
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
875
876
        self.__controller.set_off(self)
        state = self.__controller.state(self)
877
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
878

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
896
    @property
897
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
898
899
    def measured_position(self):
        """
900
        Return the encoder value in user units.
901

902
        Return:
903
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
904
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
905
        return self.dial2user(self.dial_measured_position)
906

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
907
    @property
908
    @lazy_init
909
    def dial_measured_position(self):
910
        """
911
        Return the dial encoder position.
912

913
        Return:
914
            float: dial encoder position
915
        """
916
917
918
919
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
920

921
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
922
        user_pos = self.position
923
        old_dial = self.dial
924

925
926
927
928
929
930
931
932
933
        # 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
934
        self.settings.set("dial_position", dial_pos)
935

936
937
938
939
940
941
        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)
942

943
944
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
945
    @property
946
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
947
    def dial(self):
948
        """
949
        Return current dial position, or set dial
950

951
        Return:
952
            float: current dial position (dimensionless)
953
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
954
955
956
957
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
958

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

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
994
    @property