musst.py 36.9 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
import time
9
import weakref
10
import os
11
import hashlib
12
import functools
Perceval Guillou's avatar
Perceval Guillou committed
13
14
15
import numpy
import gevent

Perceval Guillou's avatar
Perceval Guillou committed
16
17
from itertools import chain

18
from bliss import global_map
19
from bliss.comm import get_comm
Perceval Guillou's avatar
Perceval Guillou committed
20
from bliss.common.utils import autocomplete_property
21
from bliss.common.greenlet_utils import KillMask, protect_from_kill
22
from bliss.config.channels import Cache
23
from bliss.config.conductor.client import remote_open
24
from bliss.common.switch import Switch as BaseSwitch
25
from bliss.controllers.counter import CounterController
26
from bliss.scanning.acquisition.musst import MusstDefaultAcquisitionMaster
27
28
from bliss.common.counter import Counter, SamplingCounter
from bliss.controllers.counter import SamplingCounterController, counter_namespace
Perceval Guillou's avatar
Perceval Guillou committed
29
from bliss.controllers.bliss_controller import BlissController
30

31
32

def _get_simple_property(command_name, doc_sring):
33
34
35
    def get(self):
        return self.putget("?%s" % command_name)

36
37
38
39
40
41
42
    def set(self, value):
        return self.putget("%s %s" % (command_name, value))

    return property(get, set, doc=doc_sring)


def _simple_cmd(command_name, doc_sring):
43
44
    def exec_cmd(self):
        return self.putget(command_name)
45
46
47

    return property(exec_cmd, doc=doc_sring)

48

49
50
51
52
53
54
def _clear_cmd():
    def exec_cmd(self):
        try:
            return self.putget("CLEAR")
        finally:
            self._musst__last_md5.value = None
55

56
57
    return property(exec_cmd, doc="Delete the current program")

58

59
60
61
62
63
64
65
66
67
68
def _reset_cmd():
    def exec_cmd(self):
        try:
            return self.putget("RESET")
        finally:
            self._musst__last_md5.value = None

    return property(exec_cmd, doc="Musst reset")


69
70
71
def lazy_init(func):
    @functools.wraps(func)
    def f(self, *args, **kwargs):
Perceval Guillou's avatar
Perceval Guillou committed
72
73
        if self._channels is None:
            self._channels_init(self.config)
74
75
76
77
78
        return func(self, *args, **kwargs)

    return f


Perceval Guillou's avatar
Perceval Guillou committed
79
class MusstSamplingCounter(SamplingCounter):
Perceval Guillou's avatar
Perceval Guillou committed
80
81
    def __init__(self, name, channel, convert, controller, **kwargs):
        SamplingCounter.__init__(self, name, controller, **kwargs)
82
        self.channel = channel
83
        self.convert = convert
Perceval Guillou's avatar
Perceval Guillou committed
84
85


86
class MusstIntegratingCounter(Counter):
Perceval Guillou's avatar
Perceval Guillou committed
87
88
    def __init__(self, name, channel, convert, controller, **kwargs):
        super().__init__(name, controller, **kwargs)
Perceval Guillou's avatar
Perceval Guillou committed
89
        self.channel = channel
90
        self.convert = convert
Perceval Guillou's avatar
Perceval Guillou committed
91
92
93


class MusstSamplingCounterController(SamplingCounterController):
94
95
96
    def __init__(self, musst):
        super().__init__(musst.name, register_counters=False)
        self.musst_ctrl = musst
97
98
        # High frequency acquisition loop
        self.max_sampling_frequency = None
Perceval Guillou's avatar
Perceval Guillou committed
99
100
101
102
103

    def read_all(self, *counters):
        """ return the values of the given counters as a list.
            If possible this method should optimize the reading of all counters at once.
        """
104
        return self.musst_ctrl.read_all(*counters)
Perceval Guillou's avatar
Perceval Guillou committed
105
106


107
108
109
110
class MusstIntegratingCounterController(CounterController):
    def __init__(self, musst):
        super().__init__(name=musst.name, register_counters=False)
        self.musst = musst
111

Linus Pithan's avatar
Linus Pithan committed
112
    def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
113
114
        return MusstDefaultAcquisitionMaster(
            self, self.musst, ctrl_params=ctrl_params, **acq_params
Perceval Guillou's avatar
Perceval Guillou committed
115
116
        )

117
118
    def get_default_chain_parameters(self, scan_params, acq_params):
        return {"count_time": acq_params.get("count_time", scan_params["count_time"])}
119

120

Perceval Guillou's avatar
Perceval Guillou committed
121
class musst(BlissController):
122
    class channel(object):
Vincent Michel's avatar
Vincent Michel committed
123
        COUNTER, ENCODER, SSI, ADC10, ADC5, SWITCH = list(range(6))
124
125

        def __init__(self, musst, channel_id, type=None, switch=None, switch_name=None):
126
127
            self._musst = weakref.ref(musst)
            self._channel_id = channel_id
Cyril Guilloud's avatar
Cyril Guilloud committed
128
            self._mode_number = None
129
            self._string2mode = {
130
131
132
133
134
135
                "CNT": self.COUNTER,
                "ENCODER": self.ENCODER,
                "SSI": self.SSI,
                "ADC10": self.ADC10,
                "ADC5": self.ADC5,
                "SWITCH": self.SWITCH,
Cyril Guilloud's avatar
Cyril Guilloud committed
136
                "ENC": self.ENCODER,
137
            }
Cyril Guilloud's avatar
Cyril Guilloud committed
138
139
140
141
142
143
144
145
146
            self._mode2string = [
                "CNT",
                "ENCODER",
                "SSI",
                "ADC10",
                "ADC5",
                "SWITCH",
                "ENC",
            ]
147
            if type is not None:
Vincent Michel's avatar
Vincent Michel committed
148
                if isinstance(type, str):
149
150
151
                    MODE = type.upper()
                    mode = self._string2mode.get(MODE)
                    if mode is None:
152
                        raise RuntimeError("musst: mode (%s) is not known" % type)
Cyril Guilloud's avatar
Cyril Guilloud committed
153
                    self._mode_number = mode
154
                else:
Cyril Guilloud's avatar
Cyril Guilloud committed
155
                    self._mode_number = type
156
            if switch is not None:
157
                # check if has the good interface
158
                if switch_name is None:
159
160
161
162
163
164
165
166
167
                    raise RuntimeError(
                        "musst: channel (%d) with external switch musst have a switch_name defined"
                        % channel_id
                    )
                if not hasattr(switch, "set"):
                    raise RuntimeError(
                        "musst: channel (%d), switch object doesn't have a set method"
                        % channel_id
                    )
168
                self._switch = switch
169
170
171
172
                self._switch_name = switch_name
            else:
                self._switch = None

173
174
        @property
        def mode(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
175
176
177
178
179
            return self._mode_number

        @property
        def mode_str(self):
            return self._mode2string[self._mode_number]
180

181
182
        @property
        def value(self):
183
            if self._switch is not None:
184
                self._switch.set(self._switch_name)
185
186
187
188
189
            musst = self._musst()
            string_value = musst.putget("?CH CH%d" % self._channel_id).split()[0]
            return self._convert(string_value)

        @value.setter
190
        def value(self, val):
191
            if self._switch is not None:
192
                self._switch.set(self._switch_name)
193
            musst = self._musst()
194
            musst.putget("CH CH%d %s" % (self._channel_id, val))
195
196
197
198
199
200
201

        @property
        def status(self):
            musst = self._musst()
            status_string = musst.putget("?CH CH%d" % self._channel_id).split()[1]
            return musst._string2state.get(status_string)

Cyril Guilloud's avatar
Cyril Guilloud committed
202
203
204
205
206
207
        @property
        def status_string(self):
            musst = self._musst()
            status_string = musst.putget("?CH CH%d" % self._channel_id).split()[1]
            return status_string

208
209
        @property
        def channel_id(self):
210
211
            if self._switch is not None:
                self._switch.set(self._switch_name)
212
            return self._channel_id
213

214
215
216
217
        @property
        def switch(self):
            return self._switch

218
219
220
221
222
        def run(self, program_name=None):
            if program_name is None:
                self._cnt_cmd("RUN")
            else:
                self._cnt_cmd("RUN %s" % program_name)
223
224
225

        def stop(self):
            self._cnt_cmd("STOP")
226
227

        def _cnt_cmd(self, cmd):
228
            self._read_config()
Cyril Guilloud's avatar
Cyril Guilloud committed
229
            if self._mode_number == self.COUNTER or self._mode_number == self.ENCODER:
230
                musst = self._musst()
231
                musst.putget("CH CH%d %s" % (self._channel_id, cmd))
232
            else:
233
234
235
236
                raise RuntimeError(
                    "%s command on "
                    "channel %d is not allowed in this mode" % (cmd, self._channel_id)
                )
237

238
        def _convert(self, string_value):
Cyril Guilloud's avatar
Cyril Guilloud committed
239
240
            """Return channel value, converted according to the configured mode.
            """
241
            self._read_config()
Cyril Guilloud's avatar
Cyril Guilloud committed
242
            if self._mode_number == self.COUNTER:
243
                return int(string_value)
Cyril Guilloud's avatar
Cyril Guilloud committed
244
            elif self._mode_number == self.ADC10:
245
                return int(string_value) * (10. / 0x7fffffff)
Cyril Guilloud's avatar
Cyril Guilloud committed
246
            elif self._mode_number == self.ADC5:
247
                return int(string_value) * (5. / 0x7fffffff)
248
            else:  # not managed yet
249
                return int(string_value)
250

251
        def _read_config(self):
Cyril Guilloud's avatar
Cyril Guilloud committed
252
            """Read configuration of the current channel from MUSST board to
Cyril Guilloud's avatar
Cyril Guilloud committed
253
254
            determine the usage mode of the channel.
            Fill self._mode_number attribute.
Cyril Guilloud's avatar
Cyril Guilloud committed
255
            """
Cyril Guilloud's avatar
Cyril Guilloud committed
256
            if self._mode_number is None:
257
258
259
                musst = self._musst()
                string_config = musst.putget("?CHCFG CH%d" % self._channel_id)
                split_config = string_config.split()
Cyril Guilloud's avatar
Cyril Guilloud committed
260
261
                self._mode_number = self._string2mode.get(split_config[0])
                if self._mode_number == self.ADC10:  # TEST if it's not a 5 volt ADC
262
                    if len(split_config) > 1 and split_config[1].find("5") > -1:
Cyril Guilloud's avatar
Cyril Guilloud committed
263
                        self._mode_number = self.ADC5
264

265
266
267
268
269
270
271
272
273
274
    ADDR = _get_simple_property("ADDR", "Set/query serial line address")
    BTRIG = _get_simple_property(
        "BTRIG", "Set/query the level of the TRIG out B output signal"
    )
    NAME = _get_simple_property("NAME", "Set/query module name")
    EBUFF = _get_simple_property("EBUFF", "Set/ query current event buffer")
    HBUFF = _get_simple_property("HBUFF", "Set/ query current histogram buffer")

    ABORT = _simple_cmd("ABORT", "Program abort")
    STOP = _simple_cmd("STOP", "Program stop")
275
    RESET = _reset_cmd()
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
    CONT = _simple_cmd(
        "CONT", "Continue the program when stopped in STOP or BREAK states"
    )
    CLEAR = _clear_cmd()
    LIST = _simple_cmd("?LIST CODE", "List the current program")
    LISTVAR = _simple_cmd("?LIST VAR", "List the current program")
    DBINFO = _simple_cmd("?DBINFO *", "Returns the list of installed daughter boards")
    HELP = _simple_cmd("?HELP", "Query list of available commands")
    INFO = _simple_cmd("?INFO", "Query module configuration")
    RETCODE = _simple_cmd("?RETCODE", "Query exit or stop code")
    TIMER = _simple_cmd("?TIMER", "Query timer")
    VAL = _simple_cmd("?VAL", "Query values")

    VARINIT = _simple_cmd("VARINIT", "Reset program variables")

    # STATE
Vincent Michel's avatar
Vincent Michel committed
292
293
    NOPROG_STATE, BADPROG_STATE, IDLE_STATE, RUN_STATE, BREAK_STATE, STOP_STATE, ERROR_STATE = list(
        range(7)
294
295
    )
    # FREQUENCY TIMEBASE
Vincent Michel's avatar
Vincent Michel committed
296
    F_1KHZ, F_10KHZ, F_100KHZ, F_1MHZ, F_10MHZ, F_50MHZ = list(range(6))
297

Perceval Guillou's avatar
Perceval Guillou committed
298
    def __init__(self, config):
299
300
        """Base Musst controller.

Perceval Guillou's avatar
Perceval Guillou committed
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
            args: 
                config: controller configuration as ConfigNode or dict

            config keywords:

                musst_prg_root      -- default path for musst programs
                block_size          -- default is 8k but can be lowered to 512 depend on gpib.
                one_line_programing -- default is False we send several lines to program the musst
              
                gpib:
                    url             -- url of the gpib controller i.s:enet://gpib0.esrf.fr
                    pad             -- primary address of the musst controller
                    timeout         -- communication timeout in seconds, default is 1s
                    eos             -- end of line termination
                
                channels:           -- list of configured channels
                    label:          -- name alias for a channel
                    type:           -- channel type (cnt, encoder, ssi, adc5, adc10 and switch)
                    channel:        -- channel number
                    name:           -- use to reference an external switch
                    counter_name:   -- associate a counter to that channel (optional) 
                    counter_mode:   -- sampling counter mode (optional)
323
        """
324

Perceval Guillou's avatar
Perceval Guillou committed
325
        super().__init__(config)
326

327
        self._string2state = {
328
329
330
331
332
333
334
335
            "NOPROG": self.NOPROG_STATE,
            "BADPROG": self.BADPROG_STATE,
            "IDLE": self.IDLE_STATE,
            "RUN": self.RUN_STATE,
            "BREAK": self.BREAK_STATE,
            "STOP": self.STOP_STATE,
            "ERROR": self.ERROR_STATE,
        }
336

337
        self.__frequency_conversion = {
338
339
340
341
342
343
344
345
346
347
348
349
350
            self.F_1KHZ: ("1KHZ", 1e3),
            self.F_10KHZ: ("10KHZ", 10e3),
            self.F_100KHZ: ("100KHZ", 100e3),
            self.F_1MHZ: ("1MHZ", 1e6),
            self.F_10MHZ: ("10MHZ", 10e6),
            self.F_50MHZ: ("50MHZ", 50e6),
            "1KHZ": self.F_1KHZ,
            "10KHZ": self.F_10KHZ,
            "100KHZ": self.F_100KHZ,
            "1MHZ": self.F_1MHZ,
            "10MHZ": self.F_10MHZ,
            "50MHZ": self.F_50MHZ,
        }
351

352
        self._channels = None
353
        self._timer_factor = None
354

355
        self._last_run = time.time()
Perceval Guillou's avatar
Perceval Guillou committed
356
357
358
359
360
361

        # === CounterControllers
        self._counter_controllers = {}
        self._counter_controllers["scc"] = MusstSamplingCounterController(self)
        self._counter_controllers["icc"] = MusstIntegratingCounterController(self)

362
        global_map.register(self, parents_list=["counters"])
363

Perceval Guillou's avatar
Perceval Guillou committed
364
365
366
367
368
369
370
371
372
373
374
375
    def _load_config(self):

        self.__last_md5 = Cache(self, "last__md5")
        self.__event_buffer_size = Cache(self, "event_buffer_size")
        self.__prg_root = self.config.get("musst_prg_root")
        self.__block_size = self.config.get("block_size", 8 * 1024)
        self.__one_line_programing = self.config.get(
            "one_line_programing", "serial_url" in self.config
        )

        max_freq = self.config.get("max_sampling_frequency")
        self._counter_controllers["scc"].max_sampling_frequency = max_freq
Perceval Guillou's avatar
Perceval Guillou committed
376

Perceval Guillou's avatar
Perceval Guillou committed
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
    def _init(self):
        """
            Place holder for any action to perform after the configuration has been loaded.
        """
        gpib = self.config.get("gpib")
        comm_opts = dict()
        if gpib:
            gpib["eol"] = ""
            comm_opts["timeout"] = 5
            self._txterm = b""
            self._rxterm = b"\n"
            self._binary_data_read = True
        else:
            self._txterm = b"\r"
            self._rxterm = b"\r\n"
            self._binary_data_read = False

        self._cnx = get_comm(self.config, **comm_opts)

Perceval Guillou's avatar
Perceval Guillou committed
396
397
398
399
    def _get_default_chain_counter_controller(self):
        return self._counter_controllers["icc"]

    def _channels_init(self, config):
Perceval Guillou's avatar
Perceval Guillou committed
400
401
        """ Handle configured channels """

402
        self._channels = dict()
Perceval Guillou's avatar
Perceval Guillou committed
403
        channels_list = config.get("channels", list())
404
        for channel_config in channels_list:
405
            channel_number = channel_config.get("channel")
406
            channel = None
407
408
            if channel_number is None:
                raise RuntimeError("musst: channel in config must have a channel")
Perceval Guillou's avatar
Perceval Guillou committed
409
410
411
412
413
414
            elif channel_number in range(1, 7):
                channel_type = channel_config.get("type")
                if channel_type in ("cnt", "encoder", "ssi", "adc5", "adc10"):
                    channel_name = channel_config.get("label")
                    if channel_name is None:
                        raise RuntimeError("musst: channel in config must have a label")
415
                    channels = self._channels.setdefault(channel_name.upper(), list())
416
417
                    channel = self.get_channel(channel_number, type=channel_type)
                    channels.append(channel)
Perceval Guillou's avatar
Perceval Guillou committed
418
419
420
421
422
423
424
425
426
427
                elif channel_type == "switch":
                    ext_switch = channel_config.get("name")
                    if not hasattr(ext_switch, "states_list"):
                        raise RuntimeError(
                            "musst: channels (%s) switch object must have states_list method"
                            % channel_number
                        )
                    for channel_name in ext_switch.states_list():
                        channels = self._channels.setdefault(
                            channel_name.upper(), list()
428
                        )
Perceval Guillou's avatar
Perceval Guillou committed
429
430
431
432
433
434
435
436
                        channels.append(
                            self.get_channel(
                                channel_number,
                                type=channel_type,
                                switch=ext_switch,
                                switch_name=channel_name,
                            )
                        )
437
438
439
                # will read the musst config to know the type
                elif channel_type is None:
                    channel = self.get_channel(channel_number)
Perceval Guillou's avatar
Perceval Guillou committed
440
441
442
                else:
                    raise ValueError(
                        "musst: channel type can only be one of: (cnt,encoder,ssi,adc5,adc10,switch)"
443
444
                    )

445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
            cnt_name = channel_config.get("counter_name")
            if cnt_name is not None:
                if channel is None:  # Must be TIMER
                    cnt_channel = "TIMER"
                    channel_type = self.channel.COUNTER

                    def convert(string_value):
                        if self._timer_factor is None:
                            self._timer_factor = self.get_timer_factor()
                        return int(string_value) / self._timer_factor

                else:
                    if channel_type == "switch":
                        # need to get the real type of musst channel
                        # so force musst config reading
                        channel = self.get_channel(channel_number)

                    cnt_channel = "CH%d" % channel._channel_id
                    channel._read_config()  # to fill the channel type
                    channel_type = channel._mode_number
                    if channel_type == "switch":

                        def convert(string_value):
                            value = channel._convert(string_value)
                            if hasattr(ext_switch, "convert"):
                                return ext_switch.convert(value)
                            return value

                    else:
                        convert = channel._convert
Perceval Guillou's avatar
Perceval Guillou committed
475

476
                if channel_type == self.channel.COUNTER:
Perceval Guillou's avatar
Perceval Guillou committed
477
478
                    self._counter_controllers["icc"].create_counter(
                        MusstIntegratingCounter, cnt_name, cnt_channel, convert
479
480
                    )
                else:
Perceval Guillou's avatar
Perceval Guillou committed
481
482
                    cnt_mode = channel_config.get("counter_mode", "MEAN")
                    self._counter_controllers["scc"].create_counter(
Perceval Guillou's avatar
Perceval Guillou committed
483
                        MusstSamplingCounter,
Perceval Guillou's avatar
Perceval Guillou committed
484
485
486
487
                        cnt_name,
                        cnt_channel,
                        convert,
                        mode=cnt_mode,
488
489
                    )

Cyril Guilloud's avatar
Cyril Guilloud committed
490
    @lazy_init
491
492
493
    def __info__(self):
        """Default method called by the 'BLISS shell default typing helper'
        """
494
        version = self.putget("?VER")
495
496
497
        timebase = self.putget("?TMRCFG")
        hmem, hbuf = self.putget("?HSIZE").split(" ")
        emem, ebuf = self.putget("?ESIZE").split(" ")
498
499
        info_str = f"MUSST card: {self.name}, {version}\n"
        info_str += self._cnx.__info__() + "\n"
500
501
502
503
504
505
506
507
        info_str += f"TIMEBASE: {timebase}\n"
        info_str += "MEMORY:\n"
        info_str += (
            f"         MCA:     size (32b values): {hmem:>8}, buffers: {hbuf:>8}\n"
        )
        info_str += (
            f"         STORAGE: size (32b values): {emem:>8}, buffers: {ebuf:>8}\n"
        )
508
        info_str += "CHANNELS:\n"
Cyril Guilloud's avatar
Cyril Guilloud committed
509

510
511
512
513
514
515
516
517
518
519
520
        # CYRIL [13]: musst_sxm.get_channel(6).status_string
        #  Out [13]: 'STOP'

        for ii in range(6):
            ch_idx = ii + 1
            ch_value, ch_status = self.putget(f"?CH CH{ch_idx}").split(" ")
            ch_config = self.putget(f"?CHCFG CH{ch_idx}")
            info_str += (
                f"         CH{ch_idx} ({ch_status:>4}): {ch_value:>10} -  {ch_config}\n"
            )

521
        return info_str
522

523
    @protect_from_kill
524
    def putget(self, msg, ack=False):
525
526
527
528
529
        """ Raw connection to the Musst card.

        msg -- the message you want to send
        ack -- if True, wait the an acknowledge (synchronous)
        """
530
531
532
533
        if ack is True and not (msg.startswith("?") or msg.startswith("#")):
            msg = "#" + msg

        ack = msg.startswith("#")
534
535
536

        with self._cnx._lock:
            self._cnx.open()
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
537
            self._cnx._write(msg.encode() + self._txterm)
538
            if msg.startswith("?") or ack:
539
                answer = self._cnx._readline(self._rxterm)
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
540
541
                if answer == b"$":
                    return self._cnx._readline(b"$" + self._rxterm).decode()
542
                elif ack:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
543
                    if answer != b"OK":
544
545
                        raise RuntimeError("%s: invalid answer: %r", self.name, answer)
                    return True
546
                else:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
547
                    return answer.decode()
548

549
550
    def _wait(self):
        while self.STATE == self.RUN_STATE:
551
            gevent.sleep(0)
552

553
    def run(self, entryPoint=None, wait=False):
554
555
556
557
558
        """ Execute program.

        entryPoint -- program name or a program label that
        indicates the point from where the execution should be carried out
        """
Emmanuel Papillon's avatar
Emmanuel Papillon committed
559
        # DBG_EPTR        self._eptr_debug = list()
560
561
562
563
        if entryPoint is None:
            self.putget("#RUN")
        else:
            self.putget("#RUN %s" % entryPoint)
564
565
        if wait:
            self._wait()
566

567
    def ct(self, acq_time=None, wait=True):
568
569
570
571
        """Starts the system timer, all the counting channels
        and the MCA. All the counting channels
        are previously cleared.

572
        time -- If specified, the counters run for that time (in s.)
573
        """
574
        self._timer_factor = self.get_timer_factor()
575
576
577
578
579
        diff = time.time() - self._last_run
        if diff < 0.02:
            gevent.sleep(0.02 - diff)
        if acq_time is not None:
            time_clock = acq_time * self._timer_factor
580
            self.putget("#RUNCT %d" % time_clock)
581
        else:
582
583
584
            self.putget("#RUNCT")
        if wait:
            self._wait()
585
        self._last_run = time.time()
586

587
    def upload_file(self, fname, prg_root=None, template_replacement={}):
588
589
590
591
592
593
594
595
596
        """ Load a program into the musst device.

        fname -- the file-name
        prg_root -- the base path where the program files are.
        if prg_root is None use the one in configuration
        template_replacement -- will be used to replace the key by the value
        in the program file
        """
        prg_root = prg_root or self.__prg_root
597

598
599
600
601
602
        if prg_root:
            program_file = os.path.join(prg_root, fname)
        else:
            program_file = fname

603
604
605
        with remote_open(program_file) as program:
            program_bytes = program.read()

606
        self.upload_program(program_bytes, template_replacement)
607

608
609
610
611
612
613
614
615
616
617
    def __replace_using_template(self, program_bytes, template_replacement):
        for old, new in template_replacement.items():
            if isinstance(old, str):
                old = old.encode()
            if isinstance(new, str):
                new = new.encode()
            program_bytes = program_bytes.replace(old, new)
        return program_bytes

    def upload_program(self, program_data, template_replacement={}):
618
619
620
621
        """ Upload a program.

        program_data -- program data you want to upload
        """
622
623
        if isinstance(program_data, str):
            program_data = program_data.encode()
624
        program_data = self.__replace_using_template(program_data, template_replacement)
625
626
627
628
629
630
        m = hashlib.md5()
        m.update(program_data)
        md5sum = m.hexdigest()
        if self.__last_md5.value == md5sum:
            return

631
        self.putget("#CLEAR")
632
633
634
        if self.__one_line_programing:
            # split into lines for Prologix
            for l in program_data.splitlines():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
635
                self._cnx.write(b"+%s\r\n" % l)
636
        else:
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
637
            prg = b"".join([b"+%s\r\n" % l for l in program_data.splitlines()])
638
            self._cnx.write(prg)
639
        if self.STATE != self.IDLE_STATE:
640
641
            err = self.putget("?LIST ERR")
            raise RuntimeError(err)
642
643
644

        self.__last_md5.value = md5sum

645
646
647
        return True

    #    def get_data(self, nlines, npts, buf=0):
648
    def get_data(self, nb_counters, from_event_id=0):
649
650
651
652
653
654
655
        """ Read event musst data.

        nb_counters -- number counter you have in your program storelist
        from_event_id -- from which event you want to read

        Returns event data organized by event_id,counters
        """
656
657

        buffer_size, nb_buffer = self.get_event_buffer_size()
658
        buffer_memory = buffer_size * nb_buffer
Emmanuel Papillon's avatar
Emmanuel Papillon committed
659
        for idx in range(5):
660
661
            curr_state = self.STATE
            current_offset, current_buffer_id = self.get_event_memory_pointer()
Emmanuel Papillon's avatar
Emmanuel Papillon committed
662
663
664
            gevent.sleep(100e-3)
            next_offset, next_buffer_id = self.get_event_memory_pointer()
            if next_offset >= current_offset and next_buffer_id >= current_buffer_id:
665
666
667
                break
            if curr_state != self.RUN_STATE:
                break
Emmanuel Papillon's avatar
Emmanuel Papillon committed
668
669
670
            # DBG_EPTR            self._eptr_debug.append(
            # DBG_EPTR                f"get_data filter {current_offset} {current_buffer_id} {next_offset} {next_buffer_id}"
            # DBG_EPTR            )
671
            gevent.sleep(100e-3)  # wait a little bit before re-asking
Emmanuel Papillon's avatar
Emmanuel Papillon committed
672
673
674
675
676
677

        # DBG_EPTR        if idx > 0:
        # DBG_EPTR            self._eptr_debug.append(
        # DBG_EPTR                f"get_data keep {current_offset} {current_buffer_id}"
        # DBG_EPTR            )

678
679
680
        current_offset = current_buffer_id * buffer_size + current_offset

        from_offset = (from_event_id * nb_counters) % buffer_memory
681
        current_offset = current_offset // nb_counters * nb_counters
682
        if current_offset >= from_offset:
683
            nb_lines = (current_offset - from_offset) // nb_counters
684
685
            data = numpy.empty((nb_lines, nb_counters), dtype=numpy.int32)
            self._read_data(from_offset, current_offset, data)
686
        else:
687
688
            nb_lines = (buffer_memory - from_offset + current_offset) // nb_counters
            data = numpy.empty((nb_lines * nb_counters,), dtype=numpy.int32)
689
            self._read_data(from_offset, buffer_memory, data)
690
691
            self._read_data(0, current_offset, data[buffer_memory - from_offset :])
            data.shape = (nb_lines, nb_counters)
Emmanuel Papillon's avatar
Emmanuel Papillon committed
692
        # DBG_EPTR        self._eptr_debug.append(f"get_data read {nb_lines*nb_counters}")
693
694
        return data

695
    def _read_data(self, from_offset, to_offset, data):
696
        BLOCK_SIZE = self.__block_size
697
        total_int32 = to_offset - from_offset
698
        data_pt = data.flat
699
        dt = numpy.dtype(numpy.int32)
700
        for offset, data_offset in zip(
Vincent Michel's avatar
Vincent Michel committed
701
            range(from_offset, to_offset, BLOCK_SIZE), range(0, total_int32, BLOCK_SIZE)
702
703
        ):
            size_to_read = min(BLOCK_SIZE, total_int32)
704
            total_int32 -= BLOCK_SIZE
705
706
707
708
            if self._binary_data_read:
                with self._cnx._lock:
                    self._cnx.open()
                    with KillMask():
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
709
                        self._cnx._write(b"?*EDAT %d %d %d" % (size_to_read, 0, offset))
710
                        raw_data = b""
711
                        while len(raw_data) < (size_to_read * 4):
712
                            raw_data += self._cnx.raw_read()
713
714
715
                        data_pt[
                            data_offset : data_offset + size_to_read
                        ] = numpy.frombuffer(raw_data, dtype=numpy.int32)
716
            else:
717
718
719
720
                raw_data = self.putget("?EDAT %d %d %d" % (size_to_read, 0, offset))
                data_pt[data_offset : data_offset + size_to_read] = [
                    int(x, 16) for x in raw_data.split(self._rxterm) if x
                ]
721
722
723
724
725

    def get_event_buffer_size(self):
        """ query event buffer size.
        Returns buffer size and number of buffers
        """
726
727
728
729
730
        event_buffer_size = self.__event_buffer_size.value
        if event_buffer_size is None:
            event_buffer_size = [int(x) for x in self.putget("?ESIZE").split()]
            self.__event_buffer_size.value = event_buffer_size
        return event_buffer_size
731

732
    def set_event_buffer_size(self, buffer_size, nb_buffer=1):
733
734
735
736
737
        """ set event buffer size.

        buffer_size -- request buffer size
        nb_buffer -- the number of allocated buffer
        """
738
        self.__event_buffer_size.value = None
739
        return self.putget("ESIZE %d %d" % (buffer_size, nb_buffer))
740
741
742
743
744
745
746

    def get_histogram_buffer_size(self):
        """ query histogram buffer size.
        Returns buffer size and number of buffers
        """
        return [int(x) for x in self.putget("?HSIZE").split()]

747
    def set_histogram_buffer_size(self, buffer_size, nb_buffer=1):
748
749
750
751
752
        """ set histogram buffer size.

        buffer_size -- request buffer size
        nb_buffer -- the number of allocated buffer
        """
753
        return self.putget("HSIZE %d %d" % (buffer_size, nb_buffer))
754
755
756
757
758
759

    def get_event_memory_pointer(self):
        """Query event memory pointer.

        Returns the current position of the event data memory pointer (offset,buffN)
        """
Emmanuel Papillon's avatar
Emmanuel Papillon committed
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
        for idx in range(5):
            buff_values = self.get_event_buffer_size()
            eptr_values = [int(x) for x in self.putget("?EPTR").split()]
            if eptr_values == [64, 0] or eptr_values == [256, 0]:
                # DBG_EPTR                self._eptr_debug.append(
                # DBG_EPTR                    f"eptr read filter {eptr_values[0]} {eptr_values[1]}"
                # DBG_EPTR                )
                gevent.sleep(100e-3)
                eptr_values = [int(x) for x in self.putget("?EPTR").split()]
            # DBG_EPTR            self._eptr_debug.append(f"eptr read {eptr_values[0]} {eptr_values[1]}")
            if (
                eptr_values[0] < buff_values[0]
                and eptr_values[1] >= 0
                and eptr_values[1] < buff_values[1]
            ):
                break
            # DBG_EPTR            self._eptr_debug.append(f"eptr filter {eptr_values[0]} {eptr_values[1]}")
            gevent.sleep(100e-3)

        return eptr_values

    # DBG_EPTR    def dump_eptr_debug(self, filename):
    # DBG_EPTR        df = open(filename, "w")
    # DBG_EPTR        df.writelines(map(lambda x: x + "\n", self._eptr_debug))
    # DBG_EPTR        df.close()
785

786
    def set_event_memory_pointer(self, offset, buff_number=0):
787
788
789
790
791
        """Set event memory pointer.

        Sets the internal event data memory pointer to point
        to the data position at offset <offset> in the buffer number <buff_number>.
        """
792
        return self.putget("EPTR %d %d" % (offset, buff_number))
793

794
795
796
797
798
799
800
801
802
    def get_variable_info(self, name):
        return self.putget("?VARINFO %s" % name)

    def get_variable(self, name):
        return float(self.putget("?VAR %s" % name))

    def set_variable(self, name, val):
        self.putget("VAR %s %s" % (name, val))

803
804
805
806
807
808
809
810
    @property
    def STATE(self):
        """ Query module state """
        return self._string2state.get(self.putget("?STATE"))

    @property
    def TMRCFG(self):
        """ Set/query main timer timebase """
811
812
813
        return self.__frequency_conversion[
            self.__frequency_conversion.get(self.putget("?TMRCFG"))
        ]
814
815

    def get_timer_factor(self):
816
        str_freq, freq = self.TMRCFG
817
        return freq
818

819
    @TMRCFG.setter
820
    def TMRCFG(self, value):
821
        if value not in self.__frequency_conversion:
822
823
            raise ValueError("Value not allowed")

824
        if not isinstance(value, str):
825
            value = self.__frequency_conversion.get(value)
826
827
        return self.putget("TMRCFG %s" % value)

828
    @lazy_init
829
    def get_channel(self, channel_id, type=None, switch=None, switch_name=None):
830
        if 0 < channel_id <= 6:
831
832
833
            return self.channel(
                self, channel_id, type=type, switch=switch, switch_name=switch_name
            )
834
835
        else:
            raise RuntimeError("musst doesn't have channel id %d" % channel_id)
836

837
    @lazy_init
838
    def get_channel_by_name(self, channel_name):
Cyril Guilloud's avatar
Cyril Guilloud committed
839
840
        """<channel_name>: Label of the channel.
        """
841
        channel_name = channel_name.upper()
842
843
        channels = self._channels.get(channel_name)
        if channels is None:
844
845
846
847
            raise RuntimeError(
                "musst doesn't have channel (%s) in his config" % channel_name
            )
        return channels[0]  # first match
848

849
    @lazy_init
850
    def get_channel_by_names(self, *channel_names):
Cyril Guilloud's avatar
Cyril Guilloud committed
851
852
        """<channel_names>: Labels of the channels.
        """
853
        channels = dict()
854
855
856
        for channel_name in channel_names:
            chans = self._channels.get(channel_name.upper())
            if chans is None:
857
858
859
                raise RuntimeError(
                    "musst doesn't have channel (%s) in his config" % channel_name
                )
860
861
862
863
864
865
            else:
                for chan in chans:
                    if chan.channel_id not in channels:
                        channels[chan.channel_id] = chan
                        break
                else:
866
867
868
                    raise RuntimeError(
                        "Can't find a free channel for (%s)" % channel_name
                    )
Vincent Michel's avatar
Vincent Michel committed
869
        return list(channels.values())
870

871
    # Add a read_all method to read counters
872
873
874
875
876
877
878
879
    def read_all(self, *counters):
        if len(counters) > 0:
            read_cmd = ""
            for cnt in counters:
                read_cmd = read_cmd + " " + cnt.channel
            read_cmd = "?VAL " + read_cmd
            val_str = self.putget(read_cmd)
            if val_str == "ERROR":
880
881
882
883
                raise RuntimeError(
                    "Musst (%s) Counter (%s): Error reading from Musst device"
                    % (cnt.controller.name, cnt.name)
                )
884
            val_float = [
885
                cnt.convert(val) for val, cnt in zip(val_str.split(" "), counters)
886
            ]
887

888
            return val_float
889

Perceval Guillou's avatar
Perceval Guillou committed
890
    @autocomplete_property
891
    @lazy_init
Perceval Guillou's avatar
Perceval Guillou committed
892
    def counters(self):
Perceval Guillou's avatar
Perceval Guillou committed
893
894
        cnts = [ctrl.counters for ctrl in self._counter_controllers.values()]
        return counter_namespace(chain(*cnts))
Perceval Guillou's avatar
Perceval Guillou committed
895

896
897

# Musst switch
898
899
900
class Switch(BaseSwitch):
    """
    This class wrapped musst command to emulate a switch.
Valentin Valls's avatar
Valentin Valls committed
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915

    The configuration may look like this:

    .. code-block::

        musst: $musst_name
        states:
           - label: OPEN
             set_cmd: "#BTRIG 1"
             test_cmd: "?BTRIG"
             test_cmd_reply: "1"
           - label: CLOSED
             set_cmd: "#BTRIG 0"
             test_cmd: "?BTRIG"
             test_cmd_reply: "0"
916
    """
917
918
919

    def __init__(self, name, config):
        BaseSwitch.__init__(self, name, config)
920
        self.__musst = None
921
922
        self.__states = dict()
        self.__state_test = dict()
923
924
925

    def _init(self):
        config = self.config
926
927
928
929
        self.__musst = config["musst"]
        for state in config["states"]:
            label = state["label"]
            cmd = state["set_cmd"]
930
931
            self.__states[label] = cmd

932
933
            t1 = {state["test_cmd_reply"]: label}
            t = self.__state_test.setdefault(state["test_cmd"], t1)
934
935
936
            if t1 != t:
                t.update(t1)

937
    def _set(self, state):
938
939
940
941
942
943
        cmd = self.__states.get(state)
        if cmd is None:
            raise RuntimeError("State %s don't exist" % state)
        self.__musst.putget(cmd)

    def _get(self):
Vincent Michel's avatar
Vincent Michel committed
944
        for test_cmd, test_reply in self.__state_test.items():
945
946
947
948
            reply = self.__musst.putget(test_cmd)
            state = test_reply.get(reply)
            if state is not None:
                return state
949
        return "UNKNOWN"
950
951

    def _states_list(self):
Vincent Michel's avatar
Vincent Michel committed
952
        return list(self.__states.keys())
Perceval Guillou's avatar
Perceval Guillou committed
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000


class MusstMock(musst):
    class FakeCnx:
        def __init__(self):
            self._lock = gevent.lock.RLock()
            self.last_cmd = None

        def __info__(self):
            return "fake connection to a musst card"

        def open(self):
            pass

        def _readline(self, rxterm):
            cmd = self.last_cmd
            self.last_cmd = None
            # print(cmd)
            if cmd.startswith("?"):
                if cmd.startswith("?TMRCFG"):
                    return b"10KHZ"

                elif cmd.startswith("?HSIZE"):
                    return b"0 0"

                elif cmd.startswith("?ESIZE"):
                    return b"0 0"

                elif cmd.startswith("?VAR"):
                    return b"0"

                elif cmd.startswith("?LIST ERR"):
                    return b"".encode()

                elif cmd.startswith("?EDAT"):
                    return b"0 0 0 0 0 0 0 0 0 0 0"

                elif cmd.startswith("?VAL"):
                    return b"0 0 0 0 0 0 0 0 0 0 0"

                elif cmd.startswith("?VER"):
                    return b"fake card"

                elif cmd.startswith("?CHCFG"):
                    return b""

                elif cmd.startswith("?CH"):
                    h, v = cmd.split()