axis.py 86.9 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
5
# Copyright (c) 2015-2022 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.hook import execute_pre_move_hooks
15
from bliss.common.protocols import HasMetadataForDataset, Scannable
16
from bliss.common.cleanup import capture_exceptions
17
from bliss.common.motor_config import MotorConfig
18
from bliss.common.motor_settings import AxisSettings
19
from bliss.common import event
20
from bliss.common.greenlet_utils import protect_from_one_kill
21
from bliss.common.utils import with_custom_members, safe_get
22
from bliss.config.channels import Channel
23
from bliss.common.logtools import log_debug, user_print, log_warning
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
24
from bliss.common.utils import rounder
25
from bliss.common.utils import autocomplete_property
26
from bliss.comm.exceptions import CommunicationError
Lucas Felix's avatar
Lucas Felix committed
27
from bliss.common.closed_loop import ClosedLoop
28

29
import enum
30
import gevent
31
import re
32
import sys
33
import math
34
import functools
35
import collections
36
import itertools
37
import numpy
38
import warnings
39
40

warnings.simplefilter("once", DeprecationWarning)
41

42

43
#: Default polling time
44
DEFAULT_POLLING_TIME = 0.02
Matias Guijarro's avatar
Matias Guijarro committed
45

46

47
48
49
50
class AxisOnLimitError(RuntimeError):
    pass


51
52
53
54
class AxisFaultError(RuntimeError):
    pass


55
56
57
58
59
60
61
62
63
64
def float_or_inf(value, inf_sign=1):
    if value is None:
        value = float("inf")
        sign = math.copysign(1, inf_sign)
    else:
        sign = 1
    value = float(value)  # accepts float or numpy array of 1 element
    return sign * value


65
66
def _prepare_one_controller_motions(controller, motions):
    try:
67
        return controller.prepare_all(*motions)
68
    except NotImplementedError:
69
70
71
72
73
        # this is to "clear" the exception
        # (see issue #3294)
        pass
    for motion in motions:
        controller.prepare_move(motion)
74
75
76
77


def _start_one_controller_motions(controller, motions):
    try:
78
        return controller.start_all(*motions)
79
    except NotImplementedError:
80
81
82
83
84
        # this is to "clear" the exception
        # (see issue #3294)
        pass
    for motion in motions:
        controller.start_one(motion)
85
86
87
88
89
90


def _stop_one_controller_motions(controller, motions):
    try:
        controller.stop_all(*motions)
    except NotImplementedError:
91
92
93
94
95
        # this is to "clear" the exception
        # (see issue #3294)
        pass
    for motion in motions:
        controller.stop(motion.axis)
96
97


98
class GroupMove:
99
    def __init__(self, parent=None):
100
101
        self.parent = parent
        self._move_task = None
102
103
        self._motions_dict = dict()
        self._stop_motion = None
104
105
        self._interrupted_move = False
        self._backlash_started_event = gevent.event.Event()
106
107
108
109
110

    # Public API

    @property
    def is_moving(self):
Matias Guijarro's avatar
Matias Guijarro committed
111
        # A greenlet evaluates to True when it is alive
112
113
        return bool(self._move_task)

114
115
116
    def move(
        self,
        motions_dict,
117
        prepare_motion,
118
119
120
121
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
122
        polling_time=None,
123
    ):
124
125
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
126
        self._interrupted_move = False
127

128
129
130
131
132
        # motions_dict is { controller: [motion, ...] }
        all_motions = list(itertools.chain(*motions_dict.values()))
        with execute_pre_move_hooks(all_motions):
            for axis in (m.axis for m in all_motions):
                axis._check_ready()
133
134
135
136

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

138
            for motion_obj in motions:
139
140
141
142
                target_pos = motion_obj.user_target_pos
                if target_pos is not None and not isinstance(target_pos, str):
                    motion_obj.axis._set_position = target_pos

143
144
                msg = motion_obj.user_msg
                if msg:
145
                    user_print(msg)
146

147
        started = gevent.event.Event()
148

149
        self._move_task = gevent.spawn(
150
            self._move, motions_dict, start_motion, stop_motion, move_func, started
151
        )
152

153
154
155
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
156
        except BaseException:
157
158
            self.stop()
            raise
159
160
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
161
            self.wait()
162
163
164

    def wait(self):
        if self._move_task is not None:
165
166
            try:
                self._move_task.get()
167
            except BaseException:
168
169
                self.stop()
                raise
170
171

    def stop(self, wait=True):
172
173
174
        with capture_exceptions(raise_index=0) as capture:
            if self._move_task is not None:
                with capture():
175
                    self._stop_move(self._motions_dict, self._stop_motion, wait=False)
176
177
                if wait:
                    self._move_task.get()
178
179
180

    # Internal methods

181
182
    def _monitor_move(self, motions_dict, move_func, stop_func):
        monitor_move_tasks = {}
Vincent Michel's avatar
Vincent Michel committed
183
        for controller, motions in motions_dict.items():
184
            for motion in motions:
185
                if move_func is None:
186
                    move_func = "_handle_move"
187
                task = gevent.spawn(getattr(motion.axis, move_func), motion)
188
189
                monitor_move_tasks[task] = motion

190
        try:
191
192
193
194
195
196
197
198
199
200
201
202
203
            gevent.joinall(monitor_move_tasks, raise_error=True)
        except BaseException:
            # in case of error, all moves are stopped
            # _stop_move is called with the same monitoring tasks:
            # the stop command will be sent, then the same monitoring continues
            # in '_stop_move'
            self._stop_move(motions_dict, stop_func, monitor_move_tasks)
            raise
        else:
            # everything went fine: update the last motor state ;
            # we know the tasks have all completed successfully
            for task, motion in monitor_move_tasks.items():
                motion.last_state = task.get()
204

205
206
    def _stop_move(self, motions_dict, stop_motion, stop_wait_tasks=None, wait=True):
        self._interrupted_move = True
207

208
        stop_tasks = []
Vincent Michel's avatar
Vincent Michel committed
209
        for controller, motions in motions_dict.items():
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
            stop_tasks.append(gevent.spawn(stop_motion, controller, motions))

        with capture_exceptions(raise_index=0) as capture:
            # wait for all stop commands to be sent
            with capture():
                gevent.joinall(stop_tasks, raise_error=True)
            if capture.failed:
                with capture():
                    gevent.joinall(stop_tasks)

            if wait:
                if stop_wait_tasks is None:
                    # create tasks to wait for end of motion
                    stop_wait_tasks = {}
                    for controller, motions in motions_dict.items():
                        for motion in motions:
                            stop_wait_tasks[
                                gevent.spawn(
                                    motion.axis._move_loop, motion.polling_time
                                )
                            ] = motion

                # wait for end of motion
                gevent.joinall(stop_wait_tasks)

                for task, motion in stop_wait_tasks.items():
                    motion.last_state = None
                    with capture():
                        motion.last_state = task.get()
239

240
    @protect_from_one_kill
241
    def _do_backlash_move(self, motions_dict):
242
        backlash_motions = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
243
        for controller, motions in motions_dict.items():
244
245
            for motion in motions:
                if motion.backlash:
246
247
                    if self._interrupted_move:
                        # have to recalculate target: do backlash move from where it stopped
248
249
250
                        motion.target_pos = (
                            motion.axis.dial * motion.axis.steps_per_unit
                        )
251
252
253
254
255
256
257
258
259
260
                        # 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

261
262
263
264
265
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
266
                    backlash_motions[controller].append(backlash_motion)
267

268
269
270
271
272
273
274
275
276
        if backlash_motions:
            backlash_mv_group = GroupMove()
            backlash_mv_group._do_move(
                backlash_motions,
                _start_one_controller_motions,
                _stop_one_controller_motions,
                None,
                self._backlash_started_event,
            )
277

278
279
280
281
    def _do_move(
        self, motions_dict, start_motion, stop_motion, move_func, started_event
    ):
        for controller, motions in motions_dict.items():
282
            for motion in motions:
283
                motion.last_state = None
Matias Guijarro's avatar
Matias Guijarro committed
284

285
        with capture_exceptions(raise_index=0) as capture:
286
287
288
289
290
            # Spawn start motion tasks for all controllers
            start = [
                gevent.spawn(start_motion, controller, motions)
                for controller, motions in motions_dict.items()
            ]
291

Matias Guijarro's avatar
Matias Guijarro committed
292
293
294
            # wait for start tasks to be all done ;
            # in case of error or if wait is interrupted (ctrl-c, kill...),
            # immediately stop and return
295
296
297
298
299
300
301
302
            with capture():
                gevent.joinall(start, raise_error=True)
            if capture.failed:
                # either a start task failed, or ctrl-c or kill happened.
                # First, let all start task to finish
                # /!\ it is important to join those, to ensure stop is called
                # after tasks are done otherwise there is a risk 'end' is
                # called before 'start' is all done
303
                with capture():
304
                    gevent.joinall(start)
305
306
307
308
                # then, stop all axes and wait end of motion
                self._stop_move(motions_dict, stop_motion)
                # exit
                return
309

310
311
            # All controllers are now started
            if started_event is not None:
312
313
                started_event.set()

314
315
            if self.parent:
                event.send(self.parent, "move_done", False)
316

317
318
319
320
321
322
323
324
325
326
327
            # Spawn the monitoring for all motions
            with capture():
                self._monitor_move(motions_dict, move_func, stop_motion)

    def _move(self, motions_dict, start_motion, stop_motion, move_func, started_event):
        # Set axis moving state
        for motions in motions_dict.values():
            for motion in motions:
                motion.axis._set_moving_state()

        with capture_exceptions(raise_index=0) as capture:
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
            with capture():
                self._do_move(
                    motions_dict, start_motion, stop_motion, move_func, started_event
                )
            # Do backlash move, if needed
            with capture():
                self._do_backlash_move(motions_dict)

            reset_setpos = bool(capture.failed) or self._interrupted_move

            # cleanup
            # -------
            # update final state ; in case of exception
            # state is set to FAULT
            for motions in motions_dict.values():
                for motion in motions:
                    state = motion.last_state
                    if state is None:
                        # update state and update dial pos.
                        with capture():
                            motion.axis._update_settings()

            # 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
            if len(motions_dict) == 1:
                motion = motions_dict[list(motions_dict.keys()).pop()][0]
                if motion.type == "jog":
                    reset_setpos = False
                    motion.axis._jog_cleanup(
                        motion.saved_velocity, motion.reset_position
364
                    )
365
366
367
368
369
                elif motion.type == "homing":
                    reset_setpos = True
                elif motion.type == "limit_search":
                    reset_setpos = True
            if reset_setpos:
370
                with capture():
371
372
373
374
375
376
377
378
379
380
381
382
383
384
                    for motions in motions_dict.values():
                        for motion in motions:
                            motion.axis._set_position = motion.axis.position
                            event.send(motion.axis, "sync_hard")

            hooks = collections.defaultdict(list)
            for motions in motions_dict.values():
                for motion in motions:
                    axis = motion.axis

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

385
                    # set move done
386
387
388
                    motion.axis._set_move_done()

            if self._interrupted_move:
389
                user_print("")
390
391
392
                for motion in motions:
                    _axis = motion.axis
                    _axis_pos = safe_get(_axis, "position", on_error="!ERR")
393
                    user_print(f"Axis {_axis.name} stopped at position {_axis_pos}")
394
395
396
397

            try:
                if self.parent:
                    event.send(self.parent, "move_done", True)
398
            finally:
399
                for hook, motions in reversed(list(hooks.items())):
400
                    with capture():
401
                        hook.post_move(motions)
402

Cyril Guilloud's avatar
Cyril Guilloud committed
403

404
class Modulo:
405
406
407
408
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
409
        dial_pos = axis.dial
410
        axis._Axis__do_set_dial(dial_pos % self.modulo)
411
412


413
class Motion:
414
415
416
417
418
419
420
421
    """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
422

423
424
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
425
    """
Matias Guijarro's avatar
Matias Guijarro committed
426

427
428
429
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
430
        self.__axis = axis
431
        self.__type = motion_type
432
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
433
434
435
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
436
        self.polling_time = DEFAULT_POLLING_TIME
Matias Guijarro's avatar
Matias Guijarro committed
437

Matias Guijarro's avatar
Matias Guijarro committed
438
439
    @property
    def axis(self):
440
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
441
        return self.__axis
442

443
444
445
446
    @property
    def type(self):
        return self.__type

447
448
449
450
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
451
            msg = (
452
                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
453
454
455
456
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

457
458
459
460
461
462
463
464
465
466
467
        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
468

Matias Guijarro's avatar
linting    
Matias Guijarro committed
469
class Trajectory:
Valentin Valls's avatar
Valentin Valls committed
470
    """Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
471

472
473
474
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
475

Matias Guijarro's avatar
Matias Guijarro committed
476
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
477
478
479
480
481
482
483
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
484
485
486
487
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

488
489
490
491
492
493
494
    @property
    def axis(self):
        return self.__axis

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

496
497
    @property
    def events_positions(self):
498
        return self._events_positions
499

500
501
    @events_positions.setter
    def events_positions(self, events):
502
        self._events_positions = events
503

504
505
506
    def has_events(self):
        return self._events_positions.size

507
508
509
510
511
512
513
    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
        """
514
515
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
516
        pvt = numpy.copy(self.__pvt)
517
        pvt["position"] = self._axis_user2dial(user_pos) * self.axis.steps_per_unit
518
        pvt["velocity"] = user_velocity * self.axis.steps_per_unit
519
        new_obj = self.__class__(self.axis, pvt)
520
        pattern_evts = numpy.copy(self._events_positions)
521
522
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
523
        new_obj._events_positions = pattern_evts
524
        return new_obj
525

526
527
528
    def _axis_user2dial(self, user_pos):
        return self.axis.user2dial(user_pos)

529

530
class CyclicTrajectory(Trajectory):
531
532
533
534
535
536
537
538
    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)
539
540
        self.nb_cycles = nb_cycles
        self.origin = origin
541
542
543
544
545

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

546
547
548
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
549

550
551
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
552
553
        self._events_positions = values

554
555
556
557
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
558
559
560
561
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
562
563
564

    @property
    def pvt(self):
565
        """Return the full PVT table. Positions are absolute"""
566
567
568
569
570
571
572
573
574
575
576
577
578
579
        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
580
        for cycle in range(self.nb_cycles):
581
            start = cycle_size * cycle + offset
582
583
            end = start + cycle_size
            pvt[start:end] = raw_pvt
584
585
586
587
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
588
589

        if self.is_closed:
590
591
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
592
593
594

        return pvt

595
596
597
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
598
        time_offset = 0.0
599
        last_time = self.pvt_pattern["time"][-1]
600
        nb_pattern_evts = len(pattern_evts)
601
602
603
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
604
        for i in range(self.nb_cycles):
605
606
607
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
608
            sub_evts[:] = pattern_evts
609
            sub_evts["time"] += time_offset
610
611
612
            time_offset += last_time
        return all_events

613
614
615
616
617
    def _axis_user2dial(self, user_pos):
        # here the trajectory is relative to the origin so the **pvt_pattern**
        # should not contains the axis offset as it's already in **origin**
        return user_pos * self.axis.sign

618
619
620
621
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
622
623
624
625
        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
626
627


628
629
630
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
631
632
633
634
        if self.disabled:
            raise RuntimeError(f"Axis {self.name} is disabled")
        try:
            self.controller._initialize_axis(self)
635
636
637
638
        except Exception as e:
            if isinstance(e, CommunicationError):
                # also disable the controller
                self.controller._disabled = True
639
640
641
642
643
644
            self._disabled = True
            raise
        else:
            if not self.controller.axis_initialized(self):
                # failed to initialize
                self._disabled = True
645
        return func(self, *args, **kwargs)
646

647
    return func_wrapper
648

Matias Guijarro's avatar
Matias Guijarro committed
649

650
@with_custom_members
651
class Axis(Scannable, HasMetadataForDataset):
652
    """
Cyril Guilloud's avatar
Cyril Guilloud committed
653
654
    This class is typically used by motor controllers in bliss to export
    axis with harmonised interface for users and configuration.
655
656
    """

657
658
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
659
660
661
662
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
        self.__move_done = gevent.event.Event()
663
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
664
        self.__move_done.set()
665
        self.__move_done_callback.set()
666
667
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
668
            hook._add_axis(self)
669
670
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
671
672
        if self.__encoder is not None:
            self.__encoder.axis = self
673
        self.__config = MotorConfig(config)
674
        self.__settings = AxisSettings(self)
675
        self._init_config_properties()
Matias Guijarro's avatar
linting    
Matias Guijarro committed
676
        self.__no_offset = False
677
        self._group_move = GroupMove()
678
        self._lock = gevent.lock.Semaphore()
679
        self.__positioner = True
680
        self._disabled = False
Lucas Felix's avatar
Lucas Felix committed
681
682
683
684
        if config.get("closed_loop"):
            self._closed_loop = ClosedLoop(self)
        else:
            self._closed_loop = None
Matias Guijarro's avatar
Matias Guijarro committed
685

686
687
688
689
690
691
692
693
694
695
696
        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
Matias Guijarro's avatar
Matias Guijarro committed
697
698
        for setting_name in disabled_cache:
            self.settings.disable_cache(setting_name)
699
        self._unit = self.config.get("unit", str, None)
700
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
701
        global_map.register(self, parents_list=["axes", controller])
702

Matias Guijarro's avatar
linting    
Matias Guijarro committed
703
704
705
706
707
708
709
710
711
712
713
714
715
        # create Beacon channels
        self.settings.init_channels()
        self._move_stop_channel = Channel(
            f"axis.{self.name}.move_stop",
            default_value=False,
            callback=self._external_stop,
        )
        self._jog_velocity_channel = Channel(
            f"axis.{self.name}.change_jog_velocity",
            default_value=None,
            callback=self._set_jog_velocity,
        )

716
    def __close__(self):
717
718
719
720
721
722
723
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

724
725
726
727
728
729
730
731
732
733
734
    @property
    def _check_encoder(self):
        return self.config.get("check_encoder", bool, self.encoder) and self.encoder

    @property
    def _read_position_mode(self):
        if self.config.get("read_position", str, "controller") == "encoder":
            return self.READ_POSITION_MODE.ENCODER
        else:
            return self.READ_POSITION_MODE.CONTROLLER

735
736
737
738
739
740
741
742
    @property
    def no_offset(self):
        return self.__no_offset

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

743
744
    @property
    def unit(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
745
        """unit used for the Axis (mm, deg, um...)"""
746
747
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
748
749
    @property
    def name(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
750
        """name of the axis"""
Matias Guijarro's avatar
Matias Guijarro committed
751
752
        return self.__name

753
    @property
Jibril Mammeri's avatar
Jibril Mammeri committed
754
    def _positioner(self):
755
756
757
        """Axis positioner"""
        return self.__positioner

Jibril Mammeri's avatar
Jibril Mammeri committed
758
759
    @_positioner.setter
    def _positioner(self, new_p):
760
761
        self.__positioner = new_p

762
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
763
    def controller(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
764
765
766
767
        """
        Motor controller of the axis
        Reference to :class:`~bliss.controllers.motor.Controller`
        """
Matias Guijarro's avatar
Matias Guijarro committed
768
769
770
771
        return self.__controller

    @property
    def config(self):
772
        """Reference to the :class:`~bliss.common.motor_config.MotorConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
773
774
775
776
        return self.__config

    @property
    def settings(self):
777
778
779
780
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
781
782
783
784
        return self.__settings

    @property
    def is_moving(self):
785
786
787
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
788
789
        return not self.__move_done.is_set()

790
    def _init_config_properties(
Matias Guijarro's avatar
Matias Guijarro committed
791
792
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
793
794
        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
795
        if velocity:
796
            if "velocity" in self.settings.config_settings:
Matias Guijarro's avatar
Matias Guijarro committed
797
                self.__config_velocity = self.config.get("velocity", float)
798
            if "jog_velocity" in self.settings.config_settings:
799
800
801
                self.__config_jog_velocity = self.config.get(
                    "jog_velocity", float, self.__config_velocity
                )
802
803
804
805
806
807
            self.__config_velocity_low_limit = self.config.get(
                "velocity_low_limit", float, float("inf")
            )
            self.__config_velocity_high_limit = self.config.get(
                "velocity_high_limit", float, float("inf")
            )
Matias Guijarro's avatar
Matias Guijarro committed
808
        if acceleration:
809
            if "acceleration" in self.settings.config_settings:
Matias Guijarro's avatar
Matias Guijarro committed
810
811
812
813
814
815
816
817
                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)
818

819
820
    @property
    def steps_per_unit(self):
821
        """Current steps per unit (:obj:`float`)"""
822
        return self.__steps_per_unit
823

824
    @property
Matias Guijarro's avatar
Matias Guijarro committed
825
826
827
828
829
830
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
831
832
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
833
834
835
836
837
838
839
840
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

Lucas Felix's avatar
Lucas Felix committed
842
843
844
845
846
847
848
849
    @property
    @lazy_init
    def closed_loop(self):
        """
        Closed loop object associated to axis.
        """
        return self._closed_loop

850
851
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
852
        """Current Axis tolerance in dial units (:obj:`float`)"""
853
        return self.__tolerance
854

Matias Guijarro's avatar
Matias Guijarro committed
855
856
    @property
    def encoder(self):
857
858
859
860
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
861
        return self.__encoder
862

863
864
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
865
866
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
867

868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
    @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
895
    @lazy_init
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
    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)

911
    def set_setting(self, *args):
912
        """Sets the given settings"""
913
914
915
        self.settings.set(*args)

    def get_setting(self, *args):
916
        """Return the values for the given settings"""
917
918
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
919
    def has_tag(self, tag):
920
921
922
923
924
925
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

926
        Return:
927
928
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
929
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
930
931
932
933
934
935
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

936
937
938
939
940
941
942
943
    @property
    def disabled(self):
        return self._disabled

    def enable(self):
        self._disabled = False
        self.hw_state  # force update

944
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
945
    def on(self):
946
        """Turns the axis on"""
947
948
949
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
950
        self.__controller.set_on(self)
951
        state = self.__controller.state(self)
952
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
953

954
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
955
    def off(self):
956
        """Turns the axis off"""
957
958
959
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
960
961
        self.__controller.set_off(self)
        state = self.__controller.state(self)
962
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
963

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
964
965
966
967
968
969
    @property
    @lazy_init
    def _set_position(self):
        sp = self.settings.get("_set_position")
        if sp is not None:
            return sp
970
971
972
973
974
975
        if self._read_position_mode == self.READ_POSITION_MODE.ENCODER:
            # no setting, first time pos is read, init with controller hw pos.
            # issue 2463
            position = self._do_read_hw_position()
        else:
            position = self.position
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
976
977
978
979
        self._set_position = position
        return position

    @_set_position.setter
980
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
981
    def _set_position(self, new_set_pos):
982
983
984
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
985
        self.settings.set("_set_position", new_set_pos)
986

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
987
    @property
988
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
989
990
    def measured_position(self):
        """
Cyril Guilloud's avatar
Cyril Guilloud committed
991
        Return measured position (ie: usually the encoder value).
992

Cyril Guilloud's avatar
Cyril Guilloud committed
993
        Returns:
994
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
995
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
996
        return self.dial2user(self.dial_measured_position)
997

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
998
    @property
999
    @lazy_init
1000
    def dial_measured_position(self):
For faster browsing, not all history is shown. View entire blame