axis.py 70.5 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
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
Axis related classes (:class:`~bliss.common.axis.Axis`, \
:class:`~bliss.common.axis.AxisState` and :class:`~bliss.common.axis.Motion`)

These classes are part of the bliss motion subsystem.
They are not to be instantiated directly. They are the objects produced
as calls to :meth:`~bliss.config.static.Config.get`. Example::

    >>> from bliss.config.static import get_config

    >>> cfg = get_config()
    >>> energy = cfg.get('energy')
    >>> energy
    <bliss.common.axis.Axis object at 0x7f7baa7f6d10>

    >>> energy.move(120)
    >>> print(energy.position)
    120.0

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
27
    >>> print energy.state
28
29
    READY (Axis is READY)
"""
30
from bliss import global_map
31
from bliss.common.cleanup import capture_exceptions
32
33
from bliss.common.motor_config import StaticConfig
from bliss.common.motor_settings import AxisSettings
34
from bliss.common import event
35
from bliss.common.greenlet_utils import protect_from_one_kill
36
from bliss.common.utils import with_custom_members
37
from bliss.config.channels import Channel
38
from bliss.common.logtools import log_debug, lprint, lprint_disable
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
39
from bliss.common.utils import rounder
40

41
import enum
42
import gevent
43
import re
44
import sys
45
import math
46
import functools
47
import numpy
48
from unittest import mock
49
import warnings
50
51

warnings.simplefilter("once", DeprecationWarning)
52

53

54
#: Default polling time
55
DEFAULT_POLLING_TIME = 0.02
Matias Guijarro's avatar
Matias Guijarro committed
56

57

58
class GroupMove:
59
    def __init__(self, parent=None):
60
61
        self.parent = parent
        self._move_task = None
62
63
64
        self._motions_dict = dict()
        self._stop_motion = None
        self._user_stopped = False
65
66
67
68
69
70
71
72

    # Public API

    @property
    def is_moving(self):
        # A greenlet evaluates to True when not dead
        return bool(self._move_task)

73
74
75
76
77
78
79
80
81
    def move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func=None,
        wait=True,
        polling_time=DEFAULT_POLLING_TIME,
    ):
82
83
84
        self._motions_dict = motions_dict
        self._stop_motion = stop_motion
        self._user_stopped = False
85
86
        started = gevent.event.Event()
        self._move_task = gevent.spawn(
87
88
89
90
91
92
93
94
            self._move,
            motions_dict,
            start_motion,
            stop_motion,
            move_func,
            started,
            polling_time,
        )
95

96
97
98
99
100
        for _, motions in motions_dict.items():
            for motion_obj in motions:
                msg = motion_obj.user_msg
                if msg:
                    lprint(msg)
101
102
103
        try:
            # Wait for the move to be started (or finished)
            gevent.wait([started, self._move_task], count=1)
104
        except BaseException:
105
106
            self.stop()
            raise
107
108
        # Wait if necessary and raise the move task exception if any
        if wait or self._move_task.ready():
109
            self.wait()
110
111
112

    def wait(self):
        if self._move_task is not None:
113
114
            try:
                self._move_task.get()
115
            except BaseException:
116
117
                self.stop()
                raise
118
119

    def stop(self, wait=True):
120
121
122
123
124
125
        with capture_exceptions(raise_index=0) as capture:
            if self._move_task is not None:
                with capture():
                    self._stop_move(self._motions_dict, self._stop_motion)
                if wait:
                    self._move_task.get()
126
127
128

    # Internal methods

129
    def _monitor_move(self, motions_dict, move_func, polling_time):
130
        monitor_move = dict()
Vincent Michel's avatar
Vincent Michel committed
131
        for controller, motions in motions_dict.items():
132
            for motion in motions:
133
                if move_func is None:
134
135
136
137
                    move_func = "_handle_move"
                task = gevent.spawn(
                    getattr(motion.axis, move_func), motion, polling_time
                )
138
139
                monitor_move[motion] = task
        try:
140
            gevent.joinall(monitor_move.values(), raise_error=True)
141
142
        finally:
            # update the last motor state
Vincent Michel's avatar
Vincent Michel committed
143
            for motion, task in monitor_move.items():
144
145
                try:
                    motion.last_state = task.get(block=False)
146
                except BaseException:
147
                    pass
148
149

    def _stop_move(self, motions_dict, stop_motion):
150
        self._user_stopped = True
151
        stop = []
Vincent Michel's avatar
Vincent Michel committed
152
        for controller, motions in motions_dict.items():
153
            stop.append(gevent.spawn(stop_motion, controller, motions))
154
155
156
157
        # Raise exception if any, when all the stop tasks are finished
        for task in gevent.joinall(stop):
            task.get()

158
159
    def _stop_wait(self, motions_dict, exception_capture):
        stop_wait = []
Vincent Michel's avatar
Vincent Michel committed
160
        for controller, motions in motions_dict.items():
161
162
163
            for motion in motions:
                stop_wait.append(gevent.spawn(motion.axis._move_loop))
        gevent.joinall(stop_wait)
164
        task_index = 0
Vincent Michel's avatar
Vincent Michel committed
165
        for controller, motions in motions_dict.items():
166
167
168
169
            for motion in motions:
                with exception_capture():
                    motion.last_state = stop_wait[task_index].get()
                task_index += 1
170

171
    @protect_from_one_kill
172
173
    def _do_backlash_move(self, motions_dict, polling_time):
        backlash_move = []
Vincent Michel's avatar
Vincent Michel committed
174
        for controller, motions in motions_dict.items():
175
176
            for motion in motions:
                if motion.backlash:
177
178
179
180
181
                    if self._user_stopped:
                        # have to recalculate target: do backlash from where it stopped
                        motion.target_pos = (
                            motion.axis.dial * motion.axis.steps_per_unit
                        )
182
183
184
185
186
187
188
189
190
191
                    backlash_motion = Motion(
                        motion.axis,
                        motion.target_pos + motion.backlash,
                        motion.backlash,
                    )
                    backlash_move.append(
                        gevent.spawn(
                            motion.axis._backlash_move, backlash_motion, polling_time
                        )
                    )
192
        gevent.joinall(backlash_move)
193
        gevent.joinall(backlash_move, raise_error=True)
194

195
196
197
198
199
200
201
202
203
    def _move(
        self,
        motions_dict,
        start_motion,
        stop_motion,
        move_func,
        started_event,
        polling_time,
    ):
204
        # Set axis moving state
Vincent Michel's avatar
Vincent Michel committed
205
        for motions in motions_dict.values():
206
            for motion in motions:
207
                motion.last_state = None
208
                motion.axis._set_moving_state()
209

Vincent Michel's avatar
Vincent Michel committed
210
                for _, chan in motion.axis._beacon_channels.items():
211
                    chan.unregister_callback(chan._setting_update_cb)
212
        with capture_exceptions(raise_index=0) as capture:
213
214
            try:
                # Spawn start motion for all controllers
215
216
                start = [
                    gevent.spawn(start_motion, controller, motions)
Vincent Michel's avatar
Vincent Michel committed
217
                    for controller, motions in motions_dict.items()
218
                ]
219

220
                # Wait for the controllers to be started
221
                with capture():
222
223
                    gevent.joinall(start, raise_error=True)
                if capture.failed:
224
                    gevent.joinall(start)
225
226
227
                    # start failed, stop all axes and wait end of motion
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
228

229
230
                    self._stop_wait(motions_dict, capture)
                    return
231

232
233
234
235
236
237
238
                # All the controllers are now started
                started_event.set()

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

                # Spawn the monitoring for all motions
239
                with capture():
240
241
                    self._monitor_move(motions_dict, move_func, polling_time)
                if capture.failed:
242
243
244
245
246
247
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)

                # Do backlash move, if needed
                with capture():
248
249
                    self._do_backlash_move(motions_dict, polling_time)
                if capture.failed:
250
251
252
253
                    with capture():
                        self._stop_move(motions_dict, stop_motion)
                    self._stop_wait(motions_dict, capture)
            finally:
254
                reset_setpos = capture.failed or self._user_stopped
255

256
257
258
259
                # cleanup
                # -------
                # update final state ; in case of exception
                # state is set to FAULT
Vincent Michel's avatar
Vincent Michel committed
260
                for motions in motions_dict.values():
261
                    for motion in motions:
262
263
264
265
                        state = motion.last_state
                        if state is not None:
                            continue

266
                        with capture():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
267
                            state = motion.axis.hw_state
268
269
270
                        if state is None:
                            state = AxisState("FAULT")
                        # update state and update dial pos.
271
272
                        with capture():
                            motion.axis._update_settings(state)
273
274
275
276
277
278
279
280
281

                # 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
282
                if len(motions_dict) == 1:
Vincent Michel's avatar
Vincent Michel committed
283
                    motion = motions_dict[list(motions_dict.keys()).pop()][0]
284
                    if motion.type == "jog":
285
                        reset_setpos = False
286
287
288
289
                        motion.axis._jog_cleanup(
                            motion.saved_velocity, motion.reset_position
                        )
                    elif motion.type == "homing":
290
                        reset_setpos = True
291
                    elif motion.type == "limit_search":
292
293
                        reset_setpos = True
                if reset_setpos:
294
                    with capture():
Vincent Michel's avatar
Vincent Michel committed
295
                        for motions in motions_dict.values():
296
                            for motion in motions:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
297
                                motion.axis._set_position = motion.axis.position
298
299
                                event.send(motion.axis, "sync_hard")

Vincent Michel's avatar
Vincent Michel committed
300
                for motions in motions_dict.values():
301
302
303
304
                    for motion in motions:
                        with capture():
                            motion.axis._Axis__execute_post_move_hook([motion])

Vincent Michel's avatar
Vincent Michel committed
305
                        for _, chan in motion.axis._beacon_channels.items():
306
307
308
309
310
                            chan.register_callback(chan._setting_update_cb)

                        motion.axis._set_move_done()
                if self.parent:
                    event.send(self.parent, "move_done", True)
311

Cyril Guilloud's avatar
Cyril Guilloud committed
312

313
class Modulo:
314
315
316
317
    def __init__(self, mod=360):
        self.modulo = mod

    def __call__(self, axis):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
318
        dial_pos = axis.dial
319
        axis._Axis__do_set_dial(dial_pos % self.modulo)
320
321


322
class Motion:
323
324
325
326
327
328
329
330
    """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
331

332
333
    Note: target_pos and delta can be None, in case of specific motion
    types like homing or limit search
334
    """
Matias Guijarro's avatar
Matias Guijarro committed
335

336
337
338
    def __init__(
        self, axis, target_pos, delta, motion_type="move", user_target_pos=None
    ):
Matias Guijarro's avatar
Matias Guijarro committed
339
        self.__axis = axis
340
        self.__type = motion_type
341
        self.user_target_pos = user_target_pos
Matias Guijarro's avatar
Matias Guijarro committed
342
343
344
        self.target_pos = target_pos
        self.delta = delta
        self.backlash = 0
Matias Guijarro's avatar
Matias Guijarro committed
345

Matias Guijarro's avatar
Matias Guijarro committed
346
347
    @property
    def axis(self):
348
        """Reference to :class:`Axis`"""
Matias Guijarro's avatar
Matias Guijarro committed
349
        return self.__axis
350

351
352
353
354
    @property
    def type(self):
        return self.__type

355
356
357
358
    @property
    def user_msg(self):
        start_ = rounder(self.axis.tolerance, self.axis.position)
        if self.type == "jog":
Cyril Guilloud's avatar
Cyril Guilloud committed
359
360
361
362
363
364
            msg = (
                f"Moving {self.axis.name} from {start_} until it is stopped, at constant velocity: {self.target_pos}\n"
                f"To stop it: {self.axis.name}.stop()"
            )
            return msg

365
366
367
368
369
370
371
372
373
374
375
        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
376

377
378
class Trajectory(object):
    """ Trajectory information
Matias Guijarro's avatar
Matias Guijarro committed
379

380
381
382
    Represents a specific trajectory motion.

    """
Matias Guijarro's avatar
Matias Guijarro committed
383

Matias Guijarro's avatar
Matias Guijarro committed
384
    def __init__(self, axis, pvt):
Matias Guijarro's avatar
Matias Guijarro committed
385
386
387
388
389
390
391
        """
        Args:
            axis -- axis to which this motion corresponds to
            pvt  -- numpy array with three fields ('position','velocity','time')
        """
        self.__axis = axis
        self.__pvt = pvt
392
393
394
395
        self._events_positions = numpy.empty(
            0, dtype=[("position", "f8"), ("velocity", "f8"), ("time", "f8")]
        )

396
397
398
399
400
401
402
    @property
    def axis(self):
        return self.__axis

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

404
405
    @property
    def events_positions(self):
406
        return self._events_positions
407

408
409
    @events_positions.setter
    def events_positions(self, events):
410
        self._events_positions = events
411

412
413
414
    def has_events(self):
        return self._events_positions.size

415
416
417
418
419
420
421
    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
        """
422
423
        user_pos = self.__pvt["position"]
        user_velocity = self.__pvt["velocity"]
424
        pvt = numpy.copy(self.__pvt)
425
426
        pvt["position"] = self.axis.user2dial(user_pos) * self.axis.steps_per_unit
        pvt["velocity"] *= self.axis.steps_per_unit
427
        new_obj = self.__class__(self.axis, pvt)
428
        pattern_evts = numpy.copy(self._events_positions)
429
430
        pattern_evts["position"] *= self.axis.steps_per_unit
        pattern_evts["velocity"] *= self.axis.steps_per_unit
431
        new_obj._events_positions = pattern_evts
432
        return new_obj
433
434


435
class CyclicTrajectory(Trajectory):
436
437
438
439
440
441
442
443
    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)
444
445
        self.nb_cycles = nb_cycles
        self.origin = origin
446
447
448
449
450

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

451
452
453
    @property
    def events_pattern_positions(self):
        return super(CyclicTrajectory, self).events_positions
454

455
456
    @events_pattern_positions.setter
    def events_pattern_positions(self, values):
457
458
        self._events_positions = values

459
460
461
462
    @property
    def is_closed(self):
        """True if the trajectory is closed (first point == last point)"""
        pvt = self.pvt_pattern
463
464
465
466
        return (
            pvt["time"][0] == 0
            and pvt["position"][0] == pvt["position"][len(self.pvt_pattern) - 1]
        )
467
468
469

    @property
    def pvt(self):
470
        """Return the full PVT table. Positions are absolute"""
471
472
473
474
475
476
477
478
479
480
481
482
483
484
        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
485
        for cycle in range(self.nb_cycles):
486
            start = cycle_size * cycle + offset
487
488
            end = start + cycle_size
            pvt[start:end] = raw_pvt
489
490
491
492
            pvt["time"][start:end] += last_time
            last_time = pvt["time"][end - 1]
            pvt["position"][start:end] += last_position
            last_position = pvt["position"][end - 1]
493
494

        if self.is_closed:
495
496
            pvt["time"][0] = pvt_pattern["time"][0]
            pvt["position"][0] = pvt_pattern["position"][0] + self.origin
497
498
499

        return pvt

500
501
502
    @property
    def events_positions(self):
        pattern_evts = self.events_pattern_positions
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
503
        time_offset = 0.0
504
        last_time = self.pvt_pattern["time"][-1]
505
        nb_pattern_evts = len(pattern_evts)
506
507
508
        all_events = numpy.empty(
            self.nb_cycles * len(pattern_evts), dtype=pattern_evts.dtype
        )
509
        for i in range(self.nb_cycles):
510
511
512
            sub_evts = all_events[
                i * nb_pattern_evts : i * nb_pattern_evts + nb_pattern_evts
            ]
513
            sub_evts[:] = pattern_evts
514
            sub_evts["time"] += time_offset
515
516
517
            time_offset += last_time
        return all_events

518
519
520
521
    def convert_to_dial(self):
        """
        Return a new trajectory with pvt position, velocity converted to dial units and steps per unit
        """
522
523
524
525
        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
526
527


528
529
530
531
532
def lazy_init(func):
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
        self.controller._initialize_axis(self)
        return func(self, *args, **kwargs)
533

534
    return func_wrapper
535

Matias Guijarro's avatar
Matias Guijarro committed
536

537
@with_custom_members
538
class Axis:
539
540
541
542
543
544
545
    """
    Bliss motor axis

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

546
547
    READ_POSITION_MODE = enum.Enum("Axis.READ_POSITION_MODE", "CONTROLLER ENCODER")

Matias Guijarro's avatar
Matias Guijarro committed
548
549
550
    def __init__(self, name, controller, config):
        self.__name = name
        self.__controller = controller
551
        self.__settings = AxisSettings(self)
Matias Guijarro's avatar
Matias Guijarro committed
552
        self.__move_done = gevent.event.Event()
553
        self.__move_done_callback = gevent.event.Event()
Matias Guijarro's avatar
Matias Guijarro committed
554
        self.__move_done.set()
555
        self.__move_done_callback.set()
556
557
        self.__motion_hooks = []
        for hook in config.get("motion_hooks", []):
558
            hook._add_axis(self)
559
560
            self.__motion_hooks.append(hook)
        self.__encoder = config.get("encoder")
561
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
562
        self.__init_config_properties()
563
        self._group_move = GroupMove()
564
        self._beacon_channels = dict()
565
566
567
568
569
        self._move_stop_channel = Channel(
            "axis.%s.move_stop" % self.name,
            default_value=False,
            callback=self._external_stop,
        )
570
        self._lock = gevent.lock.Semaphore()
571
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
572

573
574
575
576
577
578
579
580
581
582
583
584
585
        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
        for settings_name in disabled_cache:
            self.settings.disable_cache(settings_name)
586
        self._unit = self.config.get("unit", str, None)
587
        global_map.register(self, parents_list=["axes", controller])
588

589
    def __close__(self):
590
591
592
593
594
595
596
        try:
            controller_close = self.__controller.close
        except AttributeError:
            pass
        else:
            controller_close()

597
598
599
600
601
602
603
604
    @property
    def no_offset(self):
        return self.__no_offset

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

605
606
607
608
609
    @property
    def unit(self):
        """Axis name"""
        return self._unit

Matias Guijarro's avatar
Matias Guijarro committed
610
611
    @property
    def name(self):
612
        """Axis name"""
Matias Guijarro's avatar
Matias Guijarro committed
613
614
615
616
        return self.__name

    @property
    def controller(self):
617
        """Reference to :class:`~bliss.controllers.motor.Controller`"""
Matias Guijarro's avatar
Matias Guijarro committed
618
619
620
621
        return self.__controller

    @property
    def config(self):
622
        """Reference to the :class:`~bliss.common.motor_config.StaticConfig`"""
Matias Guijarro's avatar
Matias Guijarro committed
623
624
625
626
        return self.__config

    @property
    def settings(self):
627
628
629
630
        """
        Reference to the
        :class:`~bliss.controllers.motor_settings.AxisSettings`
        """
Matias Guijarro's avatar
Matias Guijarro committed
631
632
633
634
        return self.__settings

    @property
    def is_moving(self):
635
636
637
        """
        Tells if the axis is moving (:obj:`bool`)
        """
Matias Guijarro's avatar
Matias Guijarro committed
638
639
        return not self.__move_done.is_set()

Matias Guijarro's avatar
Matias Guijarro committed
640
641
642
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
643
644
        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
645
646
647
648
649
650
651
652
653
654
655
656
657
        if velocity:
            if self.controller.axis_settings.config_setting["velocity"]:
                self.__config_velocity = self.config.get("velocity", float)
        if acceleration:
            if self.controller.axis_settings.config_setting["acceleration"]:
                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)
658

659
660
    @property
    def steps_per_unit(self):
661
        """Current steps per unit (:obj:`float`)"""
662
        return self.__steps_per_unit
663

664
    @property
Matias Guijarro's avatar
Matias Guijarro committed
665
666
667
668
669
670
    def config_backlash(self):
        """Current backlash in user units (:obj:`float`)"""
        return self.__config_backlash

    @property
    @lazy_init
671
672
    def backlash(self):
        """Current backlash in user units (:obj:`float`)"""
Matias Guijarro's avatar
Matias Guijarro committed
673
674
675
676
677
678
679
680
        backlash = self.settings.get("backlash")
        if backlash is None:
            return 0
        return backlash

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

682
683
    @property
    def tolerance(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
684
        """Current Axis tolerance in dial units (:obj:`float`)"""
685
        return self.__tolerance
686

Matias Guijarro's avatar
Matias Guijarro committed
687
688
    @property
    def encoder(self):
689
690
691
692
        """
        Reference to :class:`~bliss.common.encoder.Encoder` or None if no
        encoder is defined
        """
693
        return self.__encoder
694

695
696
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
697
698
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
699

700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
    @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
    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)

742
    def set_setting(self, *args):
743
        """Sets the given settings"""
744
745
746
        self.settings.set(*args)

    def get_setting(self, *args):
747
        """Return the values for the given settings"""
748
749
        return self.settings.get(*args)

Matias Guijarro's avatar
Matias Guijarro committed
750
    def has_tag(self, tag):
751
752
753
754
755
756
        """
        Tells if the axis has the given tag

        Args:
            tag (str): tag name

757
        Return:
758
759
            bool: True if the axis has the tag or False otherwise
        """
Vincent Michel's avatar
Vincent Michel committed
760
        for t, axis_list in self.__controller._tagged.items():
Matias Guijarro's avatar
Matias Guijarro committed
761
762
763
764
765
766
            if t != tag:
                continue
            if self.name in [axis.name for axis in axis_list]:
                return True
        return False

767
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
768
    def on(self):
769
        """Turns the axis on"""
770
771
772
        if self.is_moving:
            return

Matias Guijarro's avatar
Matias Guijarro committed
773
        self.__controller.set_on(self)
774
        state = self.__controller.state(self)
775
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
776

777
    @lazy_init
blissadm@ID16NI's avatar
blissadm@ID16NI committed
778
    def off(self):
779
        """Turns the axis off"""
780
781
782
        if self.is_moving:
            raise RuntimeError("Can't set power off while axis is moving")

Matias Guijarro's avatar
Matias Guijarro committed
783
784
        self.__controller.set_off(self)
        state = self.__controller.state(self)
785
        self.settings.set("state", state)
blissadm@ID16NI's avatar
blissadm@ID16NI committed
786

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
787
788
789
790
791
792
793
794
795
796
797
    @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
798
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
799
    def _set_position(self, new_set_pos):
800
801
802
        new_set_pos = float(
            new_set_pos
        )  # accepts both float or numpy array of 1 element
803
        self.settings.set("_set_position", new_set_pos)
804

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
805
    @property
806
    @lazy_init
Cyril Guilloud's avatar
Cyril Guilloud committed
807
808
    def measured_position(self):
        """
809
        Return the encoder value in user units.
810

811
        Return:
812
            float: encoder value in user units
Cyril Guilloud's avatar
Cyril Guilloud committed
813
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
814
        return self.dial2user(self.dial_measured_position)
815

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
816
    @property
817
    @lazy_init
818
    def dial_measured_position(self):
819
        """
820
        Return the dial encoder position.
821

822
        Return:
823
            float: dial encoder position
824
        """
825
826
827
828
        if self.encoder is not None:
            return self.encoder.read()
        else:
            raise RuntimeError("Axis '%s` has no encoder." % self.name)
829

830
    def __do_set_dial(self, new_dial):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
831
        user_pos = self.position
832
        old_dial = self.dial
833

834
835
836
837
838
839
840
841
842
        # 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
843
        self.settings.set("dial_position", dial_pos)
844

845
846
847
848
849
850
        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)
851

852
853
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
854
    @property
855
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
856
    def dial(self):
857
        """
858
        Return current dial position, or set dial
859

860
        Return:
861
            float: current dial position (dimensionless)
862
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
863
864
865
866
        dial_pos = self.settings.get("dial_position")
        if dial_pos is None:
            dial_pos = self._update_dial()
        return dial_pos
867

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
868
869
870
    @dial.setter
    @lazy_init
    def dial(self, new_dial):
871
        if self.is_moving:
872
873
874
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
875
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
876
877
878
        old_dial = self.dial
        new_dial = self.__do_set_dial(new_dial)
        lprint(f"'{self.name}` dial position reset from {old_dial} to {new_dial}")
879

880
881
882
883
884
885
    def __do_set_position(self, new_pos=None, offset=None):
        dial = self.dial
        curr_offset = self.offset
        if offset is None:
            # calc offset
            offset = new_pos - self.sign * dial
886
887
888
889
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
890
891
892
893
894
895
896
        if math.isclose(offset, 0):
            offset = 0
        if not math.isclose(curr_offset, offset):
            self.settings.set("offset", offset)
        if new_pos is None:
            # calc pos from offset
            new_pos = self.sign * dial + offset
897
898
899
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
900
901
902
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed
903

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
904
    @property
905
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
906
    def position(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
907
        """
908
        Return current user position, or set new user position
909

910
        Return:
911
            float: current user position (user units)
Cyril Guilloud's avatar
Cyril Guilloud committed
912
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
913
914
915
916
        pos = self.settings.get("position")
        if pos is None:
            pos = self.dial2user(self.dial)
        return pos
917

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
918
919
920
    @position.setter
    @lazy_init
    def position(self, new_pos):
921
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)
922
        if self.is_moving:
923
924
925
            raise RuntimeError(
                "%s: can't set axis user position " "while moving" % self.name
            )
926
        new_pos = float(new_pos)  # accepts both float or numpy array of 1 element
927
928
929
930
931
932
933
        curr_pos = self.position
        if self.no_offset:
            self.dial = new_pos
        if self.__do_set_position(new_pos):
            lprint(
                f"'{self.name}` position reset from {curr_pos} to {new_pos} (sign: {self.sign}, offset: {self.offset})"
            )
934

Vincent Michel's avatar
Vincent Michel committed
935
    @lazy_init
936
    def _update_dial(self, update_user=True):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
937
        dial_pos = self._hw_position
938
        self.settings.set("dial_position", dial_pos)
939
940
        if update_user:
            user_pos = self.dial2user(dial_pos, self.offset)
941
            self.settings.set("position", user_pos)
942
943
        return dial_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
944
    @property
945
    @lazy_init
Matias's avatar
Matias committed
946
    def _hw_position(self):
947
948
949
        if self._read_position_mode == self.READ_POSITION_MODE.ENCODER:
            return self.dial_measured_position

Matias's avatar
Matias committed
950
        try:
951
            curr_pos = self.__controller.read_position(self) / self.steps_per_unit
Matias's avatar
Matias committed
952
953
954
955
956
957
        except NotImplementedError:
            # this controller does not have a 'position'
            # (e.g like some piezo controllers)
            curr_pos = 0
        return curr_pos

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
958
    @property
959
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
960
    def state(self):
961
        """
962
        Return the axis state
963
964
965
966

        Keyword Args:
            read_hw (bool): read from hardware [default: False]

967
        Return:
968
969
            AxisState: axis state
        """
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
970
971
972
        if self.is_moving:
            return AxisState("MOVING")
        state = self.settings.get("state")