tango_shutter.py 11.8 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
Tango shutter is used to control Tango safety shutter or valve.
10

Valentin Valls's avatar
Valentin Valls committed
11
12
13
14
15
16
17
18
Example yml file:

.. code-block::

    -
      # safety shutter
      class: TangoShutter
      name: safshut
19
      uri: id42/bsh/1
20
21
"""

22
23
from enum import Enum
from gevent import Timeout, sleep
24
from bliss import global_map
25
26
from bliss.common.shutter import BaseShutter, BaseShutterState
from bliss.common.tango import DeviceProxy, DevFailed
27
from bliss.common.logtools import log_warning, user_print
28
29
from bliss.config.channels import Channel
from bliss.common import event
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45


TangoShutterState = Enum(
    "TangoShutterState",
    dict(
        {
            "MOVING": "Moving",
            "DISABLE": "Hutch not searched",
            "STANDBY": "Wait for permission",
            "RUNNING": "Automatic opening",
        },
        **{item.name: item.value for item in BaseShutterState},
    ),
)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
""" FRONTEND

    The state of a Tango FrontEnd server can be (according to JLP):

    * Tango::OPEN:    FrontEnd is open with the automtic mode disabled
    * Tango::RUNNING: FrontEnd is open with the automtic mode enabled
    => OPEN

    * Tango::CLOSE:   FrontEnd is close with the injection mode disabled
    * Tango::STANDBY: FrontEnd is close with the injection mode enabled
    => CLOSED

    * Tango::FAULT:   FrontEnd in fault
    * Tango::DISABLE: No operation permission
    => ???
"""


""" SAFETY SHUTTER

    Overloading of `is_open` and `is_closed` properties to deal with Tango
    states.
    The state of a Safety Shutter Tango server can be:

    * Tango::OPEN
    => OPEN

    * Tango::CLOSE (not CLOSED)
    * Tango::DISABLE
    => CLOSED

    * ON OFF INSERT EXTRACT MOVING STANDBY FAULT INIT RUNNING ALARM UNKNOWN
    => ???
"""

""" Valve

    Overloading of `is_open` and `is_closed` properties to deal with Tango
    Vacuum Valves
    The state of a Valve Tango server can be:

    * Tango::OPEN
    => OPEN

    * Tango::CLOSE (not CLOSED)
    * Tango::DISABLE
    => CLOSED

    * ON OFF INSERT EXTRACT MOVING STANDBY FAULT INIT RUNNING ALARM UNKNOWN
    => ???
"""


TANGO_OPEN_STATES = {
    "SafetyShutter": ["OPEN", "RUNNING"],
    "FrontEnd": ["OPEN", "RUNNING"],
    "Valve": ["OPEN", "RUNNING"],
    "Default": ["OPEN", "RUNNING"],
}

TANGO_CLOSED_STATES = {
    "SafetyShutter": ["CLOSE", "STANDBY", "FAULT"],
    "FrontEnd": ["CLOSE", "STANDBY"],
    "Valve": ["CLOSE", "STANDBY", "FAULT", "DISABLE"],
    "Default": ["CLOSE", "STANDBY"],
}


114
115
class TangoShutter(BaseShutter):
    """ Handle Tango frontend or safety shutter or a valve"""
116

117
118
119
120
121
122
123
124
    def __init__(self, name, config, shutter_type=None):
        self._tango_uri = config.get("uri")

        if shutter_type is None:
            self.__shutter_type = config.get("shutter_type", "Default")
        else:
            self.__shutter_type = shutter_type

125
126
        self.__name = name
        self.__config = config
127
        self.__control = DeviceProxy(self._tango_uri)
128
        global_map.register(self, children_list=[self.__control], tag=f"Shutter:{name}")
129
        self._mode = None
130

131
        self._state_channel = Channel(
132
            f"{name}:state", default_value="UNKNOWN", callback=self._state_changed
133
134
        )

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
    @property
    def shutter_type(self):
        """
        Set / Get shutter type in

        Parameters:
            stype (str): shutter type in "FrontEnd" "SafetyShutter" "Valve"
        """
        return self.__shutter_type

    @shutter_type.setter
    def shutter_type(self, stype):
        self.__shutter_type = stype

    """
    Overloading of `is_open` and `is_closed` properties to deal with Tango
    states.
    """
153

Matias Guijarro's avatar
Matias Guijarro committed
154
    @property
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
    def is_open(self):
        """Check if the Tango Shutter is open"""
        _state = self._tango_state
        _open = _state in TANGO_OPEN_STATES[self.shutter_type]
        _closed = _state in TANGO_CLOSED_STATES[self.shutter_type]
        if _open and _closed:
            user_print(
                f"WARNING: {self.shutter_type} state coherency problem: state is : {_state} (please report this error)"
            )
        if not _open and not _closed:
            user_print(
                f"WARNING: {self.shutter_type} state coherency problem: state is : {_state} (please report this error)"
            )

        return _open
Matias Guijarro's avatar
Matias Guijarro committed
170

171
    @property
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
    def is_closed(self):
        """Check if the Tango Shutter is closed"""
        _state = self._tango_state
        _open = _state in TANGO_OPEN_STATES[self.shutter_type]
        _closed = _state in TANGO_CLOSED_STATES[self.shutter_type]
        if _open and _closed:
            user_print(
                f"WARNING: {self.shutter_type} state coherency problem: state is : {_state} (please report this error)"
            )
        if not _open and not _closed:
            user_print(
                f"WARNING: {self.shutter_type} state coherency problem: state is : {_state} (please report this error)"
            )

        return _closed

    @property
    def proxy(self):
        return self.__control
191

192
193
194
195
196
197
198
199
200
201
202
    @property
    def name(self):
        """A unique name"""
        return self.__name

    @property
    def config(self):
        """Config of shutter"""
        return self.__config

    @property
203
    def _tango_state(self):
204
        """ Read the tango state. PyTango states: 'ALARM', 'CLOSE',
205
206
207
            'DISABLE', 'EXTRACT', 'FAULT', 'INIT', 'INSERT', 'MOVING', 'OFF',
            'ON', 'OPEN', 'RUNNING', 'STANDBY', 'UNKNOWN'.
        Returns:
208
            (str): The state read from the device server.
209
210
        """
        return self.__control.state().name
211

212
    @property
213
214
215
216
217
218
    def _tango_status(self):
        """ Read the status.
        Returns:
            (str): Complete state from the device server.
        """
        return self.__control.status()
219

220
    @property
221
222
223
224
225
226
    def state(self):
        """ Read the state.
        Returns:
            (enum): state as enum
        Raises:
            RuntimeError: If DevFailed from the device server
227
228
229

        Notes:
            Use this state with care: values do not match with Tango States.
230
231
232
233
234
235
236
237
        """
        try:
            state = self._tango_state
            if "CLOSE" in state:
                return TangoShutterState.CLOSED
            return TangoShutterState.__members__[state]
        except DevFailed:
            raise RuntimeError(f"Communication error with {self.__control.dev_name()}")
238

239
    def _state_changed(self, stat):
240
241
242
        """Send a signal when state changes"""
        event.send(self, "state", stat)

243
    @property
244
    def state_string(self):
Valentin Valls's avatar
Valentin Valls committed
245
246
        """Return state as combined string.

247
248
249
250
251
        Returns
            (tuple): state as string, tango status
        """
        return self.state.value, self._tango_status

Emmanuel Papillon's avatar
Emmanuel Papillon committed
252
    def __info__(self):
253
254
255
256
257
258
        if self.is_closed:
            info_str = f"{self.shutter_type} `{self.name}` is closed\n"
        else:
            info_str = f"{self.shutter_type} `{self.name}` is open\n"
        info_str += self._tango_status.rstrip("\n")
        return info_str
Emmanuel Papillon's avatar
Emmanuel Papillon committed
259

260
261
262
263
264
265
266
267
    def open(self, timeout=60):
        """Open
        Args:
            (float): Timeout [s] to wait until execution finished
        Raises:
            RuntimeError: Cannot execute if device in wrong state
        """
        state = self.state
268

269
        if state.name in ("OPEN", "RUNNING"):
270
            log_warning(self, f"{self.name} already open, command ignored")
Matias Guijarro's avatar
Matias Guijarro committed
271
272
        elif state == TangoShutterState.DISABLE:
            log_warning(self, f"{self.name} disabled, command ignored")
273
        elif state == TangoShutterState.CLOSED:
274
            try:
275
                self.__control.open()
276
                self._wait(TangoShutterState.OPEN, timeout)
277
                user_print(f"{self.name} was {state.name} and is now {self.state.name}")
278
279
            except RuntimeError as err:
                raise
280
        else:
281
282
283
            raise RuntimeError(
                f"Cannot open {self.name}, current state is: {state.value}"
            )
284
285
286
287
288
289
290
291
292
293

    def close(self, timeout=60):
        """Close
        Args:
            (float): Timeout [s] to wait until execution finished
        Raises:
            RuntimeError: Cannot execute if device in wrong state
        """
        state = self.state
        if state == TangoShutterState.CLOSED:
294
            log_warning(self, f"{self.name} already closed, command ignored")
Matias Guijarro's avatar
Matias Guijarro committed
295
296
        elif state == TangoShutterState.DISABLE:
            log_warning(self, f"{self.name} disabled, command ignored")
297
        elif state.name in ("OPEN", "RUNNING"):
298
            try:
299
                self.__control.close()
300
                self._wait(TangoShutterState.CLOSED, timeout)
301
                user_print(f"{self.name} was {state.name} and is now {self.state.name}")
302
303
            except RuntimeError as err:
                raise
304
        else:
305
306
307
            raise RuntimeError(
                f"Cannot close {self.name}, current state is: {state.value}"
            )
308

309
310
    @property
    def mode(self):
311
312
313
314
315
316
317
318
        """
        Get or set the opening mode of the FrontEnd.
        state is read from tango attribute: `automatic_mode`.
        Only available for FrontEnd shutters.

        Parameters:
            mode: (str): 'MANUAL' or 'AUTOMATIC'

319
320
321
        Raises:
            NotImplementedError: Not a Frontend shutter
        """
322
        if self.shutter_type != "FrontEnd":
323
324
            raise NotImplementedError("Not a Frontend shutter")

325
326
        try:
            _mode = self.__control.automatic_mode
327
        except AttributeError:
328
            _mode = None
329
        self._mode = "AUTOMATIC" if _mode else "MANUAL" if _mode is False else "UNKNOWN"
330
331
        return self._mode

332
333
    @mode.setter
    def mode(self, mode):
334
        if self.shutter_type != "FrontEnd":
335
336
337
338
339
340
341
            raise NotImplementedError("Not a Frontend shutter")

        try:
            if mode == "MANUAL":
                self.__control.manual()
            elif mode == "AUTOMATIC":
                self.__control.automatic()
342
343
344
            else:
                raise RuntimeError(f"Unknown mode: {mode}")

345
            self._wait_mode(mode=mode)
346
347
        except DevFailed as df_err:
            raise RuntimeError(f"Cannot set {mode} opening") from df_err
348

349
350
351
352
    def reset(self):
        """Reset
        Args:
        Raises:
353
            RuntimeError: Cannot execute
354
355
356
        """
        self.__control.Reset()

357
    def _wait(self, state, timeout=3):
358
359
360
361
362
363
364
365
366
        """Wait execution to finish
        Args:
            (enum): state
            (float): timeout [s].
        Raises:
            RuntimeError: Execution timeout.
        """
        with Timeout(timeout, RuntimeError("Execution timeout")):
            while self.state != state:
367
                sleep(1)
368
            self._state_changed(self.state)
369
370

    def _wait_mode(self, mode, timeout=3):
371
372
373
374
375
376
377
378
379
380
381
382
        """
        Wait until set mode is equal to read mode.
        """
        with Timeout(timeout, RuntimeError(f"Cannot set {mode} opening mode")):

            # FE tango server feature: 'automatic_mode' goes True even
            # if it's not allowed (ex: MDT)
            # It switches back to False after ~1 second.
            # So this method can return without error even if AUTOMATIC
            # mode is not set properly.
            sleep(2)  # to be removed when FE tango server will be fixed.

383
            while self.mode != mode:
384
385
386
387
388
389
                # print(f"{self.mode} != {mode}")    # to be removed when FE tango server will be fixed.
                sleep(0.2)

            # for i in range(100):                   # to be removed when FE tango server will be fixed.
            #     print(f"{self.mode} =?  {mode}")   # to be removed when FE tango server will be fixed.
            #     sleep(0.05)                        # to be removed when FE tango server will be fixed.
390
391
392
393
394
395

    def __enter__(self):
        self.open()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()