axis.py 80 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
from bliss.common.motor_config import MotorConfig
16
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, user_print
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
44
45
46
class AxisOnLimitError(RuntimeError):
    pass


47
48
49
50
class AxisFaultError(RuntimeError):
    pass


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def _prepare_one_controller_motions(controller, motions):
    try:
        controller.prepare_all(*motions)
    except NotImplementedError:
        for motion in motions:
            controller.prepare_move(motion)


def _start_one_controller_motions(controller, motions):
    try:
        controller.start_all(*motions)
    except NotImplementedError:
        for motion in motions:
            controller.start_one(motion)


def _stop_one_controller_motions(controller, motions):
    try:
        controller.stop_all(*motions)
    except NotImplementedError:
        for motion in motions:
            controller.stop(motion.axis)


75
class GroupMove:
76
    def __init__(self, parent=None):
77
78
        self.parent = parent
        self._move_task = None
79
80
        self._motions_dict = dict()
        self._stop_motion = None
81
82
        self._interrupted_move = False
        self._backlash_started_event = gevent.event.Event()
83
84
85
86
87

    # Public API

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

91
92
93
    def move(
        self,
        motions_dict,
94
        prepare_motion,
95
96
97
98
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
99
        polling_time=None,
100
    ):
101
102
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
103
        self._interrupted_move = False
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
130
131
132
133
134
135
136
137
138

        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,
Matias Guijarro's avatar
Matias Guijarro committed
139
        # some axes can **become** ready thanks to the hook
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
        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)
158

159
            for motion_obj in motions:
160
161
162
163
                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

164
165
                msg = motion_obj.user_msg
                if msg:
166
                    user_print(msg)
167

168
        started = gevent.event.Event()
169

170
        self._move_task = gevent.spawn(
171
            self._move, motions_dict, start_motion, stop_motion, move_func, started
172
        )
173

174
175
176
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
177
        except BaseException:
178
179
            self.stop()
            raise
180
181
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
182
            self.wait()
183
184
185

    def wait(self):
        if self._move_task is not None:
186
187
            try:
                self._move_task.get()
188
            except BaseException:
189
190
                self.stop()
                raise
191
192

    def stop(self, wait=True):
193
194
195
        with capture_exceptions(raise_index=0) as capture:
            if self._move_task is not None:
                with capture():
196
                    self._stop_move(self._motions_dict, self._stop_motion, wait=False)
197
198
                if wait:
                    self._move_task.get()
199
200
201

    # Internal methods

202
203
    def _monitor_move(self, motions_dict, move_func, stop_func):
        monitor_move_tasks = {}
Vincent Michel's avatar
Vincent Michel committed
204
        for controller, motions in motions_dict.items():
205
            for motion in motions:
206
                if move_func is None:
207
                    move_func = "_handle_move"
208
                task = gevent.spawn(getattr(motion.axis, move_func), motion)
209
210
                monitor_move_tasks[task] = motion

211
        try:
212
213
214
215
216
217
218
219
220
221
222
223
224
            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()
225

226
227
    def _stop_move(self, motions_dict, stop_motion, stop_wait_tasks=None, wait=True):
        self._interrupted_move = True
228

229
        stop_tasks = []
Vincent Michel's avatar
Vincent Michel committed
230
        for controller, motions in motions_dict.items():
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
            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()
260

261
    @protect_from_one_kill
262
    def _do_backlash_move(self, motions_dict):
263
        backlash_motions = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
264
        for controller, motions in motions_dict.items():
265
266
            for motion in motions:
                if motion.backlash:
267
268
                    if self._interrupted_move:
                        # have to recalculate target: do backlash move from where it stopped
269
270
271
                        motion.target_pos = (
                            motion.axis.dial * motion.axis.steps_per_unit
                        )
272
273
274
275
276
277
278
279
280
281
                        # 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

282
283
284
285
286
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
287
                    backlash_motions[controller].append(backlash_motion)
288

289
290
291
292
293
294
295
296
297
        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,
            )
298

299
300
301
302
    def _do_move(
        self, motions_dict, start_motion, stop_motion, move_func, started_event
    ):
        for controller, motions in motions_dict.items():
303
            for motion in motions:
304
                motion.last_state = None
Matias Guijarro's avatar
Matias Guijarro committed
305

306
        with capture_exceptions(raise_index=0) as capture:
307
308
309
310
311
            # Spawn start motion tasks for all controllers
            start = [
                gevent.spawn(start_motion, controller, motions)
                for controller, motions in motions_dict.items()
            ]
312

Matias Guijarro's avatar
Matias Guijarro committed
313
314
315
            # wait for start tasks to be all done ;
            # in case of error or if wait is interrupted (ctrl-c, kill...),
            # immediately stop and return
316
317
318
319
320
321
322
323
            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
324
                with capture():
325
                    gevent.joinall(start)
326
327
328
329
                # then, stop all axes and wait end of motion
                self._stop_move(motions_dict, stop_motion)
                # exit
                return
330

331
332
            # All controllers are now started
            if started_event is not None:
333
334
                started_event.set()

335
336
            if self.parent:
                event.send(self.parent, "move_done", False)
337

338
339
340
341
342
343
344
345
346
347
            # 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()

348
                motion.axis.settings.unregister_channels_callbacks()
349

350
        with capture_exceptions(raise_index=0) as capture:
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
            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
387
                    )
388
389
390
391
392
                elif motion.type == "homing":
                    reset_setpos = True
                elif motion.type == "limit_search":
                    reset_setpos = True
            if reset_setpos:
393
                with capture():
394
395
396
397
398
399
400
401
402
403
404
405
406
407
                    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)

408
                    axis.settings.register_channels_callbacks()
409

410
                    # set move done
411
412
413
                    motion.axis._set_move_done()

            if self._interrupted_move:
414
                user_print("")
415
416
417
                for motion in motions:
                    _axis = motion.axis
                    _axis_pos = safe_get(_axis, "position", on_error="!ERR")
418
                    user_print(f"Axis {_axis.name} stopped at position {_axis_pos}")
419
420
421
422

            try:
                if self.parent:
                    event.send(self.parent, "move_done", True)
423
            finally:
424
                for hook, motions in reversed(list(hooks.items())):
425
                    with capture():
426
                        hook.post_move(motions)
427

Cyril Guilloud's avatar
Cyril Guilloud committed
428

429
class Modulo:
430
431
432
433
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
434
        dial_pos = axis.dial
435
        axis._Axis__do_set_dial(dial_pos % self.modulo)
436
437


438
class Motion:
439
440
441
442
443
444
445
446
    """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
447

448
449
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
450
    """
Matias Guijarro's avatar
Matias Guijarro committed
451

452
453
454
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
455
        self.__axis = axis
456
        self.__type = motion_type
457
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
458
459
460
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
461
        self.polling_time = DEFAULT_POLLING_TIME
Matias Guijarro's avatar
Matias Guijarro committed
462

Matias Guijarro's avatar
Matias Guijarro committed
463
464
    @property
    def axis(self):
465
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
466
        return self.__axis
467

468
469
470
471
    @property
    def type(self):
        return self.__type

472
473
474
475
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
476
            msg = (
477
                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
478
479
480
481
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

482
483
484
485
486
487
488
489
490
491
492
        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
493

Matias Guijarro's avatar
linting    
Matias Guijarro committed
494
class Trajectory:
495
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
496

497
498
499
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
500

Matias Guijarro's avatar
Matias Guijarro committed
501
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
502
503
504
505
506
507
508
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
509
510
511
512
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

513
514
515
516
517
518
519
    @property
    def axis(self):
        return self.__axis

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

521
522
    @property
    def events_positions(self):
523
        return self._events_positions
524

525
526
    @events_positions.setter
    def events_positions(self, events):
527
        self._events_positions = events
528

529
530
531
    def has_events(self):
        return self._events_positions.size

532
533
534
535
536
537
538
    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
        """
539
540
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
541
        pvt = numpy.copy(self.__pvt)
542
543
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
544
        new_obj = self.__class__(self.axis, pvt)
545
        pattern_evts = numpy.copy(self._events_positions)
546
547
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
548
        new_obj._events_positions = pattern_evts
549
        return new_obj
550
551


552
class CyclicTrajectory(Trajectory):
553
554
555
556
557
558
559
560
    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)
561
562
        self.nb_cycles = nb_cycles
        self.origin = origin
563
564
565
566
567

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

568
569
570
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
571

572
573
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
574
575
        self._events_positions = values

576
577
578
579
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
580
581
582
583
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
584
585
586

    @property
    def pvt(self):
587
        """Return the full PVT table. Positions are absolute"""
588
589
590
591
592
593
594
595
596
597
598
599
600
601
        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
602
        for cycle in range(self.nb_cycles):
603
            start = cycle_size * cycle + offset
604
605
            end = start + cycle_size
            pvt[start:end] = raw_pvt
606
607
608
609
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
610
611

        if self.is_closed:
612
613
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
614
615
616

        return pvt

617
618
619
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
620
        time_offset = 0.0
621
        last_time = self.pvt_pattern["time"][-1]
622
        nb_pattern_evts = len(pattern_evts)
623
624
625
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
626
        for i in range(self.nb_cycles):
627
628
629
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
630
            sub_evts[:] = pattern_evts
631
            sub_evts["time"] += time_offset
632
633
634
            time_offset += last_time
        return all_events

635
636
637
638
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
639
640
641
642
        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
643
644


645
646
647
648
649
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
650

651
    return func_wrapper
652

Matias Guijarro's avatar
Matias Guijarro committed
653

654
@with_custom_members
655
class Axis:
656
657
658
659
660
661
662
    """
    Bliss motor axis

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

663
664
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
665
666
667
668
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
        self.__move_done = gevent.event.Event()
669
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
670
        self.__move_done.set()
671
        self.__move_done_callback.set()
672
673
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
674
            hook._add_axis(self)
675
676
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
677
678
        if self.__encoder is not None:
            self.__encoder.axis = self
679
        self.__config = MotorConfig(config)
680
        self.__settings = AxisSettings(self)
681
        self._init_config_properties()
Matias Guijarro's avatar
linting    
Matias Guijarro committed
682
        self.__no_offset = False
683
        self._group_move = GroupMove()
684
        self._lock = gevent.lock.Semaphore()
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
    @property
    def no_offset(self):
        return self.__no_offset

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

732
733
734
735
736
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
737
738
    @property
    def name(self):
739
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
740
741
        return self.__name

742
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
743
    def controller(self):
744
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
745
746
747
748
        return self.__controller

    @property
    def config(self):
749
        """Reference to the :class:`~bliss.common.motor_config.MotorConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
750
751
752
753
        return self.__config

    @property
    def settings(self):
754
755
756
757
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
758
759
760
761
        return self.__settings

    @property
    def is_moving(self):
762
763
764
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
765
766
        return not self.__move_done.is_set()

767
    def _init_config_properties(
Matias Guijarro's avatar
Matias Guijarro committed
768
769
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
770
771
        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
772
        if velocity:
773
            if "velocity" in self.settings.config_settings:
Matias Guijarro's avatar
Matias Guijarro committed
774
                self.__config_velocity = self.config.get("velocity", float)
775
            if "jog_velocity" in self.settings.config_settings:
776
777
778
                self.__config_jog_velocity = self.config.get(
                    "jog_velocity", float, self.__config_velocity
                )
Matias Guijarro's avatar
Matias Guijarro committed
779
        if acceleration:
780
            if "acceleration" in self.settings.config_settings:
Matias Guijarro's avatar
Matias Guijarro committed
781
782
783
784
785
786
787
788
                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)
789

790
791
    @property
    def steps_per_unit(self):
792
        """Current steps per unit (:obj:`float`)"""
793
        return self.__steps_per_unit
794

795
    @property
Matias Guijarro's avatar
Matias Guijarro committed
796
797
798
799
800
801
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
802
803
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
804
805
806
807
808
809
810
811
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

813
814
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
815
        """Current Axis tolerance in dial units (:obj:`float`)"""
816
        return self.__tolerance
817

Matias Guijarro's avatar
Matias Guijarro committed
818
819
    @property
    def encoder(self):
820
821
822
823
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
824
        return self.__encoder
825

826
827
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
828
829
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
830

831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
    @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
858
    @lazy_init
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
    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)

874
    def set_setting(self, *args):
875
        """Sets the given settings"""
876
877
878
        self.settings.set(*args)

    def get_setting(self, *args):
879
        """Return the values for the given settings"""
880
881
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
882
    def has_tag(self, tag):
883
884
885
886
887
888
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

889
        Return:
890
891
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
892
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
893
894
895
896
897
898
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

899
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
900
    def on(self):
901
        """Turns the axis on"""
902
903
904
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
905
        self.__controller.set_on(self)
906
        state = self.__controller.state(self)
907
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
908

909
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
910
    def off(self):
911
        """Turns the axis off"""
912
913
914
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
915
916
        self.__controller.set_off(self)
        state = self.__controller.state(self)
917
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
918

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
919
920
921
922
923
924
925
926
927
928
929
    @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
930
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
931
    def _set_position(self, new_set_pos):
932
933
934
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
935
        self.settings.set("_set_position", new_set_pos)
936

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
937
    @property
938
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
939
940
    def measured_position(self):
        """
941
        Return the encoder value in user units.
942

943
        Return:
944
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
945
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
946
        return self.dial2user(self.dial_measured_position)
947

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
948
    @property
949
    @lazy_init
950
    def dial_measured_position(self):
951
        """
952
        Return the dial encoder position.
953

954
        Return:
955
            float: dial encoder position
956
        """
957
958
959
960
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
961

962
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
963
        user_pos = self.position
964
        old_dial = self.dial
965

966
967
968
969
970
971
972
973
974
        # 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
975
        self.settings.set("dial_position", dial_pos)
976

977
978
979
980
981
982
        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)
983

984
985
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
986
    @property
987
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
988
    def dial(self):
989
        """
990
        Return current dial position, or set dial
991

992
        Return:
993
            float: current dial position (dimensionless)
994
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
995
996
997
998
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
999

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
1000
    @dial.setter
For faster browsing, not all history is shown. View entire blame