|
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> |
2 | 2 |
|
3 | 3 | This work is licensed under the Creative Commons |
4 | 4 | Attribution-ShareAlike 4.0 International License. To view a copy of |
|
7 | 7 | Support for New Device |
8 | 8 | ********************** |
9 | 9 |
|
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:: |
14 | 11 |
|
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. |
18 | 22 |
|
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