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
562
        if self.__encoder is not None:
            self.__encoder.axis = self
563
        self.__config = StaticConfig(config)
Matias Guijarro's avatar
Matias Guijarro committed
564
        self.__init_config_properties()
565
        self._group_move = GroupMove()
566
        self._beacon_channels = dict()
567
568
569
570
571
        self._move_stop_channel = Channel(
            "axis.%s.move_stop" % self.name,
            default_value=False,
            callback=self._external_stop,
        )
572
        self._lock = gevent.lock.Semaphore()
573
        self.__no_offset = False
Matias Guijarro's avatar
Matias Guijarro committed
574

575
576
577
578
579
580
581
582
583
584
585
586
587
        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)
588
        self._unit = self.config.get("unit", str, None)
589
        global_map.register(self, parents_list=["axes", controller])
590

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

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

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

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

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

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

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

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

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

Matias Guijarro's avatar
Matias Guijarro committed
642
643
644
    def __init_config_properties(
        self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True
    ):
645
646
        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
647
648
649
650
651
652
653
654
655
656
657
658
659
        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)
660

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

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

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

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

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

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

697
698
    @property
    def motion_hooks(self):
Matias Guijarro's avatar
Matias Guijarro committed
699
700
        """Registered motion hooks (:obj:`MotionHook`)"""
        return self.__motion_hooks
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
742
743
    @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)

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

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

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

        Args:
            tag (str): tag name

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

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

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

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

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

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

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

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

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

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

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

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

847
848
849
850
851
852
        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)
853

854
855
        return dial_pos

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

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
870
871
872
    @dial.setter
    @lazy_init
    def dial(self, new_dial):
873
        if self.is_moving:
874
875
876
            raise RuntimeError(
                "%s: can't set axis dial position " "while moving" % self.name
            )
877
        new_dial = float(new_dial)  # accepts both float or numpy array of 1 element
878
879
880
        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}")
881

882
883
884
885
886
887
    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
888
889
890
891
        if math.isnan(offset):
            # this can happen if dial is nan;
            # cannot continue
            return False
892
893
894
895
896
897
898
        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
899
900
901
        if math.isnan(new_pos):
            # do not allow to assign nan as a user position
            return False
902
903
904
        self.settings.set("position", new_pos)
        self._set_position = new_pos
        return True
Cyril Guilloud's avatar
Cyril Guilloud committed
905

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

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

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
920
921
922
    @position.setter
    @lazy_init
    def position(self, new_pos):
923
        log_debug(self, "axis.py : position(new_pos=%r)" % new_pos)
924
        if self.is_moving:
925
926
927
            raise RuntimeError(
                "%s: can't set axis user position " "while moving" % self.name
            )
928
        new_pos = float(new_pos)  # accepts both float or numpy array of 1 element
929
930
931
932
933
934
935
        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})"
            )
936

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

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

Matias's avatar
Matias committed
952
        try:
953
            curr_pos = self.__controller.read_position(self) / self.steps_per_unit
Matias's avatar
Matias committed
954
955
956
957
958
959
        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
960
    @property
961
    @lazy_init
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
962
    def state(self):
963
        """
964
        Return the axis state
965
966
967
968

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

969
        Return:
970
971
            AxisState: axis state
        """