axis.py 79.9 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
44
45
46
class AxisOnLimitError(RuntimeError):
    pass


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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)


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

    # Public API

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

87
88
89
    def move(
        self,
        motions_dict,
90
        prepare_motion,
91
92
93
94
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
95
        polling_time=None,
96
    ):
97
98
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
99
        self._interrupted_move = False
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
130
131
132
133
134

        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
135
        # some axes can **become** ready thanks to the hook
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
        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)
154

155
            for motion_obj in motions:
156
157
158
159
                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

160
161
162
163
                msg = motion_obj.user_msg
                if msg:
                    lprint(msg)

164
        started = gevent.event.Event()
165

166
        self._move_task = gevent.spawn(
167
            self._move, motions_dict, start_motion, stop_motion, move_func, started
168
        )
169

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

    def wait(self):
        if self._move_task is not None:
182
183
            try:
                self._move_task.get()
184
            except BaseException:
185
186
                self.stop()
                raise
187
188

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

    # Internal methods

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

207
        try:
208
209
210
211
212
213
214
215
216
217
218
219
220
            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()
221

222
223
    def _stop_move(self, motions_dict, stop_motion, stop_wait_tasks=None, wait=True):
        self._interrupted_move = True
224

225
        stop_tasks = []
Vincent Michel's avatar
Vincent Michel committed
226
        for controller, motions in motions_dict.items():
227
228
229
230
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
            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()
256

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

278
279
280
281
282
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
283
                    backlash_motions[controller].append(backlash_motion)
284

285
286
287
288
289
290
291
292
293
        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,
            )
294

295
296
297
298
    def _do_move(
        self, motions_dict, start_motion, stop_motion, move_func, started_event
    ):
        for controller, motions in motions_dict.items():
299
            for motion in motions:
300
                motion.last_state = None
Matias Guijarro's avatar
Matias Guijarro committed
301

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

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

327
328
            # All controllers are now started
            if started_event is not None:
329
330
                started_event.set()

331
332
            if self.parent:
                event.send(self.parent, "move_done", False)
333

334
335
336
337
338
339
340
341
342
343
            # 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()

344
                motion.axis.settings.unregister_channels_callbacks()
345

346
        with capture_exceptions(raise_index=0) as capture:
347
348
349
350
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
            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
383
                    )
384
385
386
387
388
                elif motion.type == "homing":
                    reset_setpos = True
                elif motion.type == "limit_search":
                    reset_setpos = True
            if reset_setpos:
389
                with capture():
390
391
392
393
394
395
396
397
398
399
400
401
402
403
                    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)

404
                    axis.settings.register_channels_callbacks()
405

406
                    # set move done
407
408
409
410
411
412
413
414
415
416
417
418
                    motion.axis._set_move_done()

            if self._interrupted_move:
                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}")

            try:
                if self.parent:
                    event.send(self.parent, "move_done", True)
419
            finally:
420
                for hook, motions in reversed(list(hooks.items())):
421
                    with capture():
422
                        hook.post_move(motions)
423

Cyril Guilloud's avatar
Cyril Guilloud committed
424

425
class Modulo:
426
427
428
429
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
430
        dial_pos = axis.dial
431
        axis._Axis__do_set_dial(dial_pos % self.modulo)
432
433


434
class Motion:
435
436
437
438
439
440
441
442
    """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
443

444
445
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
446
    """
Matias Guijarro's avatar
Matias Guijarro committed
447

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

Matias Guijarro's avatar
Matias Guijarro committed
459
460
    @property
    def axis(self):
461
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
462
        return self.__axis
463

464
465
466
467
    @property
    def type(self):
        return self.__type

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

478
479
480
481
482
483
484
485
486
487
488
        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
489

Matias Guijarro's avatar
linting    
Matias Guijarro committed
490
class Trajectory:
491
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
492

493
494
495
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
496

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

509
510
511
512
513
514
515
    @property
    def axis(self):
        return self.__axis

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

517
518
    @property
    def events_positions(self):
519
        return self._events_positions
520

521
522
    @events_positions.setter
    def events_positions(self, events):
523
        self._events_positions = events
524

525
526
527
    def has_events(self):
        return self._events_positions.size

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


548
class CyclicTrajectory(Trajectory):
549
550
551
552
553
554
555
556
    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)
557
558
        self.nb_cycles = nb_cycles
        self.origin = origin
559
560
561
562
563

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

564
565
566
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
567

568
569
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
570
571
        self._events_positions = values

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

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

        if self.is_closed:
608
609
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
610
611
612

        return pvt

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

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


641
642
643
644
645
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        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:
652
653
654
655
656
657
658
    """
    Bliss motor axis

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

659
660
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

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

682
683
684
685
686
687
688
689
690
691
692
        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
693
694
        for setting_name in disabled_cache:
            self.settings.disable_cache(setting_name)
695
        self._unit = self.config.get("unit", str, None)
696
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
697
        global_map.register(self, parents_list=["axes", controller])
698

Matias Guijarro's avatar
linting    
Matias Guijarro committed
699
700
701
702
703
704
705
706
707
708
709
710
711
        # 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,
        )

712
    def __close__(self):
713
714
715
716
717
718
719
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

720
721
722
723
724
725
726
727
    @property
    def no_offset(self):
        return self.__no_offset

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

728
729
730
731
732
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
733
734
    @property
    def name(self):
735
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
736
737
        return self.__name

738
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
739
    def controller(self):
740
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
741
742
743
744
        return self.__controller

    @property
    def config(self):
745
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
746
747
748
749
        return self.__config

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

    @property
    def is_moving(self):
758
759
760
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
761
762
        return not self.__move_done.is_set()

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

786
787
    @property
    def steps_per_unit(self):
788
        """Current steps per unit (:obj:`float`)"""
789
        return self.__steps_per_unit
790

791
    @property
Matias Guijarro's avatar
Matias Guijarro committed
792
793
794
795
796
797
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

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

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

809
810
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
811
        """Current Axis tolerance in dial units (:obj:`float`)"""
812
        return self.__tolerance
813

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

822
823
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
824
825
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
826

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

870
    def set_setting(self, *args):
871
        """Sets the given settings"""
872
873
874
        self.settings.set(*args)

    def get_setting(self, *args):
875
        """Return the values for the given settings"""
876
877
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
878
    def has_tag(self, tag):
879
880
881
882
883
884
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

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

895
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
896
    def on(self):
897
        """Turns the axis on"""
898
899
900
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
901
        self.__controller.set_on(self)
902
        state = self.__controller.state(self)
903
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
904

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

Matias Guijarro's avatar
Matias Guijarro committed
911
912
        self.__controller.set_off(self)
        state = self.__controller.state(self)
913
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
914

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
933
    @property
934
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
935
936
    def measured_position(self):
        """
937
        Return the encoder value in user units.
938

939
        Return:
940
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
941
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
942
        return self.dial2user(self.dial_measured_position)
943

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
944
    @property
945
    @lazy_init
946
    def dial_measured_position(self):
947
        """
948
        Return the dial encoder position.
949

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

958
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
959
        user_pos = self.position
960
        old_dial = self.dial
961

962
963
964
965
966
967
968
969
970
        # 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
971
        self.settings.set("dial_position", dial_pos)
972

973
974
975
976
977
978
        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)
979

980
981
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
982
    @property
983
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
984
    def dial(self):
985
        """
986
        Return current dial position, or set dial
987

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
996
    @dial.setter
997
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
998
    def dial(self, new_dial):
999
        if self.is_moving:
1000
            raise RuntimeError(
For faster browsing, not all history is shown. View entire blame