dev_write_ctrl.md 20 KB
Newer Older
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
1
# Writing controllers for Bliss
Cyril Guilloud's avatar
Cyril Guilloud committed
2

Perceval Guillou's avatar
add doc    
Perceval Guillou committed
3
4
Bliss put no constrains on controllers classes and developers can start from scratch and define everything.
However, there are several generic mechanisms, like loading the controller from a YML configuration (plugin) or managing the controller's counters and axes, which are already defined in Bliss and which can be inherited while writing a new controller class.
Cyril Guilloud's avatar
Cyril Guilloud committed
5

Perceval Guillou's avatar
add doc    
Perceval Guillou committed
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
## BlissController base class

As a base for the implementation of controllers, Bliss provides the `BlissController` class.
This class already implements the plugin mechanisms and is designed to ease the management of sub-objects under a top controller. 

Examples of controllers that should inherit from `BlissController` class:

- a controller of axes

- a controller with counters

- a controller with axes and counter

- a top-controller (software) managing other sub-controllers (software/hardware)


Example of the YML structure:

```yml

- plugin: bliss_controller <== the dedicated BlissController plugin
  module: custom_module    <== module of the custom bliss controller
  class: BCMockup          <== class of the custom bliss controller
  name: bcmock             <== name of the custom bliss controller  (optional)

  param_1: value           <== a parameter for the custom bliss controller (optional)

  section_1:               <== a section where subitems config can be declared (ex: 'counters') (optional) 
    - name: subitem_1      <== name of a subitem
    - name: subitem_2      <== name of another subitem of the same type

  section_2:               <== another section for another type of subitems (ex: 'axes') (optional) 
    - name: subitem_2      <== name of another subitem type

```

The signature of a `BlissController` takes a single argument `config`.
It could be a `ConfigNode` object or a standard dictionary.

```python
class BlissController(CounterContainer):
    def __init__(self, config):
```

### BlissController and subitems

A `BlissController` subitem is an object managed by the controller and which could have a name declared under a sub-section of the controller's configuration. Usually subitems are counters and axes but could be anything else (known by the controller only). 

```yml

  section_1:
    - name: subitem_1   <== a subitem using the default class (defined by the controller)
    
    - name: subitem_2   <== a subitem using a given class path (from an absolute path)
      class: bliss.foo.custom.myclass

    - name: subitem_3   <== a subitem using a given class name (default path known by the controller)
      class: myitemclass    
```

Subitems can be declared in the controller's YML configuration if they are expected to be directly imported in a user session.
If not declared in the YML, they are still accessible via the controller (see `BlissController._get_subitem(name)`).

To retrieve the subitems that can be identified as counters or axes, `BlissController` class implements the `@counters` and `@axes` properties.

The `BlissController` identifies the subitem type thanks to the name of the sub-section where the item was found (aka `parent_key`).

Also, the controller must provides a default class for each kind of `parent_key` (see `BlissController._get_subitem_default_class_name`). 

Examples:

```python
def _get_subitem_default_class_name(self, cfg, parent_key):
    if parent_key == "axes":
        return "Axis"
    elif parent_key == "encoders":
        return "Encoder"
    elif parent_key == "shutters":
        return "Shutter"
    elif parent_key == "switches":
        return "Switch"
```

or

```python
def _get_subitem_default_class_name(self, cfg, parent_key):
    if parent_key == "counters":
        tag = cfg["tag"]
        if self._COUNTER_TAGS[tag][1] == "scc":
            return "SamplingCounter"
        elif self._COUNTER_TAGS[tag][1] == "icc":
            return "IntegratingCounter"
```


The default subitem class can be overridden by specifing the `class` key in its configuration.
The class can be given as an absolute path or as a class name. 

If providing a class name the controller first look at its module level to find the item class, else it uses a default path defined by the controller (see `BlissController._get_subitem_default_module`).

Examples:

```python
def _get_subitem_default_module(self, class_name, cfg, parent_key):
    if parent_key == "axes":
        return "bliss.common.axis"

    elif parent_key == "encoders":
        return "bliss.common.encoder"

    elif parent_key == "shutters":
        return "bliss.common.shutter"

    elif parent_key == "switches":
        return "bliss.common.switch"
```

or

```python
def _get_subitem_default_module(self, class_name, cfg, parent_key):
    if class_name == "IntegratingCounter":
        return "bliss.common.counter"
```

### Bliss controller plugin

`BlissControllers` are created from the yml configuration using the `bliss_controller` plugin.

Any subitem can be imported in a Bliss session with the command `config.get('name')`.

The bliss controller itself can have a name (optional) and can be imported in the session.

The plugin ensures that the controller and subitems are only created once.

The effective creation of subitems is performed by the `BlissController` itself and the plugin just ensures that the controller is always created before subitems and only once.

The `bliss_controller` plugin will also manage the resolution order of the references to other objects within the `BlissController` configuration. It handles external and internal references and allows to use a reference for a subitem name.


Example of an advanced configuration using different kind of references:

```yml

- plugin: bliss_controller    
  module: custom_module       
  class: BCMockup             
  name: bcmock                

  custom_param_1: value       
  custom_param_2: $ref1       <== a referenced object for the controller (optional/authorized)

  sub-section-1:              
    - name: sub_item_1        
      tag : item_tag_1        
      sub_param_1: value      
      device: $ref2           <== an external reference for this subitem (optional/authorized)

  sub-section-2:              
    - name: sub_item_2        
      tag : item_tag_2        
      input: $sub_item_1      <== an internal reference to another subitem owned by the same controller (optional/authorized)

      sub-section-2-1:        <== nested sub-sections are possible (optional)
        - name: sub_item_21
          tag : item_tag_21

  sub-section-3 :             
    - name: $ref3             <== a subitem as an external reference is possible (optional/authorized)
      something: value

```

### Subitem creation

In order to keep the plugin as generic as possible, all the knowledge specfic to the controller is asked by the plugin to the `BlissController`. 

In particular, when the plugin needs to instantiate a subitem it will call the method `BlissController._create_subitem_from_config`. This abstract method must be implemented and must return the subitem instance.

To be able to decide which instance should be created, the method receives 4 arguments:

- `name`: subitem name
- `cfg`: subitem config
- `parent_key`: name of the subsection where the item was found (in controller's config)
- `item_class`: class for the subitem (see [BlissController and sub-items](dev_write_ctrl.md#BlissController-and-subitems) ).
192
193
- `item_obj`: the object instance for item as a reference (None if not a reference)
If `item_class` is `None` then the subitem is a reference and the object exist already and is contained in `item_obj`.
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
194
195
196
197
198
199
  

Examples:

```python
@check_disabled
200
def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
201
202

    if parent_key == "axes":
203
204
        if item_class is None:  # it is a reference
            axis = item_obj
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
        else:
            axis = item_class(name, self, cfg)

        self._axes[name] = axis

        axis_tags = cfg.get("tags")
        if axis_tags:
            for tag in axis_tags.split():
                self._tagged.setdefault(tag, []).append(axis)

        if axis.controller is self:
            set_custom_members(self, axis, self._initialize_axis)
        else:
            # reference axis
            return axis

        if axis.controller is self:
            axis_initialized = Cache(axis, "initialized", default_value=0)
            self.__initialized_hw_axis[axis] = axis_initialized
            self.__initialized_axis[axis] = False

        self._add_axis(axis)
        return axis

    elif parent_key == "encoders":
        encoder = self._encoder_counter_controller.create_counter(
            item_class, name, motor_controller=self, config=cfg
        )
        self._encoders[name] = encoder
        self.__initialized_encoder[encoder] = False
        return encoder
```

or

```python

242
def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
    if parent_key == "counters":
        name = cfg["name"]
        tag = cfg["tag"]
        mode = cfg.get("mode")
        unit = cfg.get("unit")
        convfunc = cfg.get("convfunc")

        if self._COUNTER_TAGS[tag][1] == "scc":
            cnt = self._counter_controllers["scc"].create_counter(
                item_class, name, unit=unit, mode=mode
            )
            cnt.tag = tag

        elif self._COUNTER_TAGS[tag][1] == "icc":
            cnt = self._counter_controllers["icc"].create_counter(
                item_class, name, unit=unit
            )
            cnt.tag = tag

        else:
            raise ValueError(f"cannot identify counter tag {tag}")

        return cnt

    elif parent_key == "operators":
        return item_class(cfg)

    elif parent_key == "axes":
271
272
        if item_class is None:  # it is a referenced axis (i.e external axis)
            axis = item_obj  # the axis instance
Perceval Guillou's avatar
add doc    
Perceval Guillou committed
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
            tag = cfg[
                "tag"
            ]  # ask for a tag which only concerns this ctrl (local tag)
            self._tag2axis[tag] = name  # store the axis tag
            return axis
        else:
            raise ValueError(
                f"{self} only accept referenced axes"
            )  # reject none-referenced axis
```



### Nested BlissControllers

A top-bliss-controller can have multiple sub-bliss-controllers. 
In that case there are two ways to create the sub-bliss-controllers:

The most simple way to do this is to declare a sub-bliss-controller as an independant object with its own yml config and use a reference to this object into the top-bliss-controller config.

Else, if a sub-bliss-controller has no reason to exist independently from the top-bliss-controller, then the top-bliss-controller will create and manage its sub-bliss-controllers from the knowledge of the top-bliss-controller configuration only.

In the second case, some items declared in the top-bliss-controller are, in fact, managed by one of the sub-bliss-controllers.
Then, the author of the top-bliss-controller class must overload the `_get_item_owner` method and specify which is the sub-bliss-controller that manages which items.

Example: 

Consider a top-bliss-controller which has internally another sub-bliss-controller that manages pseudo axes.
(`self._motor_controller = AxesBlissController(...)`)

```yml

- plugin: bliss_controller    
  module: custom_module       
  class: BCMockup             
  name: bcmock                

  axes:              
    
    - name: $xrot
      tags: real xrot

    - name: $yrot
      tags: real yrot

    - name: axis_1        
      tag : theta


```
 
The top-bliss-controller configuration declares the axes subitems but those items are in fact managed by the motors controller (`self._motor_controller`).

In that case, developers must override the `self._get_item_owner` method to specify the subitems that are managed by `self._motor_controller` instead of `self`.

```python
def _get_item_owner(self, name, cfg, pkey):
    """ Return the controller that owns the items declared in the config.
        By default, this controller is the owner of all config items.
        However if this controller has sub-controllers that are the real owners 
        of some items, this method should use to specify which sub-controller is
        the owner of which item (identified with name and pkey). 
    """
    if pkey == "axes":
        return self._motor_controller
    else:
        return self
```

The method receives the item name and the `parent_key`. So `self._motor_controller` can be associated to all subitems under the `axes` parent_key (instead of doing it for each subitem name).


### Direct instantiation 

A BlissController can be instantiated directly (i.e. not instantiated by the plugin) providing a configuration as a dictionary.

In that case, users must call the method `self._controller_init()` just after the controller instantiation to ensure that the controller is initialized in the same way as the plugin does.

The config dictionary should be structured like a YML file (i.e: nested dict and list) and references replaced by their corresponding object instances.

Example: `bctrl = BlissController( config_dict )` => `bctrl._controller_init()`


## Other tips

### @autocomplete_property decorator
359

360
361
In many controllers, the `@property` decorator is heavily used to protect certain
attributes of the instance or to limit the access to read-only. When using the
362
bliss command line interface the autocompletion will **not** suggest any
363
completion based on the return value of the method underneath the property.
364

365
This is a wanted behavior e.g. in case this would trigger hardware
366
communication. There are however also use cases where a *deeper* autocompletion
367
is wanted.
368

369
370
!!! note
     "↹" represents the action of pressing the "Tab" key of the keyboard.
371

372
373
374
375
376
Example: the `.counter` namespace of a controller. If implemented as
`@property`:
```
BLISS [1]: lima_simulator.counters. ↹
```
377

378
379
380
381
Would not show any autocompletion suggestions. To enable *deeper* autocompletion
a special decorator called `@autocomplete_property` must be used.
```python
from bliss.common.utils import autocomplete_property
Cyril Guilloud's avatar
Cyril Guilloud committed
382

383
384
385
386
387
388
389
390
391
392
393
394
395
396
class Lima(object):
    @autocomplete_property
    def counters(self):
        all_counters = [self.image]
        ...
```

Using this decorator would result in autocompletion suggestions:
```
BLISS [1]: lima_simulator.counters. ↹
                                   _roi1_
                                   _roi2_
                                   _bpm_
```
Linus Pithan's avatar
Linus Pithan committed
397

Perceval Guillou's avatar
add doc    
Perceval Guillou committed
398
### The `__info__()` method for Bliss shell
Linus Pithan's avatar
Linus Pithan committed
399

400
401
402
403
404
405
!!! info

    - Any Bliss controller that is visible to the user in the command line
      should have an `__info__()` function implemented!
    - The return type of `__info__()` must be `str`, otherwhise it fails and
      `__repr__()` is used as fallback!
Cyril Guilloud's avatar
Cyril Guilloud committed
406
    - As a rule of thumb: the return value of a custom `__repr__()` implementation
407
408
409
410
411
412
413
      should not contain `\n` and should be inspired by the standard
      implementation of `__repr__()` in python.

In Bliss, `__info__()` is used by the command line interface (Bliss shell or Bliss
repl) to enquire information of the internal state of any object / controller in
case it is available.

414
415
416
417
418
That way, a user can get information how to use the object, detailed
**from the user perspective**. This is in contrast to the built-in python function
`__repr__()`, which should return a short summary of the concerned object from
the **developer perspective**. The Protocol that is put in place in the Bliss
shell is the following:
419
420
421
422
423
424

* if the return value of a statement entered into the Bliss shel is a python
  object with `__info__()` implemented this `__info__()` function will be called
  by the Bliss shell to display the output. As a fallback option (`__info__()`
  not implemented) the standard behavior of the interactive python interpreter
  involving `__repr__` is used. (For details about `__repr__` see next section.)
Linus Pithan's avatar
Linus Pithan committed
425
426
427

Here is an example for the lima controller that is using `__info__`:
```
428
LIMA_TEST_SESSION [3]: lima_simulator
Linus Pithan's avatar
Linus Pithan committed
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
              Out [3]: Simulator - Generator (Simulator) - Lima Simulator
                       
                       Image:
                       bin = [1 1]
                       flip = [False False]
                       height = 1024
                       roi = <0,0> <1024 x 1024>
                       rotation = rotation_enum.NONE
                       sizes = [   0    4 1024 1024]
                       type = Bpp32
                       width = 1024
                       
                       Acquisition:
                       expo_time = 1.0
                       mode = mode_enum.SINGLE
                       nb_frames = 1
                       status = Ready
                       status_fault_error = No error
                       trigger_mode = trigger_mode_enum.INTERNAL_TRIGGER
                       
                       ROI Counters:
                       [default]
                       
                       Name  ROI (<X, Y> <W x H>)
                       ----  ------------------
                         r1  <0, 0> <100 x 200>
```

457
458
459
460
461
462
463
464
465
The information given above is usefull from a **user point of view**. As a
**developer** one might want to work in the Bliss shell with live object e.g.

```python
LIMA [4]: my_detectors = {'my_lima':lima_simulator,'my_mca':simu1}
LIMA [5]: my_detectors
 Out [5]: {'my_lima': <Lima Controller for Simulator (Lima Simulator)>,
                        'my_mca': <bliss.controllers.mca.simulation.SimulatedMCA
                                   object at 0x7f2f535b5f60>}
Linus Pithan's avatar
Linus Pithan committed
466
467
```

468
In this case, it is desirable that the python objects themselves are clearly
469
470
471
represented, which is exactly the role of `__repr__` (in this example the
`lima_simulator` has a custom `__repr__` while in `simu1` there is no `__repr__`
implemented so the bulid in python implementation is used).
Linus Pithan's avatar
Linus Pithan committed
472

473
The signature of `__info__()` should be `def __info__(self):` the return value
Valentin Valls's avatar
Valentin Valls committed
474
must be a string.
475
476
477
478
479
480
481
482
483

```python
BLISS [1]: class A(object):
      ...:     def __repr__(self):
      ...:         return "my repl"
      ...:     def __str__(self):
      ...:         return "my str"
      ...:     def __info__(self):
      ...:         return "my info"
Linus Pithan's avatar
Linus Pithan committed
484

485
BLISS [2]: a=A()
Linus Pithan's avatar
Linus Pithan committed
486

487
BLISS [3]: a
Linus Pithan's avatar
Linus Pithan committed
488
489
  Out [3]: my info

490
BLISS [4]: [a]
Linus Pithan's avatar
Linus Pithan committed
491
492
493
  Out [4]: [my repl]
```

494
495
496
497
498
!!! warning

    If, for any reason, there is an exception raised inside `__info__`, the
    fallback option will be used and `__repr__` is evaluated in this case.

499
    And **this will hide the error**. So, *any* error must be treated
Cyril Guilloud's avatar
Cyril Guilloud committed
500
    before returning.
Linus Pithan's avatar
Linus Pithan committed
501

502

503
504
505
506
507
508
509
510
    Example:
    ```python
        def __info__(self):
            info_str = "bla \n"
            info_str += "bli \n"

            return info_str
    ```
511

512
The equivalent of `repr(obj)` or `str(obj)` is also available in
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
513
`bliss.shell.standard` as `info(obj)` which can be used also outside the Bliss
514
shell.
Linus Pithan's avatar
Linus Pithan committed
515
516

```
Cyril Guilloud's avatar
Cyril Guilloud committed
517
Python 3.7.3 (default, Mar 27 2019, 22:11:17)
Linus Pithan's avatar
Linus Pithan committed
518
519
520
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
521
>>> from bliss.shell.standard import info
Linus Pithan's avatar
Linus Pithan committed
522
523
524
525
526
527

>>> class A(object):
...     def __repr__(self):
...          return "my repl"
...     def __info__(self):
...          return "my info"
Cyril Guilloud's avatar
Cyril Guilloud committed
528
...
Linus Pithan's avatar
Linus Pithan committed
529
530
531
532
533
534
>>> info(A())
'my info'

>>> class B(object):
...     def __repr__(self):
...          return "my repl"
Cyril Guilloud's avatar
Cyril Guilloud committed
535
...
Linus Pithan's avatar
Linus Pithan committed
536
537
538
539
540

>>> info(B())
'my repl'
```

541
## `__str__()` and `__repr__()`
Cyril Guilloud's avatar
Cyril Guilloud committed
542
543

If implemented in a Python class, `__repr__` and `__str__` methods are
Linus Pithan's avatar
Linus Pithan committed
544
build-in functions Python to return information about an object instantiating this class.
Cyril Guilloud's avatar
Cyril Guilloud committed
545
546

* `__str__` should print a readable message
547
* `__repr__` should print a __short__ message about the object that is unambiguous (e.g. name of an identifier, class name, etc.).
Cyril Guilloud's avatar
Cyril Guilloud committed
548

549
550
551
552
553
554
555
556
557
558
* `__str__` is called:
    - when the object is passed to the print() function (e.g. `print(my_obj)`).
    - wheh the object is used in string operations (e.g. `str(my_obj)` or
      `'{}'.format(my_obj)` or `f'some text {my_obj}'`)
* `__repr__` method is called:
    - when user type the name of the object in an interpreter session (a python
      shell).
    - when displaying containers like lists and dicts (the result of `__repr__`
      is used to represent the objects they contain)
    - when explicitly asking for it in the print() function. (e.g. `print("%r" % my_object)`)
Cyril Guilloud's avatar
Cyril Guilloud committed
559
560


561
562
By default when no `__str__` or `__repr__` methods are defined, the `__repr__`
returns the name of the class (Length) and `__str__` calls `__repr__`.