axis.py 80.1 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.protocols import Scannable
15
from bliss.common.cleanup import capture_exceptions
16
from bliss.common.motor_config import MotorConfig
17
from bliss.common.motor_settings import AxisSettings
18
from bliss.common import event
19
from bliss.common.greenlet_utils import protect_from_one_kill
20
from bliss.common.utils import with_custom_members, safe_get
21
from bliss.config.channels import Channel
22
from bliss.common.logtools import log_debug, user_print
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
23
from bliss.common.utils import rounder
24
from bliss.common.utils import autocomplete_property
25

26
import enum
27
import gevent
28
import re
29
import sys
30
import math
31
import functools
32
import collections
33
import numpy
34
from unittest import mock
35
import warnings
36
37

warnings.simplefilter("once", DeprecationWarning)
38

39

40
#: Default polling time
41
DEFAULT_POLLING_TIME = 0.02
Matias Guijarro's avatar
Matias Guijarro committed
42

43

44
45
46
47
class AxisOnLimitError(RuntimeError):
    pass


48
49
50
51
class AxisFaultError(RuntimeError):
    pass


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)


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

    # Public API

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

92
93
94
    def move(
        self,
        motions_dict,
95
        prepare_motion,
96
97
98
99
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
100
        polling_time=None,
101
    ):
102
103
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
104
        self._interrupted_move = False
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
139

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

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

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

169
        started = gevent.event.Event()
170

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

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

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

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

    # Internal methods

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

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

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

230
        stop_tasks = []
Vincent Michel's avatar
Vincent Michel committed
231
        for controller, motions in motions_dict.items():
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
260
            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()
261

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

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

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

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

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

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

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

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

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

349
                motion.axis.settings.unregister_channels_callbacks()
350

351
        with capture_exceptions(raise_index=0) as capture:
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
387
            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
388
                    )
389
390
391
392
393
                elif motion.type == "homing":
                    reset_setpos = True
                elif motion.type == "limit_search":
                    reset_setpos = True
            if reset_setpos:
394
                with capture():
395
396
397
398
399
400
401
402
403
404
405
406
407
408
                    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)

409
                    axis.settings.register_channels_callbacks()
410

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

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

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

Cyril Guilloud's avatar
Cyril Guilloud committed
429

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

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


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

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

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

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

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

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

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

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

498
499
500
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
501

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

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

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

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

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

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

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


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

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

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

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

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

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

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

        return pvt

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

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


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

652
    return func_wrapper
653

Matias Guijarro's avatar
Matias Guijarro committed
654

655
@with_custom_members
656
class Axis(Scannable):
657
658
659
660
661
662
663
    """
    Bliss motor axis

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

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

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

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

Matias Guijarro's avatar
linting    
Matias Guijarro committed
704
705
706
707
708
709
710
711
712
713
714
715
716
        # 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,
        )

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

725
726
727
728
729
730
731
732
    @property
    def no_offset(self):
        return self.__no_offset

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

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

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

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

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

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

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

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

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

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

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

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

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

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

827
828
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
829
830
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
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
858
    @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
859
    @lazy_init
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
    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)

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

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

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

        Args:
            tag (str): tag name

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

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

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

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

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

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

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

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

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

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

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

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

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

985
986
        return dial_pos

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

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

For faster browsing, not all history is shown. View entire blame