Skip to content

Commit 2e496c4

Browse files
committed
doc: write section with tips on how to add support for new devices.
1 parent 5fc81d6 commit 2e496c4

File tree

1 file changed

+250
-9
lines changed

1 file changed

+250
-9
lines changed

doc/get-involved/new-device.rst

Lines changed: 250 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. Copyright (C) 2020 David Miguel Susano Pinto <david.pinto@bioch.ox.ac.uk>
1+
.. Copyright (C) 2021 David Miguel Susano Pinto <david.pinto@bioch.ox.ac.uk>
22
33
This work is licensed under the Creative Commons
44
Attribution-ShareAlike 4.0 International License. To view a copy of
@@ -7,13 +7,254 @@
77
Support for New Device
88
**********************
99

10-
If you wish to add support for a new device first check if there is
11-
already an open issue about it. If not, please open a new issue
12-
before starting to work on it. This ensures that there is no
13-
duplication of work.
10+
.. note::
1411

15-
If you do implement a new device please ensure that you add the
16-
relevant information to the supported device list which is in
17-
doc/architecture/supported devices.rst .
12+
Before starting the work of adding a new device, check the `issue
13+
tracker <https://github.com/python-microscope/microscope/issues>`__
14+
first. It is likely that someone already requested it and maybe
15+
someone is already working on it. Even if they are not working on
16+
it they might have a slightly different model and willing to test
17+
your implementation on it. Sometimes there is already a
18+
half-working implementation that needs testing. If there is no
19+
related issue on the tracker, open one. If there is one, comment
20+
that you're working on it. This ensures that there is no
21+
duplication of work.
1822

19-
.. The plan is to fill this with tips to write that code.
23+
Adding support for a new device is often a relatively straightforward
24+
job. Doing so means:
25+
26+
1. Identify the correct device type and the corresponding abstract
27+
base class in the :mod:`microscope.abc` module. For example,
28+
:class:`Camera <microscope.abc.Camera>`, :class:`Controller
29+
<microscope.abc.Controller>`, or :class:`LightSource
30+
<microscope.abc.LightSource>`.
31+
32+
2. Read the documentation for that class which lists the abstract
33+
methods and properties that need to be implemented.
34+
35+
3. Create a concrete class that implement those abstract methods and
36+
properties using the documentation provided by the device vendor.
37+
38+
.. note::
39+
40+
As one can imagine, this apparently simple 3 step process hides a
41+
lot of complexity, most of it in the last step. Surprisingly, the
42+
difficulty is not in writing the actual code, which is often really
43+
simple, but in decoding the device documentation.
44+
45+
Besides the different device types, which define what methods need to
46+
be implemented, devices can also be grouped by their communication
47+
method. When it comes to this, most devices fall under two
48+
categories: serial connection or C library.
49+
50+
51+
Serial Communication
52+
====================
53+
54+
Most microscope devices will provide a RS-232 serial interface,
55+
sometimes with USB to serial bridges. Typical exceptions are devices
56+
that need to transfer large amounts of data such as cameras or
57+
deformable mirrors. Devices with serial interface are the easiest to
58+
control, one only needs to find the correct commands in the manual.
59+
Here's some tips to create a new device using serial communication:
60+
61+
1. Create a class that wraps the serial connection and provides the
62+
different commands as Python methods. The device object then "has
63+
a" device connection object, and the device connection object "has
64+
a" serial connection object. This will greatly simplify the code
65+
reducing most methods to 1-2 lines of code.
66+
67+
2. Beware of multiple threads controlling the device and note that
68+
GUIs will often have multiple threads. Consider using
69+
:class:`microscope._utils.SharedSerial` or roll your own
70+
synchronisation logic to ensure thread safety.
71+
72+
3. The first argument to the class initialiser should be the port
73+
number which identifies the device. Beware that the assigned port
74+
number might change each time the computer is restarted or even
75+
when the device itself is restarted. Checks your OS documentation
76+
to assign fixed port number/name.
77+
78+
4. Beware that indexing a single byte from a byte array returns an int
79+
and not a byte, i.e.::
80+
81+
b"CMD"[0] == b"C" # False
82+
b"CMD"[0] == 65 # True
83+
b"CMD"[0] == ord(b"C") # True
84+
b"CMD"[0:1] == b"C" # True
85+
86+
However, typically the goal often is to compare the character at a
87+
specific position with a specific character that signals error or
88+
success. Note only do we know the exact character this will be
89+
done pretty much every time a command is sent to the device. So
90+
declare the value in the module globals and use it internally, like
91+
so::
92+
93+
_K_CODE = ord(b"K")
94+
95+
96+
C libraries / SDKs
97+
==================
98+
99+
When a device is not controlled via serial it is most likely
100+
controlled via some vendor provided SDK. In these cases, adding
101+
support for such device means:
102+
103+
1. find the C library for the SDK;
104+
2. create a :mod:`ctypes` wrapper to it under `microscope._wrappers`;
105+
3. use the wrapper to add support for the new device, possibly with an
106+
intermediary wrapper.
107+
108+
.. note::
109+
110+
Some vendors provide Python bindings to their SDKs which may or may
111+
not be worth using. Often they are undocumented thin wrappers to
112+
their C library and if you use them, not only will you have to deal
113+
with undocumented behaviour from their C library you will also have
114+
to deal with the undocumented idiosyncrasies of their wrapper.
115+
116+
Finding the C library
117+
---------------------
118+
119+
The first thing to do is to identify the correct shared library file.
120+
i.e., the C libraries. On Windows, these are often called dll files.
121+
Sometimes the SDK is in C++ and there will be C bindings available.
122+
Other times, there are SDKs in many different languages and one needs
123+
to get the "low level libraries" for those SDKs. It may be required
124+
to contact the vendor directly.
125+
126+
There may be more than one C library required for a single device.
127+
For example, Andor's SDK3 requires the DLLs ``atcore`` and
128+
``atutility``.
129+
130+
ctypes wrapper
131+
--------------
132+
133+
For each library file create one Python file with the same name under
134+
`microscope._wrappers`. Each of those files should load the library,
135+
declare required constants and structures, and finally declare the
136+
function prototypes with the required argument types and return
137+
values. Take a look at the existing wrappers for examples but here's
138+
some tips to write a new one:
139+
140+
1. Keep the wrapper as thinner as possible. Namely, do not have
141+
functions automatically check the return value or convert types.
142+
The wrapper should provide the exact same interface provided by the
143+
C library but callable from Python. That said, do specify the
144+
required arguments and return type by setting the ``argtypes`` and
145+
``restype`` arguments.
146+
147+
2. Wrap only the symbols required by Python-microscope and not every
148+
single symbol declared in the header file. Wrapping only the
149+
required functions ensures that it will work with any version of
150+
the library that has the required functions. On the other hand,
151+
wrapping all the symbols may lead to failures with older library
152+
versions because they miss something that is not even required.
153+
154+
3. Use the exact same names as in the C header files even if they
155+
don't follow Python naming conventions. However, it is very common
156+
for C libraries to use a prefix for all their functions, e.g. the
157+
``mirao52e`` and ``BMC`` libraries prefix all their functions with
158+
``mro_`` and ``BMC`` respectively. In such case, remove that
159+
prefix.
160+
161+
4. Typedefs are often used for function arguments, e.g., ``RESULT`` is
162+
the return type for all functions which is a typedef for ``int`` or
163+
``HANDLE`` which is a pointer for some forwarded declared struct.
164+
Do declare those typedefs and use them when declaring the arguments
165+
and return types of functions. This eases the comparison with the
166+
header files and the long-term maintenance.
167+
168+
5. Importing the wrapper should load the library, i.e., will call
169+
``ctypes.CDLL`` or similar. This ensures that if Python fails to
170+
find the library this will fail as soon as possible. However, some
171+
libraries need to be "manually" initialised. Importing the wrapper
172+
should not initialise the library, leave it to the user of the
173+
library.
174+
175+
6. Not all Window's DLLs use ``stdcall`` so don't assume that you need
176+
to use ``ctypes.WinDLL`` just because you are in Windows. Also,
177+
using ``WinDLL`` incorrectly instead of ``CDLL`` will not fail but
178+
may lead to issues later. So check the header files and look for
179+
``__cdecl`` or ``__stdcall`` declarations.
180+
181+
7. Different structs may have different packing alignment. Check it,
182+
i.e., look for ``#pragma pack`` and ``__atribute__((packed,
183+
aligned(X)))``, and set it appropriately via the ``_pack_`` class
184+
attribute.
185+
186+
8. Do not do wildcard imports, i.e., no ``from ctypes import *``.
187+
188+
Actual device class
189+
-------------------
190+
191+
Because the thin wrapper should only declare the symbols required by
192+
the concrete device class these two should be implemented in parallel.
193+
Details on how to implement this devices are mainly device type
194+
specific.
195+
196+
197+
Tips to implement support for a new device
198+
==========================================
199+
200+
1. Only use named arguments and keyword arguments for the class
201+
``__init__``. This is required by the device server and also
202+
makes things simpler when there's multiple parent classes.
203+
204+
2. Avoid using the :class:`FloatingDeviceMixin
205+
<microscope.abc.FloatingDeviceMixin>` if possible. Some devices
206+
really need it, namely cameras, but these cause issues when there
207+
are multiple such devices available but only a subset is to be
208+
used.
209+
210+
3. While often the end goal is to use the devices via the device
211+
server, avoid using it during development since it adds an extra
212+
layer of complexity. Do test that it works via the device server
213+
in the end though.
214+
215+
4. Make use of the :mod:`microscope.gui` module which provides simple
216+
widgets to quickly test the device during development. For
217+
example, if one was testing the implementation of a deformable
218+
mirror, they could do this on a Python shell::
219+
220+
from microscope.mirrors.my_new_dm import MyNewDM
221+
222+
dm = MyNewDM()
223+
224+
from microscope.gui import DeformableMirrorWidget, MainWindow
225+
from qtpy import QtWidgets
226+
227+
app = QtWidgets.QApplication([])
228+
widget = DeformableMirrorWidget(dm)
229+
window = MainWindow(widget)
230+
window.show()
231+
app.exec()
232+
233+
5. When documenting support for the device, use the class docstring
234+
and not the module docstring. Use the module docstring if there
235+
are multiple device classes in the module and they share
236+
documentation.
237+
238+
6. Use `type annotations <https://docs.python.org/3/library/typing.html>`__.
239+
240+
7. When all is done and support for a new device is merged, do not
241+
forget to make reference to it on the `NEWS.rst` and
242+
`doc/architecture/supported-devices.rst` files.
243+
244+
Vendor issues
245+
-------------
246+
247+
More often than not a device does not really perform according to
248+
their documentation. The documentation rarely includes all of the
249+
available commands, the description or arguments in the documentation
250+
is wrong, different models behave slightly different despite using the
251+
same SDK, and changing settings have surprising side effects. Despite
252+
all this defects, vendors tend to be very protective of their
253+
documentation and can be complicated to get a copy of it --- it's
254+
almost as if they don't want us to use it.
255+
256+
Anyone implementing support for a new device is bound to find issues
257+
with the vendor interface. In that case, please be a good citizen and
258+
report it back to them so that they can improve. In addition, open an
259+
issue on Python-Microscope tracker for `vendor issues
260+
<https://github.com/python-microscope/vendor-issues>`__.

0 commit comments

Comments
 (0)