axis.py 83.6 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
def float_or_inf(value, inf_sign=1):
    if value is None:
        value = float("inf")
        sign = math.copysign(1, inf_sign)
    else:
        sign = 1
    value = float(value)  # accepts float or numpy array of 1 element
    return sign * value


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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)


86
class GroupMove:
87
    def __init__(self, parent=None):
88
89
        self.parent = parent
        self._move_task = None
90
91
        self._motions_dict = dict()
        self._stop_motion = None
92
93
        self._interrupted_move = False
        self._backlash_started_event = gevent.event.Event()
94
95
96
97
98

    # Public API

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

102
103
104
    def move(
        self,
        motions_dict,
105
        prepare_motion,
106
107
108
109
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
110
        polling_time=None,
111
    ):
112
113
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
114
        self._interrupted_move = False
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
140
141
142
143
144
145
146
147
148
149

        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
150
        # some axes can **become** ready thanks to the hook
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
        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)
169

170
            for motion_obj in motions:
171
172
173
174
                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

175
176
                msg = motion_obj.user_msg
                if msg:
177
                    user_print(msg)
178

179
        started = gevent.event.Event()
180

181
        self._move_task = gevent.spawn(
182
            self._move, motions_dict, start_motion, stop_motion, move_func, started
183
        )
184

185
186
187
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
188
        except BaseException:
189
190
            self.stop()
            raise
191
192
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
193
            self.wait()
194
195
196

    def wait(self):
        if self._move_task is not None:
197
198
            try:
                self._move_task.get()
199
            except BaseException:
200
201
                self.stop()
                raise
202
203

    def stop(self, wait=True):
204
205
206
        with capture_exceptions(raise_index=0) as capture:
            if self._move_task is not None:
                with capture():
207
                    self._stop_move(self._motions_dict, self._stop_motion, wait=False)
208
209
                if wait:
                    self._move_task.get()
210
211
212

    # Internal methods

213
214
    def _monitor_move(self, motions_dict, move_func, stop_func):
        monitor_move_tasks = {}
Vincent Michel's avatar
Vincent Michel committed
215
        for controller, motions in motions_dict.items():
216
            for motion in motions:
217
                if move_func is None:
218
                    move_func = "_handle_move"
219
                task = gevent.spawn(getattr(motion.axis, move_func), motion)
220
221
                monitor_move_tasks[task] = motion

222
        try:
223
224
225
226
227
228
229
230
231
232
233
234
235
            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()
236

237
238
    def _stop_move(self, motions_dict, stop_motion, stop_wait_tasks=None, wait=True):
        self._interrupted_move = True
239

240
        stop_tasks = []
Vincent Michel's avatar
Vincent Michel committed
241
        for controller, motions in motions_dict.items():
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
            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()
271

272
    @protect_from_one_kill
273
    def _do_backlash_move(self, motions_dict):
274
        backlash_motions = collections.defaultdict(list)
Vincent Michel's avatar
Vincent Michel committed
275
        for controller, motions in motions_dict.items():
276
277
            for motion in motions:
                if motion.backlash:
278
279
                    if self._interrupted_move:
                        # have to recalculate target: do backlash move from where it stopped
280
281
282
                        motion.target_pos = (
                            motion.axis.dial * motion.axis.steps_per_unit
                        )
283
284
285
286
287
288
289
290
291
292
                        # 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

293
294
295
296
297
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
298
                    backlash_motions[controller].append(backlash_motion)
299

300
301
302
303
304
305
306
307
308
        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,
            )
309

310
311
312
313
    def _do_move(
        self, motions_dict, start_motion, stop_motion, move_func, started_event
    ):
        for controller, motions in motions_dict.items():
314
            for motion in motions:
315
                motion.last_state = None
Matias Guijarro's avatar
Matias Guijarro committed
316

317
        with capture_exceptions(raise_index=0) as capture:
318
319
320
321
322
            # Spawn start motion tasks for all controllers
            start = [
                gevent.spawn(start_motion, controller, motions)
                for controller, motions in motions_dict.items()
            ]
323

Matias Guijarro's avatar
Matias Guijarro committed
324
325
326
            # wait for start tasks to be all done ;
            # in case of error or if wait is interrupted (ctrl-c, kill...),
            # immediately stop and return
327
328
329
330
331
332
333
334
            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
335
                with capture():
336
                    gevent.joinall(start)
337
338
339
340
                # then, stop all axes and wait end of motion
                self._stop_move(motions_dict, stop_motion)
                # exit
                return
341

342
343
            # All controllers are now started
            if started_event is not None:
344
345
                started_event.set()

346
347
            if self.parent:
                event.send(self.parent, "move_done", False)
348

349
350
351
352
353
354
355
356
357
358
            # 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()

359
                motion.axis.settings.unregister_channels_callbacks()
360

361
        with capture_exceptions(raise_index=0) as capture:
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
388
389
390
391
392
393
394
395
396
397
            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
398
                    )
399
400
401
402
403
                elif motion.type == "homing":
                    reset_setpos = True
                elif motion.type == "limit_search":
                    reset_setpos = True
            if reset_setpos:
404
                with capture():
405
406
407
408
409
410
411
412
413
414
415
416
417
418
                    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)

419
                    axis.settings.register_channels_callbacks()
420

421
                    # set move done
422
423
424
                    motion.axis._set_move_done()

            if self._interrupted_move:
425
                user_print("")
426
427
428
                for motion in motions:
                    _axis = motion.axis
                    _axis_pos = safe_get(_axis, "position", on_error="!ERR")
429
                    user_print(f"Axis {_axis.name} stopped at position {_axis_pos}")
430
431
432
433

            try:
                if self.parent:
                    event.send(self.parent, "move_done", True)
434
            finally:
435
                for hook, motions in reversed(list(hooks.items())):
436
                    with capture():
437
                        hook.post_move(motions)
438

Cyril Guilloud's avatar
Cyril Guilloud committed
439

440
class Modulo:
441
442
443
444
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
445
        dial_pos = axis.dial
446
        axis._Axis__do_set_dial(dial_pos % self.modulo)
447
448


449
class Motion:
450
451
452
453
454
455
456
457
    """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
458

459
460
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
461
    """
Matias Guijarro's avatar
Matias Guijarro committed
462

463
464
465
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
466
        self.__axis = axis
467
        self.__type = motion_type
468
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
469
470
471
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
472
        self.polling_time = DEFAULT_POLLING_TIME
Matias Guijarro's avatar
Matias Guijarro committed
473

Matias Guijarro's avatar
Matias Guijarro committed
474
475
    @property
    def axis(self):
476
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
477
        return self.__axis
478

479
480
481
482
    @property
    def type(self):
        return self.__type

483
484
485
486
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
487
            msg = (
488
                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
489
490
491
492
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

493
494
495
496
497
498
499
500
501
502
503
        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
504

Matias Guijarro's avatar
linting    
Matias Guijarro committed
505
class Trajectory:
506
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
507

508
509
510
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
511

Matias Guijarro's avatar
Matias Guijarro committed
512
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
513
514
515
516
517
518
519
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
520
521
522
523
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

524
525
526
527
528
529
530
    @property
    def axis(self):
        return self.__axis

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

532
533
    @property
    def events_positions(self):
534
        return self._events_positions
535

536
537
    @events_positions.setter
    def events_positions(self, events):
538
        self._events_positions = events
539

540
541
542
    def has_events(self):
        return self._events_positions.size

543
544
545
546
547
548
549
    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
        """
550
551
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
552
        pvt = numpy.copy(self.__pvt)
553
554
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
555
        new_obj = self.__class__(self.axis, pvt)
556
        pattern_evts = numpy.copy(self._events_positions)
557
558
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
559
        new_obj._events_positions = pattern_evts
560
        return new_obj
561
562


563
class CyclicTrajectory(Trajectory):
564
565
566
567
568
569
570
571
    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)
572
573
        self.nb_cycles = nb_cycles
        self.origin = origin
574
575
576
577
578

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

579
580
581
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
582

583
584
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
585
586
        self._events_positions = values

587
588
589
590
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
591
592
593
594
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
595
596
597

    @property
    def pvt(self):
598
        """Return the full PVT table. Positions are absolute"""
599
600
601
602
603
604
605
606
607
608
609
610
611
612
        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
613
        for cycle in range(self.nb_cycles):
614
            start = cycle_size * cycle + offset
615
616
            end = start + cycle_size
            pvt[start:end] = raw_pvt
617
618
619
620
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
621
622

        if self.is_closed:
623
624
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
625
626
627

        return pvt

628
629
630
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
631
        time_offset = 0.0
632
        last_time = self.pvt_pattern["time"][-1]
633
        nb_pattern_evts = len(pattern_evts)
634
635
636
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
637
        for i in range(self.nb_cycles):
638
639
640
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
641
            sub_evts[:] = pattern_evts
642
            sub_evts["time"] += time_offset
643
644
645
            time_offset += last_time
        return all_events

646
647
648
649
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
650
651
652
653
        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
654
655


656
657
658
659
660
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
661

662
    return func_wrapper
663

Matias Guijarro's avatar
Matias Guijarro committed
664

665
@with_custom_members
666
class Axis(Scannable):
667
668
669
670
671
672
673
    """
    Bliss motor axis

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

674
675
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
676
677
678
679
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
        self.__move_done = gevent.event.Event()
680
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
681
        self.__move_done.set()
682
        self.__move_done_callback.set()
683
684
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
685
            hook._add_axis(self)
686
687
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
688
689
        if self.__encoder is not None:
            self.__encoder.axis = self
690
        self.__config = MotorConfig(config)
691
        self.__settings = AxisSettings(self)
692
        self._init_config_properties()
Matias Guijarro's avatar
linting    
Matias Guijarro committed
693
        self.__no_offset = False
694
        self._group_move = GroupMove()
695
        self._lock = gevent.lock.Semaphore()
696
        self.__positioner = True
Matias Guijarro's avatar
Matias Guijarro committed
697

698
699
700
701
702
703
704
705
706
707
708
        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
709
710
        for setting_name in disabled_cache:
            self.settings.disable_cache(setting_name)
711
        self._unit = self.config.get("unit", str, None)
712
        self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME)
713
        global_map.register(self, parents_list=["axes", controller])
714

Matias Guijarro's avatar
linting    
Matias Guijarro committed
715
716
717
718
719
720
721
722
723
724
725
726
727
        # 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,
        )

728
    def __close__(self):
729
730
731
732
733
734
735
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

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

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

744
745
746
747
748
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
749
750
    @property
    def name(self):
751
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
752
753
        return self.__name

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

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

763
    @autocomplete_property
Matias Guijarro's avatar
Matias Guijarro committed
764
    def controller(self):
765
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
766
767
768
769
        return self.__controller

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

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

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

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

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

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

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

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

840
841
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
842
        """Current Axis tolerance in dial units (:obj:`float`)"""
843
        return self.__tolerance
844

Matias Guijarro's avatar
Matias Guijarro committed
845
846
    @property
    def encoder(self):
847
848
849
850
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
851
        return self.__encoder
852

853
854
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
855
856
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
857

858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
    @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
885
    @lazy_init
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
    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)

901
    def set_setting(self, *args):
902
        """Sets the given settings"""
903
904
905
        self.settings.set(*args)

    def get_setting(self, *args):
906
        """Return the values for the given settings"""
907
908
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
909
    def has_tag(self, tag):
910
911
912
913
914
915
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

916
        Return:
917
918
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
919
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
920
921
922
923
924
925
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

926
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
927
    def on(self):
928
        """Turns the axis on"""
929
930
931
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
932
        self.__controller.set_on(self)
933
        state = self.__controller.state(self)
934
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
935

936
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
937
    def off(self):
938
        """Turns the axis off"""
939
940
941
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
942
943
        self.__controller.set_off(self)
        state = self.__controller.state(self)
944
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
945

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
946
947
948
949
950
951
952
953
954
955
956
    @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
957
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
958
    def _set_position(self, new_set_pos):
959
960
961
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
962
        self.settings.set("_set_position", new_set_pos)
963

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
964
    @property
965
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
966
967
    def measured_position(self):
        """
968
        Return the encoder value in user units.
969

970
        Return:
971
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
972
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
973
        return self.dial2user(self.dial_measured_position)
974

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
975
    @property
976
    @lazy_init
977
    def dial_measured_position(self):
978
        """
979
        Return the dial encoder position.
980

981
        Return:
982
            float: dial encoder position
983
        """
984
985
986
987
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
988

989
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
990
        user_pos = self.position
991
        old_dial = self.dial
992

993
994
995
996
997
998
999
1000
        # 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)
For faster browsing, not all history is shown. View entire blame